各大熱補丁方案分析和比較

各大熱補丁方案分析和比較

最近開源界涌現了很多熱補丁項目,但從方案上來說,主要包括DexposedAndFixClassLoader(來源是原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進行編譯時字節碼插入。

開源實現有NuwaHotFixDroidFix

比較

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的方法拿到解密後的數據,嘿嘿),還有比如無痕埋點啊線上追蹤問題之類的,隨時可以下掉。

在Mac 10.11編譯最新的Android 6.0
 
新的換膚思路

熱評文章

Mark's blog正在使用多說


發佈了6 篇原創文章 · 獲贊 6 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章