手写热更新
热更新 / 热修复
不安装新版本的软件,直接从⽹络下载新功能模块来对软件进⾏局部更新
热更新和插件化的区别
区别有两点
- 插件化的内容在原 App 中没有,⽽热更新是原 App 中的内容做了改动
- 插件化在代码中有固定的⼊⼝,⽽热更新则可能改变任何⼀个位置的代码
热更新的原理
- ClassLoader 的 dex ⽂件替换
- 直接修改字节码
前置知识:loadClass() 的类加载过程
宏观上:是⼀个带缓存的、从上到下的加载过程(即⽹上所说的「双亲委托机制」)
对于具体的⼀个 ClassLoader:
- 先从⾃⼰的缓存中取
- ⾃⼰没有缓存,就找⽗ ClassLoader 要(
parent.loadClass()
) - ⽗ View 也没有,就⾃⼰加载(
findClass()
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//参数resolve没用到,是java中的loadClass有两个参数,此处与它一致
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
//First,check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if(c == null){
try{
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e){
//ClassNotFoundException thrown if class not found
//from the non-null parent class loader
}
if(c == null){
//If still not found, then invoke findClass in order
//to find the class.
c = findClass(name);
}
}
return c;
}BaseDexClassLoader
或者它的⼦类(DexClassLoader
、PathClassLoader
等)的findClass()
:- 通过它的
pathList.findClass()
- 它的
pathList.loadClass()
通过DexPathList
的dexElements
的findClass()
- 所以热更新的关键在于,把补丁 dex ⽂件加载放进⼀个 Element,并且插⼊到dexElements 这个数组的前⾯(插⼊到后⾯的话会被忽略掉)【先加载过的class,之后再遇到相同class就不会再加载】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// src/main/assets/apk/27_hot_update-debug.apk
File apk = new File(getCacheDir() + "/27_hot_update-debug.apk");
if(!apk.exists()){
try{
InputStream is =getAssets().open("apk/27_hot_update-debug.apk");
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
is.close();
FileOutputStream fos = new FileOutputStream(apk);
fos.write(buffer);
fos.close();
} catch (Exception e){
throw new RuntimeException(e);
}
}
try{
ClassLoader classLoader = getClassLoader();
Class loaderClass = BaseDexClassLoader.class;
Field pathListField = loaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathListObject = pathListField.get(classLoader);
Class pathListClass = pathListObject.getClass();
Field dexElementsField = pathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
//classLoader.pathList.dexElements = ???
PathClassLoader newClassLoader = new PathClassLoader(apk.getPath(), null);
Object newPathListObject = pathListField.get(newClassLoader);
Object newDexElementsObject = dexElementsField.get(newPathListObject);
dexElementsField.set(pathListObject, newDexElementsObject);
} catch (NoSuchFieldException e){
e.printStackTrace();
} catch (IllegalAccessException e){
e.printStackTrace();
}- 通过它的
⼿写热更新
- 因为⽆法在更新之前就指定要更新谁;所以不能定义新的
ClassLoader
,⽽只能选择对ClassLoader
进⾏修改,让它能够加载补丁⾥⾯的类 - 因为补丁的类在原先的 App 中已经存在,所以应该把补丁的
Element
对象插⼊到dexElements
的前⾯才⾏,插⼊到后⾯会被忽略掉。 - 具体的做法:反射
- ⾃⼰⽤补丁创建⼀个
PathClassLoader
- 把补丁
PathClassLoader
⾥⾯的elements
替换到旧的⾥⾯去 - 注意:
- 尽早加载热更新(通⽤⼿段是把加载过程放在
Application.attachBaseContext()
) - 热更新下载完成后在需要时先杀死程序才能让补丁⽣效
- 尽早加载热更新(通⽤⼿段是把加载过程放在
- 优化:热更新没必要把所有内容都打过来,只要把改变的类拿过来就⾏了
- ⽤ d8 把指定的 class 打包进 dex
- 完整化:从⽹上加载
- 再优化:把打包过程写⼀个 task
- ⾃⼰⽤补丁创建⼀个