各大熱補丁方案分析和比較
最近開源界涌現了很多熱補丁項目,但從方案上來說,主要包括Dexposed、AndFix、ClassLoader(來源是原QZone,現淘寶的工程師陳鍾,在15年年初就已經開始實現)三種。前兩個都是阿里巴巴內部的不同團隊做的(淘寶和支付寶),後者則來自騰訊的QQ空間團隊。
開源界往往一個方案會有好幾種實現(比如ClassLoader方案已經有不下三種實現了),但這三種方案的原理卻徊然不同,那麼讓我們來看看它們三者的原理和各自的優缺點吧。
Dexposed
基於Xposed的AOP框架,方法級粒度,可以進行AOP編程、插樁、熱補丁、SDK hook等功能。
Xposed需要Root權限,是因爲它要修改其他應用、系統的行爲,而對單個應用來說,其實不需要root。 Xposed通過修改Android Dalvik運行時的Zygote進程,並使用Xposed Bridge來hook方法並注入自己的代碼,實現非侵入式的runtime修改。比如蜻蜓fm和喜馬拉雅做的事情,其實就很適合這種場景,別人反編譯市場下載的代碼是看不到patch的行爲的。小米(onVmCreated裏面還未小米做了資源的處理)也重用了dexposed,去做了很多自定義主題的功能,還有沉浸式狀態欄等。
我們知道,應用啓動的時候,都會fork zygote進程,裝載class和invoke各種初始化方法,Xposed就是在這個過程中,替換了app_process,hook了各種入口級方法(比如handleBindApplication、ServerThread、ActivityThread、ApplicationPackageManager的getResourcesForApplication等),加載XposedBridge.jar提供動態hook基礎。
具體到方法,可參見XposedBridge:
1 2 3 4 5 |
/** * Intercept every call to the specified method and call a handler function instead. * @param method The method to intercept */ private native synchronized static void hookMethodNative(Member method, Class<?> declaringClass, int slot, Object additionalInfo); |
其具體native實現則在Xposed的libxposed_common.cpp裏面有註冊,根據系統版本分發到libxposed_dalvik和libxposed_art裏面,以dalvik爲例大致來說就是記錄下原來的方法信息,並把方法指針指向我們的hookedMethodCallback,從而實現攔截的目的。
方法級的替換是指,可以在方法前、方法後插入代碼,或者直接替換方法。只能針對java方法做攔截,不支持C的方法。
來說說硬傷吧,不支持art,不支持art,不支持art。
重要的事情要說三遍。儘管在6月,項目網站的roadmap就寫了7、8月會支持art,但事實是現在還無法解決art的兼容。
另外,如果線上release版本進行了混淆,那寫patch也是一件很痛苦的事情,反射+內部類,可能還有包名和內部類的名字衝突,總而言之就是寫得很痛苦。
AndFix
同樣是方法的hook,AndFix不像Dexposed從Method入手,而是以Field爲切入點。
先看Java入口,AndFixManager.fix
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
/** * 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) { @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 } // 找到了,加載補丁class clazz = dexFile.loadClass(entry, patchClassLoader); if (clazz != null) { fixClass(clazz, classLoader); } } } catch (IOException e) { Log.e(TAG, "pacth", e); } } |
看來最終fix是在fixClass方法
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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 // 要被替換的class Class<?> clzz = classLoader.loadClass(clz); // 這裏也很黑科技,通過C層,改寫accessFlags,把需要替換的類的所有方法(Field)改成了public,具體可以看Method結構體 clazz = AndFix.initTargetClass(clzz); } if (clazz != null) {// initialize class OK mFixedClass.put(key, clazz); // 需要被替換的函數 Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes()); // 這裏是調用了jni,art和dalvik分別執行不同的替換邏輯,在cpp進行實現 AndFix.addReplaceMethod(src, method); } } catch (Exception e) { Log.e(TAG, "replaceMethod", e); } } |
在dalvik和art上,系統的調用不同,但是原理類似,這裏我們嚐個鮮,以6.0爲例art_method_replace_6_0
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// 進行方法的替換 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 classloader dmeth->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_; // 實現的指針也替換爲新的 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世界是有映射關係的 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上的實現略有不同,是通過jni bridge來指向補丁的方法。
使用上,直接寫一個新的類,會由補丁工具會生成註解,描述其與要打補丁的類和方法的對應關係。
ClassLoader
原騰訊空間Android工程師,也是我的啓蒙老師的陳鍾發明的熱補丁方案,是他在看源碼的時候偶然發現的切入點。
我們知道,multidex方案的實現,其實就是把多個dex放進app的classloader之中,從而使得所有dex的類都能被找到。而實際上findClass的過程中,如果出現了重複的類,參照下面的類加載的實現,是會使用第一個找到的類的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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; } |
該熱補丁方案就是從這一點出發,只要把有問題的類修復後,放到一個單獨的dex,通過反射插入到dexElements數組的最前面,不就可以讓虛擬機加載到打完補丁的class了嗎。
說到此處,似乎已經是一個完整的方案了,但在實踐中,會發現運行加載類的時候報preverified錯誤,原來在DexPrepare.cpp
,將dex轉化成odex的過程中,會在DexVerify.cpp
進行校驗,驗證如果直接引用到的類和clazz是否在同一個dex,如果是,則會打上CLASS_ISPREVERIFIED標誌。通過在所有類(Application除外,當時還沒加載自定義類的代碼)的構造函數插入一個對在單獨的dex的類的引用,就可以解決這個問題。空間使用了javaassist進行編譯時字節碼插入。
比較
Dexposed不支持Art模式(5.0+),且寫補丁有點困難,需要反射寫混淆後的代碼,粒度太細,要替換的方法多的話,工作量會比較大。
AndFix支持2.3-6.0,但是不清楚是否有一些機型的坑在裏面,畢竟jni層不像java曾一樣標準,從實現來說,方法類似Dexposed,都是通過jni來替換方法,但是實現上更簡潔直接,應用patch不需要重啓。但由於從實現上直接跳過了類初始化,設置爲初始化完畢,所以像是靜態函數、靜態成員、構造函數都會出現問題,複雜點的類Class.forname很可能直接就會掛掉。
ClassLoader方案支持2.3-6.0,會對啓動速度略微有影響,只能在下一次應用啓動時生效,在空間中已經有了較長時間的線上應用,如果可以接受在下次啓動才應用補丁,是很好的選擇。
總的來說,在兼容性穩定性上,ClassLoader方案很可靠,如果需要應用不重啓就能修復,而且方法足夠簡單,可以使用AndFix,而Dexposed由於還不能支持art,所以只能暫時放棄,希望開發者們可以改進使它能支持art模式,畢竟xposed的種種能力還是很吸引人的(比如hook別人app的方法拿到解密後的數據,嘿嘿),還有比如無痕埋點啊線上追蹤問題之類的,隨時可以下掉。
熱評文章
回覆 mark: 有介紹嗎?
AndFix在阿里內部已經大規模使用了
回覆 xwl: cydia可以兼容art嗎?
回覆 mark: 這個也不能說是QQ空間方案,應該是QQ方案和dexpose結合。這種方案可以擴展到其他的各個領域,可以修改系統API的範疇。
回覆 xwl: 看了一下,其實空間的方法,原作者陳鍾已經有方案實現出來可以不用去阻止CLASS_ISPREVERIFIED了,呵呵。
熱補丁方案我覺得我的最好:地址http://blog.csdn.net/xwl198937/article/details/49801975
深度好文
好文,贊
阿宅文章太刁,我來up一下
翟神太刁,我來mark一下
宅神文章太屌,我來點個贊。
社交帳號登錄:
Mark's blog正在使用多說