Android插件化小结

一、动态加载技术

1、基于ClassLoader

  • ClassLoader的一个特点就是,如果程序不重新启动,加载过一次的类就无法重新加载。因此,如果使用ClassLoader来动态升级APP或者动态修复BUG,都需要重新启动APP才能生效。

2、基于jni hook

  • ClassLoader是在虚拟机上操作的,而hook已经是在Native层级的工作了,直接修改应用内存地址,所以使用jni hook的方式时,不用重新应用就能生效。

二、ClassLoader工作机制

1、有几个ClassLoader实例?

  • 动态加载的基础是ClassLoader,从名字也可以看出,ClassLoader就是专门用来处理类加载工作的,所以这货也叫类加载器,而且一个运行中的APP 不仅只有一个类加载器。

  • 其实,在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类,我们的Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。

  • 此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。由此也可以看出,一个运行的Android应用至少有2个ClassLoader。

三、创建自己ClassLoader实例

1、ClassLoader的构造

  • 创建一个ClassLoader实例的时候,需要使用一个现有的ClassLoader实例作为新创建的实例的Parent。这样一来,一个Android应用,甚至整个Android系统里所有的ClassLoader实例都会被一棵树关联起来,这也是ClassLoader的 双亲代理模型(Parent-Delegation Model)的特点。

     /*
     * constructor for the BootClassLoader which needs parent to be null.
     */
    ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
       if (parentLoader == null && !nullAllowed) {
           throw new NullPointerException("parentLoader == null && !nullAllowed");
       }
       parent = parentLoader;
    }
    

2、ClassLoader双亲代理模型加载类的特点和作用

  • 从源码中我们也可以看出,loadClass方法在加载一个类的实例的时候,会先查询当前ClassLoader实例是否加载过此类,有就返回;如果没有。查询Parent是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;如果继承路线上的ClassLoader都没有加载,才由Child执行类的加载工作;这样做有个明显的特点,如果一个类被位于树根的ClassLoader加载过,那么在以后整个系统的生命周期内,这个类永远不会被重新加载。

    public Class<?> loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, false);
    }
    
    protected Class<?> loadClass(String className, boolean resolve) throws        ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);
    
        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }
    
            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
          }
      }
    
      return clazz;
    }
    

3、注意

  • 如果你希望通过动态加载的方式,加载一个新版本的dex文件,使用里面的新类替换原有的旧类,从而修复原有类的BUG,那么你必须保证在加载新类的时候,旧类还没有被加载,因为如果已经加载过旧类,那么ClassLoader会一直优先使用旧类。

  • 如果旧类总是优先于新类被加载,我们也可以使用一个与加载旧类的ClassLoader没有树的继承关系的另一个ClassLoader来加载新类,因为ClassLoader只会检查其Parent有没有加载过当前要加载的类,如果两个ClassLoader没有继承关系,那么旧类和新类都能被加载。

  • 同一个Class = 相同的 ClassName + PackageName + ClassLoader

4、DexClassLoader 和 PathClassLoader

  • DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;

  • PathClassLoader只能加载系统中已经安装过的apk;

四、简单加载模式

1、如何获取能够加载的.dex文件

  • 首先我们可以通过JDK的编译命令javac把Java代码编译成.class文件,再使用jar命令把.class文件封装成.jar文件,这与编译普通Java程序的时候完全一样。之后再用Android SDK的DX工具把.jar文件优化成.dex文件(在“android-sdk\build-tools\具体版本\”路径下)
    dx --dex --output=target.dex origin.jar // target.dex就是我们要的了
  • 此外,我们可以现把代码编译成APK文件,再把APK里面的.dex文件解压出来,或者直接把APK文件当成.dex使用(只是APK里面的静态资源文件我们暂时还用不到)。至此我们发现,无论加载.jar,还是.apk,其实都和加载.dex是等价的,Android能加载.jar和.apk,是因为它们都包含有.dex,直接加载.apk文件时,ClassLoader也会自动把.apk里的.dex解压出来。

