百度App效能優化工具篇 - Thor原理及實踐

語言: CN / TW / HK

一.  背景

App開發過程中,如果遇到一些疑難問題或者效能問題 (如低端機卡頓) ,由於沒法拿到更多系統的有效資訊很難有效定位。 這時,Hook不失為一種好的解決方案,Hook技術是在程式執行的過程中,動態修改程式碼,植入自己的程式碼邏輯,修改原有程式執行流程的技術。 Hook技術有如下幾點能力:

耗時監控 】在程式碼前後動態插入Trace打點,統計耗時;

效能監控 】IO監控、記憶體監控、View繪製監控、大圖檢測等;

安全審查 】Hook敏感API (例如定位) ,用以安全評審;

逆向和脫殼 】對病毒樣本進行逆向和脫殼分析;

Hook技術由來已久,目前業界Java和Native Hook都有不少優秀的開源框架,但是如果我們需要將Hook能力使用到線上,都或多或少有些問題,例如相容性、穩定性、動態性等等。

鑑於此,我們開發了一套 Thor容器框架 ,提供標準的Hook介面, 降低學習成本 ,同時將開源框架按照介面 適配成外掛動態下發按需安裝,保證Hook能力的完備和輕量性 ,並且後續出現更加優秀以及自研的框架的可以 無縫的接入和Hook能力拓展 ,並且不需要上層業務程式碼和外掛進行適配, 保證相容性

.  現狀

Android系統的程式語言主要分為Java和C/C++,Hook方向也主要分為Native和Java Hook兩種,其中Native Hook原理也主要分為PLT / Inline Hook兩大類,然後Java Hook也分為替換入口點Hook (Replace Entrypoint Hook) 和類Inline Hook兩大類。

Native 方法執行流程大概如下:

Native 方法執行過程中會先通過PLT表找到GOT表中函式全域性偏移地址,然後執行其機器碼指令,PLT Hook主要是指通過修改GOT中函式的全域性偏移地址來達到Hook的目的,代表框架如:xHook、bHook等;Inline Hook則主要是指直接將函式執行的機器碼指令進行修改和指令修復來達到Hook的目的,代表框架如:Android-Inline-Hook等。

GOT(Global Offset Table):全域性偏移表用於記錄在 ELF 檔案中所用到的共享庫中符號的絕對地址。
PLT(Procedure Linkage Table):過程連結表的作用是將位置無關的符號轉移到絕對地址。當一個外部符號被呼叫時,PLT 去引用 GOT 中的其符號對應的絕對地址,然後轉入並執行。

Java 方法執行 流程大概如下:

Java 方法執行過程中會通過方法在虛擬機器中對應的結構Method或ArtMethod結構體中的入口點 (Entrypoint) ,來找到對應的位元組碼/機器碼指令執行。替換入口點Hook (Replace Entrypoint Hook) 是指替換Method/ArtMethod中的入口點來達到Hook的目的,代表框架如:Xposed、Dexposed、YAHFA等;類Inline Hook是指將入口點對應的位元組碼/機器碼進指令進行修改和指令修復來達到Hook的目的,代表框架如:Epic等,由於安卓虛擬機器的JIT/AOT機制的存在,函式執行地址可能會進行各種變化,所以通常會將位元組碼強行編譯成機器碼,然後統一通過修改機器碼指令來Hook。

2 . 1  常見Native Hook框架

2.1.1 xHook框架

xHook框架通過PLT Hook方案來實現的,PLT Hook是通過直接修改GOT表,使得在呼叫該共享庫的函式時跳轉到的是使用者自定義的Hook功能程式碼。流程如下:

瞭解PLT Hook的原理之後,知道該Hook方式有如下特點:

  • 由於修改的是GOT表中的資料,因此修改後,所有對該函式進行呼叫的地方就都會被Hook到。這個效果的影響範圍是該PLT和GOT所處的整個so庫。

  • PLT與GOT表中僅僅包含本ELF需要呼叫的共享庫函式專案,因此不在PLT表中的函式無法Hook到 (比如非export匯出函式就無法Hook到)

2.1.1 Andorid-Inline-Hook框架

Inline Hook的原理則是直接修改函式在.text實際執行的機器碼來實現Hook,不僅對所有SO生效,還能Hook非export匯出函式,補齊了PLT Hook方法的不足。流程如下:

