Android各大热补丁方案分析和比较
原文出處:http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/
最近開源界涌現(xiàn)了很多熱補丁項目,但從方案上來說,主要包括Dexposed、AndFix、ClassLoader(來源是原QZone,現(xiàn)淘寶的工程師陳鐘,在15年年初就已經(jīng)開始實現(xiàn))三種。前兩個都是阿里巴巴內(nèi)部的不同團隊做的(淘寶和支付寶),后者則來自騰訊的QQ空間團隊。
開源界往往一個方案會有好幾種實現(xiàn)(比如ClassLoader方案已經(jīng)有不下三種實現(xiàn)了),但這三種方案的原理卻徊然不同,那么讓我們來看看它們?nèi)叩脑砗透髯缘膬?yōu)缺點吧。
Dexposed
基于Xposed的AOP框架,方法級粒度,可以進行AOP編程、插樁、熱補丁、SDK hook等功能。
Xposed需要Root權(quán)限,是因為它要修改其他應用、系統(tǒng)的行為,而對單個應用來說,其實不需要root。 Xposed通過修改Android Dalvik運行時的Zygote進程,并使用Xposed Bridge來hook方法并注入自己的代碼,實現(xiàn)非侵入式的runtime修改。比如蜻蜓fm和喜馬拉雅做的事情,其實就很適合這種場景,別人反編譯市場下載的代碼是看不到patch的行為的。小米(onVmCreated里面還未小米做了資源的處理)也重用了dexposed,去做了很多自定義主題的功能,還有沉浸式狀態(tài)欄等。
具體到方法,可參見XposedBridge:
其具體native實現(xiàn)則在Xposed的libxposed_common.cpp里面有注冊,根據(jù)系統(tǒng)版本分發(fā)到libxposed_dalvik和libxposed_art里面,以dalvik為例大致來說就是記錄下原來的方法信息,并把方法指針指向我們的hookedMethodCallback,從而實現(xiàn)攔截的目的。
方法級的替換是指,可以在方法前、方法后插入代碼,或者直接替換方法。只能針對java方法做攔截,不支持C的方法。
來說說硬傷吧,不支持art,不支持art,不支持art。
重要的事情要說三遍。盡管在6月,項目網(wǎng)站的roadmap就寫了7、8月會支持art,但事實是現(xiàn)在還無法解決art的兼容。
另外,如果線上release版本進行了混淆,那寫patch也是一件很痛苦的事情,反射+內(nèi)部類,可能還有包名和內(nèi)部類的名字沖突,總而言之就是寫得很痛苦。
AndFix
同樣是方法的hook,AndFix不像Dexposed從Method入手,而是以Field為切入點。
先看Java入口,AndFixManager.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) {// 省略...判斷是否支持,安全檢查,讀取補丁的dex文件ClassLoader patchClassLoader = new ClassLoader(classLoader) {@Overrideprotected 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}// 找到了,加載補丁classclazz = dexFile.loadClass(entry, patchClassLoader);if (clazz != null) {fixClass(clazz, classLoader);}}} catch (IOException e) {Log.e(TAG, "pacth", e);} }看來最終fix是在fixClass方法:
private void fixClass(Class<?> clazz, ClassLoader classLoader) {Method[] methods = clazz.getDeclaredMethods();MethodReplace methodReplace;String clz;String meth;// 遍歷補丁class里的方法,進行一一替換,annotation則是補丁包工具自動加上的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);}} }private void replaceMethod(ClassLoader classLoader, String clz, String meth, Method method) {try {String key = clz + "@" + classLoader.toString();Class<?> clazz = mFixedClass.get(key);if (clazz == null) {// class not load// 要被替換的classClass<?> clzz = classLoader.loadClass(clz);// 這里也很黑科技,通過C層,改寫accessFlags,把需要替換的類的所有方法(Field)改成了public,具體可以看Method結(jié)構(gòu)體clazz = AndFix.initTargetClass(clzz);}if (clazz != null) {// initialize class OKmFixedClass.put(key, clazz);// 需要被替換的函數(shù)Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes());// 這里是調(diào)用了jni,art和dalvik分別執(zhí)行不同的替換邏輯,在cpp進行實現(xiàn)AndFix.addReplaceMethod(src, method);}} catch (Exception e) {Log.e(TAG, "replaceMethod", e);} }在dalvik和art上,系統(tǒng)的調(diào)用不同,但是原理類似,這里我們嘗個鮮,以6.0為例art_method_replace_6_0:
// 進行方法的替換 void replace_6_0(JNIEnv* env, jobject src, jobject dest) {art::mirror::ArtMethod* smeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(src);art::mirror::ArtMethod* dmeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);dmeth->declaring_class_->class_loader_ =smeth->declaring_class_->class_loader_; //for plugin classloaderdmeth->declaring_class_->clinit_thread_id_ =smeth->declaring_class_->clinit_thread_id_;dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;// 把原方法的各種屬性都改成補丁方法的smeth->declaring_class_ = dmeth->declaring_class_;smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;smeth->access_flags_ = dmeth->access_flags_;smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;smeth->method_index_ = dmeth->method_index_;smeth->dex_method_index_ = dmeth->dex_method_index_;// 實現(xiàn)的指針也替換為新的smeth->ptr_sized_fields_.entry_point_from_interpreter_ =dmeth->ptr_sized_fields_.entry_point_from_interpreter_;smeth->ptr_sized_fields_.entry_point_from_jni_ =dmeth->ptr_sized_fields_.entry_point_from_jni_;smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;LOGD("replace_6_0: %d , %d",smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_); }// 這就是上面提到的,把方法都改成public的,所以說了解一下jni還是很有必要的,java世界在c世界是有映射關(guān)系的 void setFieldFlag_6_0(JNIEnv* env, jobject field) {art::mirror::ArtField* artField =(art::mirror::ArtField*) env->FromReflectedField(field);artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;LOGD("setFieldFlag_6_0: %d ", artField->access_flags_); }在dalvik上的實現(xiàn)略有不同,是通過jni bridge來指向補丁的方法。
使用上,直接寫一個新的類,會由補丁工具會生成注解,描述其與要打補丁的類和方法的對應關(guān)系。
ClassLoader
原騰訊空間Android工程師,也是我的啟蒙老師的陳鐘發(fā)明的熱補丁方案,是他在看源碼的時候偶然發(fā)現(xiàn)的切入點。
我們知道,multidex方案的實現(xiàn),其實就是把多個dex放進app的classloader之中,從而使得所有dex的類都能被找到。而實際上findClass的過程中,如果出現(xiàn)了重復的類,參照下面的類加載的實現(xiàn),是會使用第一個找到的類的。
public Class findClass(String name, List<Throwable> suppressed) { for (Element element : dexElements) { //每個Element就是一個dex文件DexFile dex = element.dexFile;if (dex != null) {Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);if (clazz != null) {return clazz;}}}if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));}return null; }該熱補丁方案就是從這一點出發(fā),只要把有問題的類修復后,放到一個單獨的dex,通過反射插入到dexElements數(shù)組的最前面,不就可以讓虛擬機加載到打完補丁的class了嗎。
說到此處,似乎已經(jīng)是一個完整的方案了,但在實踐中,會發(fā)現(xiàn)運行加載類的時候報preverified錯誤,原來在DexPrepare.cpp,將dex轉(zhuǎn)化成odex的過程中,會在DexVerify.cpp進行校驗,驗證如果直接引用到的類和clazz是否在同一個dex,如果是,則會打上CLASS_ISPREVERIFIED標志。通過在所有類(Application除外,當時還沒加載自定義類的代碼)的構(gòu)造函數(shù)插入一個對在單獨的dex的類的引用,就可以解決這個問題。空間使用了javaassist進行編譯時字節(jié)碼插入。
開源實現(xiàn)有Nuwa, HotFix, DroidFix。
比較
Dexposed不支持Art模式(5.0+),且寫補丁有點困難,需要反射寫混淆后的代碼,粒度太細,要替換的方法多的話,工作量會比較大。
AndFix支持2.3-6.0,但是不清楚是否有一些機型的坑在里面,畢竟jni層不像java曾一樣標準,從實現(xiàn)來說,方法類似Dexposed,都是通過jni來替換方法,但是實現(xiàn)上更簡潔直接,應用patch不需要重啟。但由于從實現(xiàn)上直接跳過了類初始化,設置為初始化完畢,所以像是靜態(tài)函數(shù)、靜態(tài)成員、構(gòu)造函數(shù)都會出現(xiàn)問題,復雜點的類Class.forname很可能直接就會掛掉。
ClassLoader方案支持2.3-6.0,會對啟動速度略微有影響,只能在下一次應用啟動時生效,在空間中已經(jīng)有了較長時間的線上應用,如果可以接受在下次啟動才應用補丁,是很好的選擇。
總的來說,在兼容性穩(wěn)定性上,ClassLoader方案很可靠,如果需要應用不重啟就能修復,而且方法足夠簡單,可以使用AndFix,而Dexposed由于還不能支持art,所以只能暫時放棄,希望開發(fā)者們可以改進使它能支持art模式,畢竟xposed的種種能力還是很吸引人的(比如hook別人app的方法拿到解密后的數(shù)據(jù),嘿嘿),還有比如無痕埋點啊線上追蹤問題之類的,隨時可以下掉。
總結(jié)
以上是生活随笔為你收集整理的Android各大热补丁方案分析和比较的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DL动态加载框架技术
- 下一篇: Glide使用教程