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);

你可能感兴趣的:(Android插件化小结)