Android 主要的熱修復方案原理分析
目前較爲成熟的熱修復框架主要有AndFix、Nuwa以及微信的熱更新思想。現在將其主要思想總結如下:
AndFix
AndFix是支付寶開源的一套熱修復框架,使用簡單,成功率高,基本滿足大多數的bug修復場景。引入到項目中非常方便,主要分兩步:
代碼整合
build.gradle添加依賴 compile 'com.alipay.euler:andfix:0.4.0@aar'
Application.onCreate()方法中添加
PatchManager patchManager = new PatchManager(this);
patchManager.init(appversion);//current version
patchManager.loadPatch();
然後和後端協商一個補丁包下載服務器,在每次下載更新包到本地後
patchManager.addPatch(path);
打補丁
AndFix提供了一個打補丁包的工具,可以去這裏下載,使用方法如下:
apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
-a,--alias <alias> keystore entry alias.
-e,--epassword <***> keystore entry password.
-f,--from <loc> new Apk file path.
-k,--keystore <loc> keystore path.
-n,--name <name> patch name.
-o,--out <dir> output dir.
-p,--kpassword <***> keystore password.
-t,--to <loc> old Apk file path.
AndFix的思想是直接更改修復的方法,具體我們可以看源碼。先從PatchManager的init和load方法入手,這兩個方法實現了補丁包的加載並最終調用了AndFixManager的fix方法:
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
if (patchNames.contains(patchName)) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), classLoader, classes);
}
}
fix函數需要傳入三個參數:patch文件、classloader以及需要fix的class列表。fix函數代碼如下:
/**
* fix
*
* @param file
* patch file
* @param classLoader
* classloader of class that will be fixed
* @param classes
* classes will be fixed
*/
public synchronized void fix(File file, ClassLoader classLoader,
List<String> classes) {
.......省略........
//load dex文件
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);
if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}
// 定義自己的classloader
ClassLoader patchClassLoader = new ClassLoader(classLoader) {
@Override
protected Class<?> findClass(String className)
throws ClassNotFoundException {
Class<?> clazz = dexFile.loadClass(className, this);
if (clazz == null
&& className.startsWith("com.alipay.euler.andfix")) {
return Class.forName(className);// annotation’s class
// not found
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
Enumeration<String> entrys = dexFile.entries();
Class<?> clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
clazz = dexFile.loadClass(entry, patchClassLoader);
if (clazz != null) {
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}
這個方法的作用是load dex文件中的類,並依次修復,這個函數中有兩處疑問:
classes參數設計有何深意,因爲我理解的patch包裏面難道不都是需要修復的類嗎,還會把沒有修改的類打進去嗎?
自定義了一個classloader,針對”com.alipay.euler.andfix"做了特殊處理,不知道怎麼纔會有這種場景。
針對這兩個問題我特意諮詢了AndFix的作者黎三平大神,大神給我的答覆是:1. 這個設計有兩個原因:
a) 新增類
b)早期patch工具打出的補丁包不是很準確
2.AndFix的一個註解,它的類加載會走到這來的。
大神的話還是不是很明白,大家如果看到了這塊代碼請幫我解釋一下。
fix函數中遍歷dex的類,並過濾掉不需要修復的類後調用fixclass函數,代碼如下:
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
if (!isEmpty(clz) && !isEmpty(meth)) {
replaceMethod(classLoader, clz, meth, method);
}
}
}
代碼很簡潔,意思也很明瞭,就是找到這個類中需要修復的函數然後調用replaceMethod方法。替換方法在java層是無法做到的,所以這個函數最終還是調用了native的替換函數的方法,實質就是更改了類中方法所指向的地址,所以java不能做到。
jin的目錄結構如下:
AndFix做了dalvik、art以及各平臺的適配,核心是方法的替換,我們來看其中一個方法替換的函數:
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
clz->status = CLASS_INITIALIZED;
Method* meth = (Method*) env->FromReflectedMethod(src);
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
meth->clazz = target->clazz;
meth->accessFlags |= ACC_PUBLIC;
meth->methodIndex = target->methodIndex;
meth->jniArgInfo = target->jniArgInfo;
meth->registersSize = target->registersSize;
meth->outsSize = target->outsSize;
meth->insSize = target->insSize;
meth->prototype = target->prototype;
meth->insns = target->insns;
meth->nativeFunc = target->nativeFunc;
}
含義估計大家都明白,就是把方法的各個屬性值替換,實際去寫確實還是很有難度。
至此我把AndFix代碼的主要流程梳理了一遍,其中還有很多沒有get到的點,思想基本清楚了,AndFix主要採用替換方法的方式進行熱修復,好處是立即生效且補丁包較小,但是隻能基於方法修復,而且對平臺的兼容性不佳,但不失爲一個偉大的想法,也是熱修復最早開源的修復方案,向我的黎三平大神說聲感謝!
Nuwa
AndFix的思路很簡單,直接在native層替代方法,有沒有更簡單的呢,有,Nuwa!他的想法就更自然一些,直接替換類,或者廢棄掉有bug的類,怎麼做到的呢,核心就在於java.lang.ClassLoader.java這個類的loadClass方法:
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
同名的類加載一次就不再加載了,是不是想到什麼了,哈哈,對,就是把你要修復的類提前加載就ok了,那麼有bug的類便不再加載進來,實現起來也非常簡單,三行代碼搞定,我們看Nuwa的實現:
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
//新建補丁包的dexclassloader
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
//獲取原dex加載列表
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
// 新建補丁包dex加載列表
Object newDexElements = getDexElements(getPathList(dexClassLoader));
//補丁包插在最前連接成新的dex加載列表
Object allDexElements = combineArray(newDexElements, baseDexElements);
Object pathList = getPathList(getPathClassLoader());
//利用發射修改dalvik.system.BaseDexClassLoader類的pathList中的dexElements屬性
ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
}
就這麼幾行代碼就實現了替換類的熱修復,沒有native的操作,思路清晰,含義明確。當然這裏面還有一個坑就是類加載的時候會有一個標記一個類和另外在同一個dex中的標記,所以打出補丁包之後會報 “兩個類所在的dex不在一起” 的錯誤,這個也好辦,打出一個單獨的dex,裏面只有一個類,讓每個類都引用這個類,這樣就使得每個類的標記都是false。Nuwa實現了這個插入引用單獨類的插件,對使用者非常友好。
Nuwa能自由的修改和添加類,功能更加強大,不過這裏面隱含了一個缺陷就是hook classloader的dexElements必須在所有類沒有加載之前進行,所以一般放在application的oncreate方法中,這樣就導致了每次發佈補丁必須重啓app才能生效。
微信的熱修復方案
其實技術方案的迭代也是思緒不斷延伸的過程,看過Nuwa的熱修復方案之後,是不是會想到有沒有更簡單更優雅的方式,有!其實很容易想到,我們在客戶端實現補丁包的邏輯是什麼呢,無非是比較兩個dex中類文件發生更改的類提取出來,打成新的補丁dex,那這個過程能不能在客戶端逆向操作一次呢,直接將差分包和原dex進行融合,形成新的dex,這樣代碼就不用做任何修改了,答案是肯定的。微信在一篇博客中闡述了自己熱修復方案的主要思路(微信Android熱補丁實踐演進之路)。沒有具體實現,主要可能是文件權限的一些坑,但是像微信這樣的app架構中,肯定是做了精準的分包處理,自己管理dex的加載策略,所以實現起來應該非常順利。
微信在文中也坦言做熱修復起源於15年6月,相對較晚,也可以綜合比較之後設計出適合自己的方案。
以上是目前比較成熟的幾個熱修復方案,只是整理了主要思想,還有很多黑科技沒有get到,希望對大家能有所幫助。