Android热更新
组件化
组件化和模块化其实一回事,都是拆分多个 module 进行开发,组件化的叫法更偏向封装系统功能,比如统一对话框封装,网络封装等,而模块化叫法更偏向业务方面,比如登录模块等。
插件化
App 的部分功能模块在打包的时候不以传统的方式打包进 apk 中,而是另一种方式封装到apk 内部,或者放在网络上适时下载,在需要的时候动态对这些功能模块进行加载,称之为插件化。
这些单独二次封装的功能模块 apk,称为插件,初始安装的 apk 称为宿主。
插件化基础是反射。
反射
Java 提供了关键字 public、private 用来限制代码之间的可见性,但又提供了反射来访问不可见方法。
可见特性的支持并不是为了保护代码不被别人使用,而是为了程序开发的简洁性。可见性的的支持提供的是 Safety 的安全,而不是 Security 的安全。可见性的支持是让程序的开发者不容易写出 bug ,而不是更容易外部被入侵。
反射的支持可以让开发者在可见性的不对外的时候,突破可见性的限制来调用自己需要的 API.
反射的使用:
package com.reflect.utils;
class Utils {
private Utils(){}
private int method(int a) {
System.out.println("Utils.method打印了... a:" + a);
return 10;
}
}
// 通过反射调用 Utils 中的方法
try {
Class utilsClass = Class.forName("com.reflect.utils.Utils");// Utils.class
Constructor constructor = utilsClass.getDeclaredConstructor();
constructor.setAccessible(true);// 构造方法设置为可以访问
Object utilsObj = constructor.newInstance();
Method method = utilsClass.getDeclaredMethod("method", int.class);
method.setAccessible(true);// 方法设置为可访问
Object result = method.invoke(utilsObj, 1);
System.out.println(result);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
DEX
class:java 编译后的文件,每一个类对应一个 class 文件。
dex:Dalvik Executable 把 class 打包到一起,一个 dex 可以包含多个 class 文件。
odex:Optimized DEX 针对系统的优化,例如某个方法的调用指令,会把虚拟机的调用转换为使用具体的 index,这样在执行的就不用再查找了。
oat:Optimized Android file Type,使用 AOT(Ahead-Of-Time compilation预先编译) 策略对 dex 预先编译成本地指令,这样在运行阶段就不需要再经历一次解释过程。
插件化原理
动态加载,通过自定义 ClassLoader 来加载新的 dex 文件,从而让程序员原本没有的类可以被使用。
private void pluginLoad() {
File pluginApk = new File(getFilesDir() + "/plugin.apk");
AssetManager assets = getAssets();
try(Source source = Okio.source(assets.open("plugin.apk"));
BufferedSink sink = Okio.buffer(Okio.sink(pluginApk))) {
sink.writeAll(source);
} catch (IOException e) {
e.printStackTrace();
}
// 调用 plugin.apk 中的方法
DexClassLoader classLoader = new DexClassLoader(pluginApk.getAbsolutePath(),
getCodeCacheDir().getPath(),
null,
null);
try {
Class<?> utilClass = classLoader.loadClass("com.chen.hotfix.utils.Utils");
Constructor<?> constructor = utilClass.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Object utilObj = constructor.newInstance();
Method getNameMethod = utilClass.getDeclaredMethod("getName");
getNameMethod.setAccessible(true);
Object name = getNameMethod.invoke(utilObj);
Log.e("TAG", "name:" + name);
} catch (ClassNotFoundException
| InvocationTargetException
| IllegalAccessException
| InstantiationException
| NoSuchMethodException e) {
e.printStackTrace();
}
}
插件化和热更新
- 插件化的内容在原 App 中没有,而热更新是原 App 中的内容做了改动
- 插件化在代码中有固定的入口,而热更新则可能改变了任何有一个位置的代码
热更新的原理
ClassLoader 的 dex 文件的替换,直接修改字节码
类加载过程
双亲委托机制,是一个带缓存的,从上到下(从父到子)的加载过程。
//ClassLoader.java
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
对于一个具体的 ClassLoader,先从自己的缓存中取,自己缓存中没有,就找父 ClassLoader 要(parent.loadClass()),父 View 也没有,就自己加载。(父View的加载流程和自己是一样的)
BaseDexClassLoader 的 findClass() 是通过它的 pathList.findClass(),它的 pathList.loadClass() 通过 DexPathList 的 dexElements 的 findClass(),所以热更新的关键在于,把补丁 dex 文件加载放进一个 Element,并且插入到 dexElements 这个数组的前面。
手写热更新
因为无法在更新之前就指定要更新谁,所以不能定义新的 ClassLoader,而只能选择对 ClassLoader 进行修改,让它能够加载补丁里面的类。
因为补丁中的类在之前的应用中已经存在,所以应该把补丁中的 Element 对象插入到 dexElements 的前面才行,插入到后面会被忽略掉。
具体的做法:
- 用补丁创建一个 PathClassLoader
- 把补丁 PathClassLoader 里面的 elements 插入到旧的 dexElements 前面去
尽早加载热更新,通常是把加载过程放在 Application.attachBaseContext()
热更新下载完之后需要先杀死程序才能让补丁生效
用 d8 把指定的 class 打包进 dex
sample
HotfixApplication
public class HotfixApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 通过反射将dex的dexElements插入到应用的dexElements前面,使热更新(hotfix.dex)生效
File hotfixDex = new File(getFilesDir() + "/hotfix.dex");
if (hotfixDex.exists()) {
try {
// originalLoader.pathList.dexElements = classLoader.pathList.dexElements;
// originalLoader.pathList.dexElements += classLoader.pathList.dexElements;
ClassLoader originalClassLoader = getClassLoader();// App的ClassLoader
DexClassLoader newClassLoader = new DexClassLoader(hotfixDex.getPath(),
getCodeCacheDir().getPath(), null, null);// dex的ClassLoader
Class<BaseDexClassLoader> loaderClass = BaseDexClassLoader.class;
Field pathListField = loaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathListObj = pathListField.get(newClassLoader);
Class<?> pathListClass = pathListObj.getClass();
Field dexElementsField = pathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object dexElementsObj = dexElementsField.get(pathListObj);
Object originalPathListObj = pathListField.get(originalClassLoader);
Object originalDexElementsObj = dexElementsField.get(originalPathListObj);
int originalLength = Array.getLength(originalDexElementsObj);
int newLength = Array.getLength(dexElementsObj);
Object concatDexElementsObject = Array.newInstance(dexElementsObj.getClass().getComponentType(), originalLength + newLength);
for (int i = 0; i < newLength; i++) {
Array.set(concatDexElementsObject, i, Array.get(dexElementsObj, i));
}
for (int i = 0; i < originalLength; i++) {
Array.set(concatDexElementsObject, newLength + i, Array.get(originalDexElementsObj, i));
}
dexElementsField.set(originalPathListObj, concatDexElementsObject);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
下载热更新包
private void hotfixLoad() {
File hotfixDex = new File(getFilesDir() + "/hotfix.dex");
AssetManager assets = getAssets();
try (Source source = Okio.source(assets.open("hotfix.dex"));//TODO 需要从网络下载hotfix.dex
BufferedSink sink = Okio.buffer(Okio.sink(hotfixDex))) {
sink.writeAll(source);
} catch (IOException e) {
e.printStackTrace();
}
android.os.Process.killProcess(android.os.Process.myPid());// 重启生效
}