但是由於你直接修改的是機器碼指令,由於指令架構版本的差異以及後續要進行指令修復,容易有相容性的問題。

2 . 2  常見Java Hook框架

2.2.1 Dexposed框架

Dexposed框架只支援Dalvik虛擬機器,此虛擬機器通過Method結構體中accessFlags欄位來判斷當前方法是位元組碼還是機器碼。該框架通過修改accessFlags欄位為ACC_NATIVE,將Java原方法註冊為Native方法,呼叫時會先呼叫Native Hook方法,再反射呼叫Java原方法來實現Hook的目的,流程圖如下所示:

2.2.2 Epic框架

Epic框架則是在Dexposed的基礎上,增加了對ART虛擬機器Hook的能力。由於ART虛擬機器的複雜性 (AOT和JIT) ,Java程式碼執行的入口可能隨時都在變化,如果通過ArtMethod中的entry_point_from_quick_compiled_code_欄位入口進行Hook,可能會發生不可預期的崩潰。Epic則是在 Wißfeld, Marvin 的論文 ArtHook: Callee-side Method Hook Injection on the New Android Runtime ART 基礎上做了實現,大概思路是把 entry_point_from_quick_compiled_code_ 指向的機器碼地址 (未編譯的位元組碼也會強制編譯成機器碼,類似於Inline Hook) 進行修改,跳轉到跳板程式碼,然後通過跳轉程式碼進行分發,呼叫Hook方法之後再呼叫原方法,來達到Hook的目的。流程圖如下:

2 .3  常見框架對比

通過分析和對比可知,開源框架存在比較典型的幾個問題如下:

  • Hook能力不完備 :無法同時滿足所有的Hook場景 (Java Hook和Native Hook)

  • 相容性問題 :由於現有框架可能存在各種各樣的穩定性問題,導致如果後續替換Hook框架,則所有的業務Hook邏輯都要修改存在相容性問題;

  • 不支援動態Hook :只能將程式碼內建到主包中,沒法動態下發安裝實現動態Hook;

  • 沒有容錯機制 :大部分框架都有穩定性問題且沒有容災機制,如果導致應用崩潰,會導致災難性的後果。

三.  方案選型

從現有狀況來看,如果同時需要Java/Native Hook的能力,那麼至少需要整合兩個框架,業務程式碼也只能在主包中編寫, 增加包體積 。其次如果替換使用更加優秀或者自研的框架時,所有的業務程式碼也要跟著修改, 學習和適配相容的成本巨大 。最後Hook框架導致的崩潰,因為沒有動態能力和容災機制也只能 重新發布應用和鋪渠道,影響使用者體驗

雖然每個框架都有各自的一些問題,但是要求我們從頭開始開發一款同時支援Java和Native Hook的框架,沒有穩定性問題並且相容所有安卓版本、輕量且容災的框架,重複造輪子並且ROI太低,所以我們要開發自己的一套容器框架,取長處補短板,充分利用好已有的框架來實現目標。

百度App作為超級App,本身就是一個航空母艦,容器框架要在其上線至少需要達到以下幾點要求:

  • 完備性 :需要支援所有的Hook能力 (Java和Native Hook) ,能夠覆蓋所有程式碼範圍;

  • 相容性 外掛保證向後相容,即使替換底層Hook框架,業務完全無感知,不需要重新學習和適配新的Hook框架;

  • 輕量動態性 體積要儘量保證輕量,這對於手尤為重要,並且支援通過雲控下發的方式動態安裝執行;

  • 容災性 發生連續啟動崩潰時可以自關閉恢復,不會持續影響線上使用者。

.  Thor揭祕

為了滿足上述要求,我們開發了Thor容器框架,提供標準的Hook介面,包含Java和Native Hook介面,業務方不需要關心底層實現邏輯 (如同虛擬檔案系統VFS) ,只需要瞭解如何使用這些介面即可,極大的 降低學習接入成本 。同時將穩定的開源框架按照介面 適配成外掛 ,將這些Hook能力進行抽象, 按需動態的安裝載入,保證Hook能力的完備性和輕量性 。並且後續出現更加優秀以及自研的框架的可以 無縫的接入 ,對上層也是無感知的,不需要上層業務程式碼和外掛進行適配,很好的 保證了相容性

4 . 1  Thor整體結構

