Android插件化(一) 动态加载技术

Android支持动态加载的两种方式是:DexClassLoader和PathClassLoader,DexClassLoader可加载jar/apk/dex,且支持从SD卡加载;PathClassLoader只能加载已经安装在Android系统内APK文件( /data/app 目录下),其它位置的文件加载的时候都会出现 ClassNotFoundException。 因为 PathClassLoader 会去读取 /data/dalvik-cache 目录下的经过 Dalvik 优化过的 dex 文件,这个目录的 dex 文件是在安装 apk 包的时候由 Dalvik 生成的,没有安装的时候,自然没有生成那个文件。

我们现在要实现的一个需求是:如何调用一个非本应用的java程序,如下:


豆芽图片20171130151459511.png

app 与loutillib两个模块没有任何的依耐关系,在Module App中,我们想调用Loutillib中的LogUitl输出一条log。

所以我们采取dexClassLoader来加载这个jar

第一步:

首先我们将logutil这个module 依赖主app 然后运行下,这是时候会生成class.jar
然后在logutil中添加如下代码: 将class.jar重新命名下log.jar

task makeJar(type:Copy){
    delete 'build/libs/log.jar'
    from('build/intermediates/bundles/release/')
    into('build/libs/')
    include('classes.jar')
    rename ('classes.jar', 'log.jar')
    exclude('test/','BuildConfig.class','R.class')
    exclude{it.name.startsWith('R$');}
}

makeJar.dependsOn(build)

上面配置好gradle 环境变量后,就可以再终端执行: gradlew makeJar

注意,这个jar还不能被加载,这个是基于class的jar,Dalvik虚拟机加载的是dex字节码,所以需要将class转化为dex字节码。这个需要用到dx命令,这个可以在Android\sdk\build-tools\23.0.0中找到,把log.jar拷贝到这个目录下,执行

dx --dex --output=new_log.jar log.jar

生成新的jar 然后push 到sdcard中

adb  push  new_log.jar  sdcard/

以上准备工作 做好了之后, 第二步就开始撸DexClassLoader的代码了。

第二步:直接贴上代码,利用java 类加载
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    //动态加载dex文件  这里是个jar 包里面的方法
    public void start(View view) {
        //dex解压释放后的目录
        final File dexOutPutDir = getDir("dex", 0);
        //dex所在目录
        final String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "new_log.jar";

        //第一个参数:是dex压缩文件的路径
        //第二个参数:是dex解压缩后存放的目录
        //第三个参数:是C/C++依赖的本地库文件目录,可以为null
        //第四个参数:是上一级的类加载器
        DexClassLoader classLoader = new DexClassLoader(dexPath, dexOutPutDir.getAbsolutePath(), null, getClassLoader());

        try {
            final Class<?> loadClazz = classLoader.loadClass("plugin.charles.com.logutil.LogUitl");
            final Object o = loadClazz.newInstance();
            final Method printLogMethod = loadClazz.getDeclaredMethod("printLog");
            printLogMethod.setAccessible(true);
            printLogMethod.invoke(o);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

下面介绍的是动态加载未安装的apk.然后读取apk中图片,显示在宿主app中
1.首先准备换肤的apk

 skindemo-debug.apk
 skindemo2-debug.apk
 这2个apk中都有一张图片,然后我们push这两个apk到sdcard中就可以
  1. 资源加载问题,怎么通过Resources 加载另外一个apk中的图片
    /**
     * 获取AssetManager   用来加载插件资源
     *
     * @param pFilePath 插件的路径
     * @return
     */
    private AssetManager createAssetManager(String pFilePath) {
        try {
            final AssetManager assetManager = AssetManager.class.newInstance();
            final Class<?> assetManagerClazz = Class.forName("android.content.res.AssetManager");
            final Method addAssetPathMethod = assetManagerClazz.getDeclaredMethod("addAssetPath", String.class);
            addAssetPathMethod.setAccessible(true);
            addAssetPathMethod.invoke(assetManager, pFilePath);
            return assetManager;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    //这个Resources就可以加载非宿主apk中的资源
    private Resources createResources(String pFilePath) {
        final AssetManager assetManager = createAssetManager(pFilePath);
        Resources superRes = this.getResources();
        return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
    }

3.宿主apk,动态换肤的点击事件代码

private boolean mChange = false;
   private String skinType = "";
   private int APK_TYPE;

   /**
    * 替换bg
    */
   public void replace(View view) {
       if (!mChange) {
           skinType = "skindemo-debug.apk";
           APK_TYPE = 1;
           mChange = true;
       } else {
           skinType = "skindemo2-debug.apk";
           APK_TYPE = 2;
           mChange = false;
       }

       final String path = Environment.getExternalStorageDirectory() + File.separator + skinType;
       final String pkgName = getUninstallApkPkgName(this, path);
       dynamicLoadApk(path, pkgName);

   }

   private void dynamicLoadApk(String pApkFilePath, String pApkPacketName) {
       File file = getDir("dex", Context.MODE_PRIVATE);
       //第一个参数:是dex压缩文件的路径
       //第二个参数:是dex解压缩后存放的目录
       //第三个参数:是C/C++依赖的本地库文件目录,可以为null
       //第四个参数:是上一级的类加载器
       DexClassLoader classLoader = new DexClassLoader(pApkFilePath, file.getAbsolutePath(), null, getClassLoader());
       try {
           final Class<?> loadClazz = classLoader.loadClass(pApkPacketName + ".R$drawable");
           //插件中皮肤的名称是skin_one
           final Field skinOneField = APK_TYPE == 1 ? loadClazz.getDeclaredField("skin_one") : loadClazz.getDeclaredField("skin_two");
           skinOneField.setAccessible(true);
           //反射获取skin_one的resousreId
           final int resousreId = (int) skinOneField.get(R.id.class);
           //可以加载插件资源的Resources
           final Resources resources = createResources(pApkFilePath);
           if (resources != null) {
               final Drawable drawable = resources.getDrawable(resousreId);
               linearLayout.setBackgroundDrawable(drawable);
           }
       } catch (Exception e) {
           e.printStackTrace();
       }

   }
 
   /**
    * 根据sdcard路径,获取未安装apk的信息
    *
    * @param context
    * @param pApkFilePath apk文件的path
    * @return
    */
   private String getUninstallApkPkgName(Context context, String pApkFilePath) {
       PackageManager pm = context.getPackageManager();
       PackageInfo pkgInfo = pm.getPackageArchiveInfo(pApkFilePath, PackageManager.GET_ACTIVITIES);
       if (pkgInfo != null) {
           ApplicationInfo appInfo = pkgInfo.applicationInfo;
           return appInfo.packageName;
       }
       return "";
   }

此时就完成了,只需要看效果了
源码链接:https://github.com/15189611/pluginDemo

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容