一、动态加载技术
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);
网友评论