4.1.1 Thor架構圖

  • 支撐業務 :支撐了低端機、隱私合規、OOM和流水線等多個業務;

  • Thor抽象層:主要包含Java / Native Hook和Thor Module的業務模組等抽象層介面;

  • 應用層外掛:包含了SP、IO、執行緒、記憶體等基礎外掛或者業務相關外掛,其適配實現了Thor Module的業務模組介面;

  • 實現層外掛:Epic (Java Hook) 、xHook (PLT Hook) 、Android-Inline-Hook (Inline Hook) 或者自研等外掛,其適配實現了Java / Native Hook介面;

  • Thor框架

    • 外掛模組: 支援自主開發外掛,支援外掛熱插拔,可以通過內建或雲控動態下發, 即時生效 維護和排程外掛的生命週期;

    • 沙盒模組: 支援在沙盒程序安裝外掛,不影響主程序, 重啟生效

    • 校驗模組: 支援對外掛進行安全校驗,保證外掛來源安全性;

    • 外掛管理介面: 支援對已有外掛動態安裝和解除安裝的控制管理介面。

Thor實現層外掛和Thor應用層外掛都是apk的形式存在,但是也可以以元件原始碼的形式整合打包到宿主中。

4 . 2  Thor核心優勢

4.2.1 易用性

Thor只開發抽象層介面,底層實現對業務是不可見的,不需要反覆學習, 這樣最大程度的保證了易用性 。Java/Native Hook都提供了標準的介面供業務方使用,介面如下:

  • Java Hook介面 (Thor提供Java Hook能力的介面)

public interface IHookEntity {
......
/**
* Hook指定的方法
*
* @param hookedMethod 待Hook的方法Method物件
* @param hookCallback Hook回撥{@link IHookCallback}
*/
void hookMethod(Member hookedMethod, IHookCallback hookCallback);

......
}

如果是Java Hook使用方只需要直接使用該介面的能力即可;如果是能力提供方,則需要將Java Hook能力注入到Thor抽象層的Java Hook介面實現中。

  • Native Hook介面 (Thor提供Native Hook能力的介面,包含PLT Hook和Inline Hook)

struct thor_abstract {
// 函式定義:PLT Hook實現框架的函式指標
// lib_name 被Hook的so庫的名稱
// symbol 被Hook的函式名
// hook_func Hook方法
// backup_func 原方法備份(函式指標)
int (*thor_plt_hook)(const char *lib_name, const char *symbol, void *hook_func, void **backup_func);
// 函式定義:Inline Hook實現框架的函式指標
// target_func 原方法
// hook_func Hook方法
// backup_func 原方法備份(函式指標)
int (*thor_inline_hook)(void *target_func, void *hook_func, void **backup_func);
// PLT Hook二期(新增介面,支援批量plt hook)
struct thor_plt_ext *plt_ext;
};

如果是Nava Hook 使用方只需要直接 使用該介面的能力即可;如果是能力提供方,則需要將Nava Hook能力注入到Thor抽象層的Native Hook介面實現中。

  • Thor Module介面 (Thor提供的業務模組介面)

public abstract class ThorModule implements IThorModule {
/**
* 排程外掛的載入生命週期
*/
public abstract void handleLoadModule();


/**
* 宿主通知和更新外掛配置資訊生命週期
*/
public void onPluginFuncControl(PluginFuncInfo pluginFuncInfo) {
}
}

主要提供給業務模組使用,如果需要使用Hook能力,直接在handleLoadModule子類實現中呼叫Thor的各個Hook能力即可 (不是必須使用的,Thor作為容器框架只是額外提供了Hook的能力而已)

4.2.2 完備性

該框架同時支援Java / Native Hook的能力,具有完備的Hook能力 。上小節講解了提供給業務方的Java/Native Hook和 Thor Module業務模組等抽象層介面,底層實現則根據介面進行適配之後,通過靜態程式碼依賴注入或動態模組載入注入到抽象層實現中,這樣Thor就具備了完備的Hook能力。

  • Thor的 Java Hook 能力 (類Xposed API)

Hook Handler#dispatchMessage方法,程式碼如下:

ThorHook.findAndHookMethod(Handler.class, "dispatchMessage", new IHookCallback() {
@Override
public void beforeHookedMethod(IHookParam param) {
Message msg = (Message) param.getArguments()[0];
Log.d(TAG, ">>>>>>>>>>>dispatchMessage: " + msg);
}

@Override
public void afterHookedMethod(IHookParam param) {
Log.d(TAG, "<<<<<<<<<<<<dispatchMessage: ");
}
}, Message.class);