2、加载并调用.dex里面的方法

  • 使用前,先看看DexClassLoader的构造方法

    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath,           ClassLoader parent) {
          super((String)null, (File)null, (String)null, (ClassLoader)null);
          throw new RuntimeException("Stub!");
     }
    
  • 注意,我们之前提到的,DexClassLoader并不能直接加载外部存储的.dex文件,而是要先拷贝到内部存储里。这里的dexPath就是.dex的外部存储路径,而optimizedDirectory则是内部路径,libraryPath用null即可,parent则是要传入当前应用的ClassLoader,这与ClassLoader的“双亲代理模式”有关。

  • 实例使用DexClassLoader的代码

      File optimizedDexOutputPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "test_dexloader.jar");// 外部路径
      File dexOutputDir = this.getDir("dex", 0);// 无法直接从外部路径加载.dex文件,需要指定APP内部路径作为缓存目录(.dex文件会被解压到此目录)
      DexClassLoader dexClassLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(),dexOutputDir.getAbsolutePath(), null, getClassLoader());
    

3、如何调用.dex里面的代码,主要有两种方式

  • 使用反射的方式

    DexClassLoader dexClassLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(), dexOutputDir.getAbsolutePath(), null, getClassLoader());
          Class libProviderClazz = null;
          try {
              libProviderClazz = dexClassLoader.loadClass("me.kaede.dexclassloader.MyLoader");
              // 遍历类里所有方法
              Method[] methods = libProviderClazz.getDeclaredMethods();
              for (int i = 0; i < methods.length; i++) {
                  Log.e(TAG, methods[i].toString());
              }
              Method start = libProviderClazz.getDeclaredMethod("func");// 获取方法
              start.setAccessible(true);// 把方法设为public,让外部可以调用
              String string = (String) start.invoke(libProviderClazz.newInstance());// 调用方法并获取返回值
              Toast.makeText(this, string, Toast.LENGTH_LONG).show();
          } catch (Exception exception) {
              // Handle exception gracefully here.
              exception.printStackTrace();
          }
    
  • 使用接口的方式
    毕竟.dex文件也是我们自己维护的,所以可以把方法抽象成公共接口,把这些接口也复制到主项目里面去,就可以通过这些接口调用动态加载得到的实例的方法了。

    pulic interface IFunc{
        public String func();
    }
    
    // 调用
    IFunc ifunc = (IFunc)libProviderClazz;
    String string = ifunc.func();
    Toast.makeText(this, string, Toast.LENGTH_LONG).show();
    

五、代理Activity模式

1、启动没有注册的Activity的两个主要问题:

  • 如何使插件APK里的Activity具有生命周期;
  • 如何使插件APK里的Activity具有上下文环境(使用R资源);

2、使用Fragment代替Activity

  • Fragment自带生命周期,不需要在Manifest里注册,所以可以在.dex里使用Fragment来代替Activity,代价就是Fragment之间的切换会繁琐许多。

3、代理Activity模式

  • 其主要特点是:主项目APK注册一个代理Activity(命名为ProxyActivity),ProxyActivity是一个普通的Activity,但只是一个空壳,自身并没有什么业务逻辑。每次打开插件APK里的某一个Activity的时候,都是在主项目里使用标准的方式启动ProxyActivity,再在ProxyActivity的生命周期里同步调用插件中的Activity实例的生命周期方法,从而执行插件APK的业务逻辑。

4、处理插件Activity的生命周期

  • 用ProxyActivity(一个标准的Activity实例)的生命周期同步控制插件Activity(普通类的实例)的生命周期,同步的方式可以有下面两种:
    ①在ProxyActivity生命周期里用反射调用插件Activity相应生命周期的方法,简单粗暴。
    ②把插件Activity的生命周期抽象成接口,在ProxyActivity的生命周期里调用。另外,多了这一层接口,也方便主项目控制插件Activity。

5、在插件Activity里使用R资源

  • 使用代理的方式同步调用生命周期的做法容易理解,也没什么问题,但是要使用插件里面的res资源就有点麻烦了。简单的说,res里的每一个资源都会在R.java里生成一个对应的Integer类型的id,APP启动时会先把R.java注册到当前的上下文环境,我们在代码里以R文件的方式使用资源时正是通过使用这些id访问res资源,然而插件的R.java并没有注册到当前的上下文环境,所以插件的res资源也就无法通过id使用了。

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

推荐阅读更多精彩内容