實(shí)現(xiàn)原理
重置dex加載順序,把修復(fù)后的class所在的dex提前加載,由于包名類名一致,所以后面的有問題的class就不會被加載。
代碼實(shí)現(xiàn)
private fun fix() {
//1.把內(nèi)存卡中的.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[]替換調(diào)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
}
代碼解析
其實(shí)代碼上有注釋,這里我歸納下。
1.把內(nèi)存卡中的.dex移動至app私有目錄,防止被惡意修改
這一步呢比較簡單,就是io操作一下就好了,目的就是把修復(fù)后的dex放在自己的私有的文件夾下比較安全。
在講第2,3點(diǎn)之前,我們首先通過源碼了解下加載的機(jī)制。
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) {
//****重點(diǎn)****
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)
...
}
到這里呢,我們可以看出,其實(shí)一個(gè)個(gè)Element就對應(yīng)了一個(gè)個(gè)DexFile,也就是對應(yīng)了我們一個(gè)個(gè).dex文件。
所以大家這時(shí)候再看下前面所說的實(shí)現(xiàn)原理,只要在這個(gè)for循環(huán)遍歷Element的時(shí)候先遍歷我們最新修復(fù)的Dex文件,然后找到了我們最新修復(fù)的class文件的話,就能實(shí)現(xiàn)熱修復(fù)了。所以下面采用反射替換掉dexElements這個(gè)變量。
在看下面第2,3點(diǎn)前,我們還需要了解下,DexClassLoader,PathClassLoader,'BaseDexClassLoader'的關(guān)系和區(qū)別。
由于我們這里不是主要介紹ClassLoader,所以大家具體可以參考下
http://www.itdecent.cn/p/4b4f1fa6633c
這篇文章。
總之,DexClassLoader用來加載app外部的dex文件,比如我們熱更新修復(fù)的class打成的dex文件。PathClassLoader用來加載apk文件內(nèi)部的dex文件,也就是用來加載app內(nèi)部本來就有的class。而BaseDexClassLoader 是它們的父類。
2.DexClassLoader加載.dex,然后反射獲取Element[]
3.PathClassLoader反射獲取Element[]
2,3點(diǎn)分別是反射獲取到熱修復(fù)后的dex文件和app內(nèi)部原有的dex文件所對應(yīng)的Element數(shù)組。這里值得一提的是dalvik.system.DexPathList,這個(gè)類是不能直接使用的,需要通過Class.forName去反射調(diào)用。
4.合成新的Element[]替換調(diào)PathClassLoader中的Element[]
這里把2個(gè)Element數(shù)組合成了一個(gè)新的Element數(shù)組,值得注意的是,需要把熱修復(fù)的對應(yīng)的Element數(shù)組放在前面。還有一點(diǎn)就是dexElementsFiled.set(pathList2,newElements)
pathList2指的是PathClassLoader中的DexPathList,這里必須要替換調(diào)用PathClassLoader中的DexPathList,因?yàn)閍pp內(nèi)部加載class使用的是PathClassLoader,而不是 DexClassLoader。
個(gè)人所得
前面一直理解為,tinker只能重新啟動才能生效,這是不對的,我們可以直接從原理上去看,只要你的有問題的class沒有去加載,這時(shí)候去下載修復(fù)后的dex文件然后加載修復(fù),就不必一定要去重新啟動app才生效了。
總結(jié)
原理很簡單,大家可以自己寫著練習(xí)下,需要了解andfix實(shí)現(xiàn)原理的同學(xué)可以參考:http://www.itdecent.cn/p/3a1c4c89225a
實(shí)現(xiàn)的原理和機(jī)制都是不一樣的,可以了解一下。