繼續看Thor#findAndHookMethod的邏輯,程式碼如下:

/**
* 尋找方法並將其Hook,最後一個引數必須是Hook方法的回撥
*
* @param clazz Hook方法所在類的類名稱
* @param methodName Hook方法名
* @param hookCallback 回撥{@link IHookCallback}
* @param parameterTypes Hook方法的引數型別
*/
public static void findAndHookMethod(Class<?> clazz, String methodName,
IHookCallback hookCallback, Class<?>... parameterTypes) {
......
Method methodExact = ThorHelpers.findMethodExact(clazz, methodName, parameterTypes);
hookMethod(methodExact, hookCallback);
......
}

ThorHook#findAndHookMethod通過類的類型別、函式名和引數,找到相應的Method,再呼叫ThorHook#hookMethod進行Hook,繼續看如下程式碼:

/**
* Hook指定的方法
*
* @param hookedMethod 待Hook的方法Method物件
* @param hookCallback Hook回撥{@link IHookCallback}
*/
public static void hookMethod(Member hookedMethod, IHookCallback hookCallback) throws HookMethodException {
......
CallbacksHandler callbacksHandler;
synchronized (sHookedMethodCallbacks) {
callbacksHandler = sHookedMethodCallbacks.get(hookedMethod);
if (callbacksHandler == null) { // 未Hook過的Method
callbacksHandler = new CallbacksHandler();
callbacksHandler.register(hookCallback);
sHookedMethodCallbacks.put(hookedMethod, callbacksHandler);
} else { // Hook過的Method,只需要註冊回撥即可
callbacksHandler.register(hookCallback);
return;
}
}

ThorManager.getInstance().getHookEntity().hookMethod(hookedMethod, callbacksHandler);
}

多個業務方如果Hook了同一個java 方法,會被加到快取中,Hook回撥的時候再逐個進行分發;繼續可以看到hookMethod最後呼叫到了getHookEntity#hookMethod方法,最終會呼叫到具體Java Hook框架實現的hookMethod方法,例如Epic的適配程式碼如下:

/**
* Epic框架適配類
*/
public class EpicHookEntity implements IHookEntity {
@Override
public void hookMethod(Member hookedMethod, final IHookCallback hookCallback) {
// Epic Hook方法回撥
XC_MethodHook xc_methodHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
// 將Epic Hook方法資訊包裝成抽象層Hook方法資訊
IHookParam hookParam = new EpicHookParam(param);


if (hookCallback != null) {
// 呼叫before回撥
hookCallback.beforeHookedMethod(hookParam);
}
}


@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// 將Epic Hook方法資訊包裝成抽象層Hook方法資訊
IHookParam hookParam = new EpicHookParam(param);


if (hookCallback != null) {
// 呼叫after回撥
hookCallback.afterHookedMethod(hookParam);
}
}
};


// Epic Hook Method
DexposedBridge.hookMethod(hookedMethod, xc_methodHook);
}
}
  • Thor的 Native Hook 能力

使用PLT Hook 對應SO所在PLT表的open函式,Inline Hook puts方法,部分程式碼如下:

thor_abstract *impl = reinterpret_cast<thor_abstract *>(nativePtr);


// plt hook open
thor->thor_plt_hook(so_name, "open", (void *) ProxyOpen, (void **) &original_open);


// inline hook puts
impl->thor_inline_hook((void *) puts, (void *) new_puts, (void **) &origin_puts);

根據4.2.1中的Native Hook介面可知,thor_plt_hook和thor_inline_hook成員都是函式指標,指標只有指向真正的Native Hook能力,程式碼才會生效,所以相應的Hook框架也需要根據Native Hook介面進行適配,例如xHook適配PLT Hook部分程式碼如下:

thor_abstract *thor = reinterpret_cast<thor_abstract *>(nativePtr);
// plt hook函式指標賦值
thor->thor_plt_hook = xhook_impl_plt_hook;
.....


// xhook適配部分程式碼
int xhook_impl_plt_hook(const char *so_name, const char *symbol, void *new_func, void **old_func) {
void *soinfo = xhook_elf_open(so_name);
if (!soinfo) {
return -1;
}


if (xhook_hook_symbol(soinfo, symbol, new_func, old_func) != 0) {
return -2;
}


xhook_elf_close(soinfo);
return 0;
}

