tinker热修复核心代码解析
实现原理
重置dex加载顺序,把修复后的class所在的dex提前加载,由于包名类名一致,所以后面的有问题的class就不会被加载。
代码实现
private fun fix() {
//1.把内存卡中的.dex移动至app私有目录,防止被恶意修改
val dexPath = "${Environment.getExternalStorageDirectory()}${File.separator}out.dex"
val newDexPath = copyFile(File(dexPath),getDir("fix_dexs",Context.MODE_PRIVATE))
//2.DexClassLoader加载.dex,然后反射获取Element[]
val optimizedDir = getDir("dex_opt",Context.MODE_PRIVATE)
if(!optimizedDir.exists()){
optimizedDir.mkdir()
}
val dexClassLoader = DexClassLoader(newDexPath,optimizedDir.absolutePath,null,classLoader)
val field = BaseDexClassLoader::class.java.getDeclaredField("pathList")
field.isAccessible = true
val pathList = field.get(dexClassLoader)
val dexPathListCls = Class.forName("dalvik.system.DexPathList")
val dexElementsFiled = dexPathListCls.getDeclaredField("dexElements")
dexElementsFiled.isAccessible = true
val dexElements = dexElementsFiled.get(pathList)
//3.PathClassLoader反射获取Element[]
val pathList2 = field.get(classLoader)
val dexElements2 = dexElementsFiled.get(pathList2)
//4.合成新的Element[]替换调PathClassLoader中的Element[]
val length1 = (dexElements as kotlin.Array<*>).size
val length2 = (dexElements2 as kotlin.Array<*>).size
val newElements = Array.newInstance(dexElements::class.java.componentType,length1+length2) as kotlin.Array<Any>
for(i in 0 until length1){
newElements[i] = dexElements[i]!!
}
for(i in 0 until length2){
newElements[i+length1] = dexElements2[i]!!
}
dexElementsFiled.set(pathList2,newElements)
}
//拷贝文件
private fun copyFile(file: File, dir: File):String {
if(!dir.exists()){
dir.mkdir()
}
val dexName = file.name
val newPath = File(dir,dexName)
if(!newPath.exists()){
newPath.createNewFile()
}
val fis = FileInputStream(file)
val fos = FileOutputStream(newPath)
var temp=0
while (true){
temp = fis.read()
if(temp==-1)break
fos.write(temp)
}
fis.close()
fos.close()
return newPath.absolutePath
}
代码解析
其实代码上有注释,这里我归纳下。
1.把内存卡中的.dex移动至app私有目录,防止被恶意修改
这一步呢比较简单,就是io操作一下就好了,目的就是把修复后的dex放在自己的私有的文件夹下比较安全。
在讲第2,3点之前,我们首先通过源码了解下加载的机制。
libcore/ojluni/src/main/java/java/lang/ClassLoader.java
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) {
//****重点****
c = findClass(name);
}
}
return c;
}
//直接查看子类BaseDexClassLoader,这里是抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
...
private final DexPathList pathList;
...
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
...
//很显然是利用 pathList去加载类
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
...
}
return c;
}
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
...
}
...
static class Element{
...
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
...
}
到这里呢,我们可以看出,其实一个个Element就对应了一个个DexFile,也就是对应了我们一个个.dex文件。
所以大家这时候再看下前面所说的实现原理,只要在这个for循环遍历Element的时候先遍历我们最新修复的Dex文件,然后找到了我们最新修复的class文件的话,就能实现热修复了。所以下面采用反射替换掉dexElements这个变量。
在看下面第2,3点前,我们还需要了解下,DexClassLoader
,PathClassLoader
,'BaseDexClassLoader'的关系和区别。
由于我们这里不是主要介绍ClassLoader
,所以大家具体可以参考下
https://www.jianshu.com/p/4b4f1fa6633c
这篇文章。
总之,DexClassLoader
用来加载app外部的dex文件,比如我们热更新修复的class打成的dex文件。PathClassLoader
用来加载apk文件内部的dex文件,也就是用来加载app内部本来就有的class。而BaseDexClassLoader
是它们的父类。
2.DexClassLoader加载.dex,然后反射获取Element[]
3.PathClassLoader反射获取Element[]
2,3点分别是反射获取到热修复后的dex文件和app内部原有的dex文件所对应的Element数组。这里值得一提的是dalvik.system.DexPathList
,这个类是不能直接使用的,需要通过Class.forName去反射调用。
4.合成新的Element[]替换调PathClassLoader中的Element[]
这里把2个Element数组合成了一个新的Element数组,值得注意的是,需要把热修复的对应的Element数组放在前面。还有一点就是dexElementsFiled.set(pathList2,newElements)
pathList2指的是PathClassLoader
中的DexPathList
,这里必须要替换调用PathClassLoader
中的DexPathList
,因为app内部加载class使用的是PathClassLoader
,而不是 DexClassLoader
。
个人所得
前面一直理解为,tinker只能重新启动才能生效,这是不对的,我们可以直接从原理上去看,只要你的有问题的class没有去加载,这时候去下载修复后的dex文件然后加载修复,就不必一定要去重新启动app才生效了。
总结
原理很简单,大家可以自己写着练习下,需要了解andfix实现原理的同学可以参考:https://www.jianshu.com/p/3a1c4c89225a
实现的原理和机制都是不一样的,可以了解一下。