背景
Android 平臺上長期存在一類發生在 app 呼叫 CookieManager.getCookie(String url) 過程中的 native crash,困擾著很多研發,也嚴重影響了使用者體驗。此類問題 Android 4.1-9.0 均有覆蓋,基本都發生在啟動階段。西瓜視訊上此類問題長期佔據 Top 3 榜單之一,存在時間已相當久遠。在 Top 10 的 native crash 中佔比超過 40%,Native crash 整體佔比>30%,影響使用者比例>1‰(此類 crash 的使用者佔比);主要集中在 Android 4.2.2、4.4.2、8.1、9.0 等版本上,其他 Android 版本上也均大量此類問題。最為嚴重的是此類 crash 基本都發生在啟動 2s 以內,嚴重影響西瓜視訊 app 的使用者體驗。其典型堆疊截圖如下:
Native 堆疊
Java 堆疊(有>50%的 crash 沒有 Java 堆疊)
排查思路
此類 crash 堆疊中只有 so 及偏移地址資訊,沒有相應的函式名,又不是必現問題,很難直接定位到問題原因。所以排查的關鍵是先找到有明確函式名的堆疊,有了詳細的函式資訊,才能進一步通過相關的函式名對照 AOSP 原始碼分析定位出原因。
初步調查
出現問題的 Android 版本和機型雖分佈極廣(Android 4.x - 9.0),但絕大部分堆疊幾乎沒有任何 Crash 相關的核心函式資訊。幸運的是通過梳理所有相關的 crash,發現 Android 4.2.2 上有一類 crash 有一個函式資訊_ZN4GURLC2ERKSs(GURL::GURL(std::string const&))。
這類 crash 的堆疊跟上述問題是一致的,都是在 Java 層呼叫到 nativeGetCookie 時 native 層出現了 crash,堆疊也基本相同,可以判定是一類問題。拉取並分析 Android 4.2.2 GURL 相關的原始碼,發現 GURL 涉及到的程式碼也是非常廣的,具體哪個環節哪一層呼叫了 memmove 函式有點兒大海撈針。
既然能搜到 GURL 相關,猜測似乎跟 URL 相關。於是線上做了個簡單的實驗,看看是不是 getCookie 時傳入的 URL 的問題。通過 hook 應用層所有 CookieManager.getCookie 的呼叫發現,發生 crash 時均存在多個執行緒同時呼叫 CookieManager.getCookie,懷疑可能是執行緒安全問題。
僅有這些資訊是不夠的,如果能拿到 crash 時的函式名,問題才能被確認。再次梳理這類包含有 GURL 堆疊的 crash 時發現,果然存在這類堆疊(ZN8url_util20LowerCaseEqualsASCIIEPKcS1_S1)。
同時梳理出的這種有明確上層 crash 函式名的堆疊還有以下兩種,均是 GURL 的建構函式執行過程中出現的 crash,這其中有一類是 vector 相關的操作異常(vector 是非執行緒安全,這個本人印象很深刻,AOSP 原始碼裡存在很多這類 vector 執行緒安全的問題:如 RenderNodeAnimator 等),這類異常也進一步加深了執行緒安全問題的懷疑。
深入分析
ZN8url_util20LowerCaseEqualsASCIIEPKcS1_S1 的原形是 url_util::LowerCaseEqualsASCII(char const*, char const*, char const*),這個堆疊跟前述問題是基本一致的,都是 crash 在 GURL::GURL(std::string const&)的建構函式呼叫鏈上,只是 crash 的原因不同。雖然不能簡單判定為是同一類問題,但種種跡象表面就是同一類問題。這個堆疊有明確的 crash 時的函式名,通過這個問題或許可以發現問題的根本原因。
根據 PC=5cb1453e 發現,crash 是因為 R2 暫存器裡為空(0x0)導致的,結合DoLowerCaseEqualsASCII 的原始碼可以判定 R2 寄存裡存的正是函式的第三個引數 b,這說明 crash 是因 b 為 null 導致的。
確認了 crash 的原因,再結合原始碼發現呼叫 url_util::LowerCaseEqualsASCII(char const*, char const*, char const*) 的且堆疊在 GURL 建構函式的呼叫鏈上的有兩處,一處是下圖裡的 CompareSchemeComponent 函式,另一處是 DoIsStandard 函式,相關原始碼截圖如下:
第一處 CompareSchemeComponent 函式的第三個引數正是LowerCaseEqualsASCII的第三個引數,但這個引數 kFileScheme 是個常量,不可能為 null,所以首先排除嫌疑。
第二處 DoIsStandard 裡的 LowerCaseEqualsASCII 的第三個引數是個全域性變數,是在 InitStandardSchemes 裡初始化的,仔細分析 InitStandardSchemes 的原始碼可以發現,standard_schemes 雖然是個全域性變數,但採用的是懶載入的方式初始化的。那麼問題來了,這個初始化過程/全域性變數是執行緒安全的嗎?
很遺憾這個函式並沒有加鎖,vector 也不是執行緒安全的,當然 std::vector<const char*>* standard_schemes 也就不是執行緒安全的。多個執行緒同時調到這裡的話就會出問題,當有執行緒正在初始化 standard_schemes 時,另一個執行緒可能也在執行初始化,這時會出 vector 操作的同步問題;同樣的,當一個執行緒正在遍歷 standard_schemes 時,另一個執行緒可能給 standard_schemes 重新設定了新的值,這時候就會有機率觸發空指標問題。
查閱 chromium 原始碼發現 Android 4.0-9.0 裡依賴的原始碼均存在 GURL 初始化的執行緒安全問題,該問題存在時間已經相當久遠,好在是已在 2019.05.21 提交了相關修復(Make //url initialization thread-safe)。但遠水解不了近渴,市面上 Android 10 以內的老版本 chromium 仍存在此類問題,依賴系統升級最終解決此類問題是遙不可及的,為了不影響體驗需要應用層主動修復或者採取措施規避。
修復方案
通過上述的分析可知,只需要保證 standard_schemes 在初始化完成前不會有第二個執行緒執行同樣的邏輯即可。雖然沒有系統層面的同步方案,但問題丟擲的點都集中在應用層的同一處,在這個位置加個同步限制即可解決!不過為了保險起見還是在應用層做個全域性防範(第一個執行完成之後放開限制)。西瓜視訊 app 是通過自研的 AOP 工具 hook 應用層所有 CookieManager.getCookie(String url)的呼叫。
此方案在西瓜視訊 app 432 版本灰度&全量上線後,再無此類問題,用很小的成本徹底解決了這類問題。
總結
調查 Native 問題時符號表資訊是不可或缺的,大多數情況下可能缺少關鍵的符號資訊,這給調查 Native 問題增加了很高的難度。但由於 Android 系統更新迭代的版本很多,加上廠商定製的差異,一些小眾機型或 Android 版本的 crash 可能攜帶著關鍵的符號資訊,這些往往就是突破點,排查問題時小眾問題也應得到足夠的重視。我們同時呼籲手機廠商儘量保留一些關鍵的符號表資訊,為開發者保留一些可以方便定位問題的關鍵資訊。
此外,雖然 androidxref.com 和 cs.android.com 都可以線上查閱原始碼,但這兩處的Android版本並不全。android.googlesource.com 這裡可以下載到幾乎所有版本的原始碼,本地通過 Sublime 分析原始碼也十分方便(可以直接顯示和跳轉到方法的定義&引用位置)。
歡迎關注位元組跳動技術團隊