Android-Inline-Hook適配Inline Hook介面部分示例程式碼如下:

// inline hook函式指標賦值
thor->thor_inline_hook = impl_inline_hook;


// andorid-inline-hook適配部分程式碼
int impl_inline_hook(void *target_func, void *new_func, void **old_func) {
if (registerInlineHook((uint32_t) target_func, (uint32_t) new_func, (uint32_t **) old_func)) {
return -1;
}


if (inlineHook((uint32_t) target_func) != ELE7EN_OK) {
return -2;
}


return 0;
}

我們在使用這些底層Hook框架適配元件 (外掛) 的過程中,也遇到了一些問題,例如Epic在Hook Handler#dispatchMessage的過程中,會發生不符合預期的崩潰,但是在進一步調研了SandHook可以解決該問題之後,馬上就適配了SandHook的實現來解決問題,業務方的程式碼不需要做任何修改和適配,再例如xHook的作者新寫了一款PLT Hook框架bHook,解決了xHook的一些問題 (例如增量Hook,unHook能力等等) ,我們也很快跟進對bHook框架進行了調研和適配,同樣業務方也是無感知的, 這兩個例子從側面佐證了Thor容器框架具有良好的相容性和可擴充套件性。

同時同學們可能會有如下疑惑,如果Hook框架出問題,難道只能去找更好的開源方案進行適配嗎?有沒有銀彈呢?這其實就回到了方案選型時所說的,由於安卓的碎片化和複雜性,從頭開始開發一款同時支援Java和Native Hook的框架,沒有穩定性問題並且相容所有安卓版本、輕量且容災的框架,重複造輪子並且ROI太低,所以我們要開發自己的一套容器框架, 取長處補短板,充分利用好已有的框架來實現目標 ,當然也不排除在所有開源方案都不滿足的情況下,進行深度二次開發或者自研底層Hook框架,不過這些對業務程式碼都不可見,不需要修改適配。

4.2.3  輕量動態性

百度App作為一個航母級應用,對於包體積大小還是比較敏感的,根據Google Store的資料,包體積每增加6M,就降低1%的轉化率,影響巨大,所以Thor容器框架要儘可能的做到輕量,基於此,我們需要把業務程式碼做成動態載入的外掛,甚至是底層適配的Hook實現也要做成動態可載入的外掛。

業務程式碼可以不在宿主中編寫,只在外掛程式碼中編寫, 然後將生成的外掛動態下發到手機上,再通過外掛載入模組動態載入生效。 例如:在需要監控應用IO的情況下,下發IO外掛和xHook外掛到手機上安裝,Hook IO操作 (例如:open、read、write等) ,將不合理的IO操作上報給平臺,同時在不需要監控的時候動態解除安裝關閉即可。

外掛動態載入生效的大致流程如下:

  • Thor容器框架會對外掛進行v2簽名校驗,保證外掛來源的安全性;

  • 解析外掛中的清單檔案儲存為info物件,包含外掛包名、外掛入口類、ABI List、外掛版本等等;

  • 對外掛中的SO進行釋放,不然classloader會找不到外掛中的SO;

  • 建立自定義的ThorClassLoader進行外掛類載入,會先載入外掛中的類再載入宿主中的類,部分程式碼如下:

