前兩天看到QQ空間發(fā)了一篇動態(tài)熱補丁的文章幕屹,昨天晚上實現(xiàn)了一個波势。
項目地址:https://github.com/dodola/HotFix
HotFix
安卓App熱補丁動態(tài)修復(fù)框架
介紹
該項目是基于QQ空間終端開發(fā)團隊的技術(shù)文章實現(xiàn)的肮帐,完成了文章中提到的基本功能铺根。
文章地址:安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹
項目部分代碼從 dalvik_patch 項目中修改而來胚股,這個項目本來是用來實現(xiàn)multidex的庭再,發(fā)現(xiàn)可以用來實現(xiàn)方法替換的效果雷客。
項目包括核心類庫芒珠,補丁制作庫,例子搅裙≈遄浚可以直接運行代碼看效果。
詳細說明
補丁制作
該技術(shù)的原理很簡單部逮,其實就是用ClassLoader加載機制娜汁,覆蓋掉有問題的方法。所以我們的補丁其實就是有問題的類打成的一個包兄朋。
例子中的出現(xiàn)問題的類是 dodola.hotfix.BugClass
原始代碼如下:
public class BugClass {
public String bug() {
return "bug class";
}
}
我們假設(shè)BugClass
類里的bug()
方法出現(xiàn)錯誤掐禁,需要修復(fù),修復(fù)代碼如下:
public class BugClass {
public String bug() {
return "fixed class";
}
}
那么我們只需要將修復(fù)過的類編譯后打包成dex即可
步驟如下:
-
將補丁類提取出來到一個文件夾里
將class文件打入一個jar包中
jar cvf path.jar *
將jar包轉(zhuǎn)換成dex的jar包
dx --dex --output=path_dex.jar path.jar
這樣就生成了補丁包path_dex.jar
實現(xiàn)javassist動態(tài)代碼注入
實現(xiàn)這一部分功能的原因主要是因為出現(xiàn)如下異常
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
問題原因在文檔中已經(jīng)描述的比較清楚蜈漓。
就是如果以上方法中直接引用到的類(第一層級關(guān)系穆桂,不會進行遞歸搜索)和clazz都在同一個dex中的話,那么這個類就會被打上CLASS_ISPREVERIFIED
很明顯融虽,解決的方法就是在類中引用一個其他dex中的類享完,但是源碼方式的引用會將引用的類打入同一個dex中,所以我們需要找到一種既能編譯通過并且將兩個互相引用的類分離到不同的dex中有额,于是就有了這個動態(tài)的代碼植入方式般又。
首先我們需要制作引用類的dex包彼绷,代碼在hackdex
中,我直接使用了文檔中的類名 AntilazyLoad
這樣可以和文章中對應(yīng)起來茴迁,方便一些寄悯。
我們將這個庫打包成dex的jar包,方法跟制作補丁一樣堕义。
下面是重點猜旬,我們要用javassist
將這個類在編譯打包的過程中插入到目標(biāo)類中。
為了方便倦卖,我將這個過程做成了一個Gradle的Task洒擦,代碼在buildSrc
中。
這個項目是使用Groovy開發(fā)的怕膛,需要配置Groovy SDK才可以編譯成功熟嫩。
核心代碼如下:
/**
* 植入代碼
* @param buildDir 是項目的build class目錄,就是我們需要注入的class所在地
* @param lib 這個是hackdex的目錄,就是AntilazyLoad類的class文件所在地
*/
public static void process(String buildDir, String lib) {
println(lib)
ClassPool classes = ClassPool.getDefault()
classes.appendClassPath(buildDir)
classes.appendClassPath(lib)
//下面的操作比較容易理解,在將需要關(guān)聯(lián)的類的構(gòu)造方法中插入引用代碼
CtClass c = classes.getCtClass("dodola.hotfix.BugClass")
println("====添加構(gòu)造方法====")
def constructor = c.getConstructors()[0];
constructor.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")
c.writeFile(buildDir)
CtClass c1 = classes.getCtClass("dodola.hotfix.LoadBugClass")
println("====添加構(gòu)造方法====")
def constructor1 = c1.getConstructors()[0];
constructor1.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")
c1.writeFile(buildDir)
growl("ClassDumper", "${c.frozen}")
}
下面在代碼編譯完成,打包之前褐捻,執(zhí)行植入代碼的task就可以了掸茅。
在 app 項目的 build.gradle 中插入如下代碼
task('processWithJavassist') << {
String classPath = file('build/intermediates/classes/debug')//項目編譯class所在目錄
dodola.patch.PatchClass.process(classPath, project(':hackdex').buildDir
.absolutePath + '/intermediates/classes/debug')//第二個參數(shù)是hackdex的class所在目錄
}
android{
.......
applicationVariants.all { variant ->
variant.dex.dependsOn << processWithJavassist //在執(zhí)行dx命令之前將代碼打入到class中
}
}
反編譯編譯后的apk可以發(fā)現(xiàn),代碼已經(jīng)植入進去柠逞,而且包里并不存在dodola.hackdex.AntilazyLoad
這個類
ISSUE
開發(fā)測試過程中遇到一些問題昧狮,這種方法無法在已經(jīng)加載好的類中實現(xiàn)動態(tài)替換,只能在類加載之前替換掉板壮。就是說陵且,補丁下載下來后,只能等待用戶重啟應(yīng)用才能完成補丁效果个束。