背景

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, https://source.chromium.org/chromium/chromium/src/+/0ef8191485b6327872bf7f644ee8c2fb4861bb4a?originalUrl=https://cs.chromium.org/ )。但遠水解不了近渴,市面上 Android 10 以內的老版本 chromium 仍存在此類問題,依賴系統升級最終解決此類問題是遙不可及的,爲了不影響體驗需要應用層主動修復或者採取措施規避。

修復方案

通過上述的分析可知,只需要保證 standard_schemes 在初始化完成前不會有第二個線程執行同樣的邏輯即可。雖然沒有系統層面的同步方案,但問題拋出的點都集中在應用層的同一處,在這個位置加個同步限制即可解決!不過爲了保險起見還是在應用層做個全局防範(第一個執行完成之後放開限制)。西瓜視頻 app 是通過自研的 AOP 工具 hook 應用層所有 CookieManager.getCookie(String url)的調用。

此方案在西瓜視頻 app 432 版本灰度&全量上線後,再無此類問題,用很小的成本徹底解決了這類問題。

總結

調查 Native 問題時符號表信息是不可或缺的,大多數情況下可能缺少關鍵的符號信息,這給調查 Native 問題增加了很高的難度。但由於 Android 系統更新迭代的版本很多,加上廠商定製的差異,一些小衆機型或 Android 版本的 crash 可能攜帶着關鍵的符號信息,這些往往就是突破點,排查問題時小衆問題也應得到足夠的重視。我們同時呼籲手機廠商儘量保留一些關鍵的符號表信息,爲開發者保留一些可以方便定位問題的關鍵信息。

此外,雖然 http://androidxref.comhttps://cs.android.com 都可以在線查閱源碼,但這兩處的Android版本並不全。 https://android.googlesource.com 這裏可以下載到幾乎所有版本的源碼,本地通過 Sublime 分析源碼也十分方便(可以直接顯示和跳轉到方法的定義&引用位置)。

歡迎關注「字節跳動技術團隊」

相關文章