前兩天看到QQ空間發了一篇動態熱補丁的文章,昨天晚上實現了一個。
項目地址:https://github.com/dodola/HotFix
HotFix
安卓App熱補丁動態修復框架
介紹
該項目是基于QQ空間終端開發團隊的技術文章實現的,完成了文章中提到的基本功能。
文章地址:安卓App熱補丁動態修復技術介紹
項目部分代碼從 dalvik_patch 項目中修改而來,這個項目本來是用來實現multidex的,發現可以用來實現方法替換的效果。
項目包括核心類庫,補丁制作庫,例子。可以直接運行代碼看效果。
詳細說明
補丁制作
該技術的原理很簡單,其實就是用ClassLoader加載機制,覆蓋掉有問題的方法。所以我們的補丁其實就是有問題的類打成的一個包。
例子中的出現問題的類是 dodola.hotfix.BugClass
原始代碼如下:
public class BugClass {
public String bug() {
return "bug class";
}
}
我們假設BugClass
類里的bug()
方法出現錯誤,需要修復,修復代碼如下:
public class BugClass {
public String bug() {
return "fixed class";
}
}
那么我們只需要將修復過的類編譯后打包成dex即可
步驟如下:
-
將補丁類提取出來到一個文件夾里
patch1.png 將class文件打入一個jar包中
jar cvf path.jar *
將jar包轉換成dex的jar包
dx --dex --output=path_dex.jar path.jar
這樣就生成了補丁包path_dex.jar
實現javassist動態代碼注入
實現這一部分功能的原因主要是因為出現如下異常
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
問題原因在文檔中已經描述的比較清楚。
就是如果以上方法中直接引用到的類(第一層級關系,不會進行遞歸搜索)和clazz都在同一個dex中的話,那么這個類就會被打上CLASS_ISPREVERIFIED
很明顯,解決的方法就是在類中引用一個其他dex中的類,但是源碼方式的引用會將引用的類打入同一個dex中,所以我們需要找到一種既能編譯通過并且將兩個互相引用的類分離到不同的dex中,于是就有了這個動態的代碼植入方式。
首先我們需要制作引用類的dex包,代碼在hackdex
中,我直接使用了文檔中的類名 AntilazyLoad
這樣可以和文章中對應起來,方便一些。
我們將這個庫打包成dex的jar包,方法跟制作補丁一樣。
下面是重點,我們要用javassist
將這個類在編譯打包的過程中插入到目標類中。
為了方便,我將這個過程做成了一個Gradle的Task,代碼在buildSrc
中。
這個項目是使用Groovy開發的,需要配置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)
//下面的操作比較容易理解,在將需要關聯的類的構造方法中插入引用代碼
CtClass c = classes.getCtClass("dodola.hotfix.BugClass")
println("====添加構造方法====")
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("====添加構造方法====")
def constructor1 = c1.getConstructors()[0];
constructor1.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")
c1.writeFile(buildDir)
growl("ClassDumper", "${c.frozen}")
}
下面在代碼編譯完成,打包之前,執行植入代碼的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')//第二個參數是hackdex的class所在目錄
}
android{
.......
applicationVariants.all { variant ->
variant.dex.dependsOn << processWithJavassist //在執行dx命令之前將代碼打入到class中
}
}
反編譯編譯后的apk可以發現,代碼已經植入進去,而且包里并不存在dodola.hackdex.AntilazyLoad
這個類
ISSUE
開發測試過程中遇到一些問題,這種方法無法在已經加載好的類中實現動態替換,只能在類加載之前替換掉。就是說,補丁下載下來后,只能等待用戶重啟應用才能完成補丁效果。