/**
* 先載入Thor外掛中的類,再載入宿主中的類
*
* @param name
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = null;
try {
// 載入外掛中的類
clazz = findOwnClass(name);
} catch (ClassNotFoundException ignored) {
// ignored
}


if (clazz != null) {
return clazz;
}


// 載入宿主中的類
return mHostClassLoader.loadClass(name);
}
  • 例項化外掛入口類並判斷其介面型別,注入相應的能力到Thor抽象層:

  1. 如果是Java Hook介面實現類,則注入Java Hook例項能力給Thor抽象層;

  2. 如果是Native Hook介面實現類,則注入Native Hook例項能力給Thor抽象層;

  3. 如果是Thor Module業務介面實現類,則將業務例項儲存到map中,等待後續外掛管理模組排程相應的生命週期。

大概流程圖如下:

這裡大家可能會有以下疑問:

  • 如果上層的業務層外掛先安裝,底層實現層外掛後安裝的情況怎麼辦?

    Thor有一個pending模式會等到實現層安裝生效之後,業務層的邏輯再開始執行生效;

  • Android 8.0的classloader有namespace機制,不同的classloader載入相同SO,會有多份SO在記憶體中,這個時候如何將外掛中Native Hook能力傳遞給Thor抽象層呢?

通過翻看原始碼,Binder呼叫中的Parcel類擁有Native物件的記憶體指標,所以我們也借鑑相同的方法,將Native物件記憶體指標地址通過Java層進行傳遞,然後使用擁有相同記憶體佈局的struct結構體進行轉換,這樣就可以拿到Native Hook實現了。

4.2.4 容災性

Hook技術畢竟是一個比較hack的技術,誰也無法保證百分百的相容和穩定性,所以我們要針對這種特殊的崩潰情況進行兜底,將該框架可能造成的影響降到最低。目前有三個容災能力:

  • Thor容器框架在及時性要求不高的情況下,支援沙盒程序安裝。如果安裝過程中發生了崩潰,不會影響主程序,使用者無感知,並且會自動回滾外掛進行止損;

  • Thor容器框架會結合安全模式,可以監控連續啟動崩潰次數,如果超過閾值,就自動關閉Thor框架,快速自恢復及時止損;

  • 通過百度內部的效能平臺監控Thor相關崩潰,可以通過雲控動態關閉Thor框架。

通過這三個容災能力,基本能夠保證百度App不會因為Thor容器框架發生大規模的崩潰影響使用者體驗,能夠較好的管理風險。

五.  業務實踐案例

Thor框架作為一套動態外掛容器基礎設施, 真正讓其起作用的是豐富的外掛生態 (如IO、記憶體、執行緒、隱私等等) ,可以根據實際需要, 大膽的發揮想象,開發適合業務場景的外掛 。目前該框架可以應用於 線下RD開發、線下流水線和線上雲控開啟 ,由於篇幅限制,摘選其中一些案例講述。

5 . 1  執行緒外掛

由於在開發過程中隨手就可以建立一個執行緒執行,也沒有強制約束使用執行緒池,這樣會導致很多遊離執行緒,執行緒過多不僅會提高記憶體使用導致IO放大和OOM崩潰,並且有頻繁的上下文切換會導致啟動和流暢度問題。執行緒外掛則通過Thor框架的PLT Hook能力Hook libart.so庫中的pthead_create的函式,來監控執行緒的建立。核心程式碼如下:

// 原始被方法函式指標
static void *(*origin_pthread_create)(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) = NULL;


// Hook之後的Proxy方法
void *proxy_pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) {
......


// 呼叫原始方法
void *ret = origin_pthread_create(thread, attr, start_routine, arg);
// 列印Java堆疊
printJavaStackTrace();
return ret;
}


void doHook(long nativePtr){
thor_abstract *impl = reinterpret_cast<thor_abstract *>(nativePtr);
// plt hook pthread_create
impl->thor_plt_hook("libart.so", "pthread_create", (void *) proxy_pthread_create, (void **) &origin_pthread_create);}
}

Hook完成之後,會在建立執行緒的過程中先呼叫  proxy_pthread_create  的代理方法再呼叫原始的建立執行緒方法,在代理方法中通過反射列印建立當前執行緒的Java堆疊。在 百度App啟動階段 通過執行緒外掛監控記錄發現有 100+ 個SP執行緒,和 50+ 非執行緒池管理的執行緒,嚴重影響啟動速度和使用者體驗。協助組內同學進行優化後 (SP遷移KV元件,所有執行緒通過執行緒池管理) ,降低啟動過程中執行緒數 100+ ,優化TTI(Time To Interactive) 1s+

5 .2  IO外掛

由於在開發過程中有同學會把一些IO操作在主執行緒中執行,例如檔案讀寫、網路傳輸,這樣會導致主執行緒卡頓,影響啟動速度、流暢度等,即使是 小檔案 也可能因為記憶體不足和磁碟不足等原因 導致IO讀寫放大 ,從而導致長耗時的IO,同時還有一些不合理的IO,例如:讀寫buffer過小會導致頻繁的系統呼叫影響效能,以及重複讀同一個檔案等。IO外掛則通過Thor框架的 PLT Hook能力 Hook IO操作 (open、read和write、close等) 用來記錄監控主執行緒不合理的IO 。核心程式碼如下:

......
thor->thor_plt_hook(so_name, "open", (void *) ProxyOpen, (void **) &original_open);
thor->thor_plt_hook(so_name, "read", (void *) ProxyRead, (void **) &original_read);
thor->thor_plt_hook(so_name, "write", (void *) ProxyWrite, (void **) &original_write);
thor->thor_plt_hook(so_name, "close", (void *) ProxyClose, (void **) &original_close);
......

呼叫open時會先呼叫ProxyOpen,ProxyOpen中會儲存fd (檔案描述符) 和IOInfo的map對映關係,後續的ProxyRead、ProxyWrite和ProxyClose則通過fd來完善IOInfo的資訊,IOInfo部分欄位如下:

class IOInfo {    public:
// 檔案路徑
const std::string file_path_;
// 開始時間
int64_t start_time_μs_;
// buffer大小
long buffer_size_ = 0;
// 連續讀寫次數
long max_continual_rw_cnt_ = 0;
// 檔案大小
long file_size_ = 0;
// 總耗時
long total_cost_μs_ = 0;
};

在最後檔案Close的時候通過分析IOInfo即可分析出不合理的IO操作 (例如主執行緒IO耗時過長、IO的buffer過小(導致系統呼叫增多)、重複讀等) 。在百度App啟動過程中通過IO外掛監控記錄發現有 20+ 不合理的IO操作,與各個業務方的同學進行協同和優化,最終啟動速度TTI優化 400ms+ ,提升了使用者體驗。

5 .3  隱私合規外掛

由於個人資訊法的頒佈,應用不可以在隱私彈窗確認前獲取使用者個人資訊,基於此,隱私合規外掛使用Thor框架的Java Hook的能力, 監控記錄隱私彈窗前不合理的隱私API呼叫 (例如定位、WI-FI、藍芽等等) ,部分程式碼如下:

// hook getDeviceId
ThorHook.findAndHookMethod(TelephonyManager.class, "getDeviceId", new IHookCallbackImpl(), String.class);

隱私合規外掛結合了手百內部通用防劣化流水線的能力(這裡不展開講解),每天自動編譯打包內建隱私合規外掛,然後自動在真機上測試,監控記錄隱私彈框前的隱私問題,最後自動分析、分發問題卡片給相應的業務同學進行修改, 有效的規避了合規風險,防止被下架整改

5 .4  記憶體外掛

記憶體優化是效能和穩定性優化中的一大課題,如果記憶體佔用過大, 輕則導致頻繁GC造成卡頓,重則記憶體溢位導致OOM應用崩潰。 記憶體外掛則通過Thor框架PLT Hook的能力,監控記錄Java堆記憶體和Native記憶體 (監控 malloc 和 free等函式) 。記憶體外掛目前有兩個使用場景:

  • 結合 線下 流水線的能力,每天自動編譯打包內建記憶體外掛,然後在真機上使用monkey隨機測試,在記憶體水位高時dump hprof檔案,並進行裁剪 (通過PLT Hook write方法實現,參考Tailor在hprof檔案寫入過程中過濾不需要的資訊) ,最後自動分析出記憶體洩漏問題和大物件問題,自動分發問題給相應的業務同學進行修改,將記憶體問題前置,防止問題上線影響使用者體驗。

  • 結合 線上 Thor豐富的雲控能力,動態下發到OOM使用者手機上開啟記憶體監控能力,然後回撈上報相關的資料進行問題分析、分發,解決線下不易復現的線上OOM崩潰問題。

六.  總結

Hook這個話題由來以久,框架種類繁多,但是沒有一款全面性、動態性以及相容性好的框架,但是正是有這些優秀的框架 (Xposed、Dexposed、Epic、xHook等) ,我們才能學習和借鑑其優秀的設計和理念,補齊不足,Thor只是在這條道路上邁出了一小步,後面需要更加 完善和夯實Thor基礎設施,並且豐富外掛生態 ,在Android效能和穩定性治理上添磚加瓦。

相關連結

[1] Dexposed連結: http://github.com/alibaba/dexposed

[2] ArtHook論文連結: http://publications.cispa.saarland/143/

[3] Epic連結: http://github.com/tiann/epic

[4] xHook連結: http://github.com/iqiyi/xHook

[5] Android-Inline-Hook連結: http://github.com/ele7enxxh/Android-Inline-Hook

[6] Tailor連結: http://github.com/bytedance/tailor

[7] Matrix連結: http://github.com/Tencent/matrix/