美文网首页
Android插件化小结

Android插件化小结

作者: Dane_404 | 来源:发表于2019-03-19 23:23 被阅读0次

    一、动态加载技术

    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插件化小结

          本文链接:https://www.haomeiwen.com/subject/yjaomqtx.html