優(yōu)化踩坑記:百度地圖 SDK 延遲初始化必崩 std::bad_cast,兇手竟是 AVM SDK 里靜態(tài)鏈接的 libc++)
一、背景一次合理的啟動(dòng)優(yōu)化車機(jī) App 啟動(dòng)速度優(yōu)化思路很常規(guī)把非首屏必需的 SDK 初始化挪到首幀渲染之后。百度地圖 百度導(dǎo)航 SDK 首當(dāng)其沖——它初始化耗時(shí)高、首屏又用不到于是寫了一個(gè)延遲初始化器object DeferredSdkInitializer { private val mainHandler Handler(Looper.getMainLooper()) fun startAfterFirstFrame(application: Application) { // 首幀后延遲初始化百度地圖/導(dǎo)航 SDK mainHandler.postDelayed({ SDKInitializer.setAgreePrivacy(application.applicationContext, true) initBaiduNavi(application) SDKInitializer.initialize(application.applicationContext) SDKInitializer.setCoordType(CoordType.BD09LL) }, 0) } }在MainActivity第一幀繪制完成后觸發(fā)。邏輯看起來無懈可擊initialize只是從Application.onCreate挪到了首幀后執(zhí)行內(nèi)容完全一樣。然后崩了。必現(xiàn)。二、現(xiàn)象SIGABRT std::bad_castF DEBUG : signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr -------- F DEBUG : Abort message: terminating with uncaught exception of type std::bad_cast: std::bad_cast崩潰棧關(guān)鍵幀#05 liba2.so (__cxa_throw124) #07 libc_shared.so (std::__ndk1::locale::use_facet176) #08 libBaiduMapSDK_base_v7_6_4.so (_baidu_vi::CVCMMap::UrlEncode648) #09 libBaiduMapSDK_base_v7_6_4.so (Java_com_baidu_mapsdkplatform_comjni_util_JNIMD5_encodeUrlParamsValue80) #17 com.baidu.mapsdkplatform.comapi.BMapManagerInternal.permcheck86 #19 com.baidu.mapsdkplatform.comapi.Initializer.initialize126 #21 com.baidu.mapapi.SDKInitializer.initialize12 #23 xxxx.DeferredSdkInitializer.startAfterFirstFrame$lambda$124調(diào)用鏈SDKInitializer.initialize → permcheck → AppMD5.encodeUrlParamsValue → JNI → libBaiduMapSDK_base_v7_6_4.so 的 UrlEncode → locale::use_facet → 拋 std::bad_cast。第一反應(yīng)是用 try-catch 兜住不就行了——不行。這是native 層 SIGABRTJava 層的runCatching根本攔不住進(jìn)程直接死。三、排查崩潰棧里的雙面間諜仔細(xì)觀察崩潰棧發(fā)現(xiàn)一個(gè)詭異的地方use_facet這個(gè)函數(shù)在libc_shared.so里執(zhí)行#07 幀但它拋異常時(shí)調(diào)用的__cxa_throw卻在liba2.so里#05 幀等等liba2.so是誰百度地圖 SDK 的依賴嗎用 PowerShell 暴力搜了一下所有 AARGet-ChildItem -Path . -Recurse -Filter *.aar | ForEach-Object { $zip [System.IO.Compression.ZipFile]::OpenRead($_.FullName) $entries $zip.Entries | Where-Object { $_.FullName -match liba2\.so|libc\\_shared\.so } if ($entries) { $($_.FullName): $($entries.FullName -join , ) } $zip.Dispose() }結(jié)果D:\work\xxxx\mod_avm\libs\avmsdk-release.aar:jni/arm64-v8a/liba2.so, jni/arm64-v8a/libc_shared.soliba2.so 來自 360 環(huán)視AVMSDK再驗(yàn)證它的符號$bytes [System.IO.File]::ReadAllBytes($so) $text [System.Text.Encoding]::ASCII.GetString($bytes) __cxa_throw: $($text.Contains(__cxa_throw)) # True use_facet: $($text.Contains(use_facet)) # True std::bad_cast: $($text.Contains(std::bad_cast)) # True libc_shared.so: $($text.Contains(libc_shared.so)) # False ← 不依賴共享庫結(jié)論浮出水面liba2.so 靜態(tài)內(nèi)嵌了一份完整的 libc而且把__cxa_throw等符號導(dǎo)出到了全局符號表否則 libcshared.so 的代碼不可能調(diào)用到它。四、根因兩份 libc 的符號搶占進(jìn)程里出現(xiàn)了兩份 libc庫libc 來源使用方libc_shared.so共享版動(dòng)態(tài)鏈接百度地圖 SDK、火星 XLog 等liba2.so靜態(tài)內(nèi)嵌一份 導(dǎo)出符號360 環(huán)視 AVM SDKAndroid 動(dòng)態(tài)鏈接器對重名符號采用先加載先得first-loaded wins規(guī)則后加載的庫導(dǎo)出的重名符號不會覆蓋全局符號表中已有的符號反過來后加載的庫內(nèi)部對同名符號的引用會被解析到先加載的庫。于是時(shí)序就成了關(guān)鍵優(yōu)化前同步初始化Application.onCreate里百度 SDK 先執(zhí)行 →libc_shared.so先加載__cxa_throw先進(jìn)全局表 → 之后liba2.so加載符號搶不走 → 兩邊各用各的 libc不崩。優(yōu)化后延遲初始化車機(jī)場景下開機(jī)/倒車會自動(dòng)觸發(fā) 360 環(huán)視liba2.so先加載等百度 SDK 初始化時(shí)libc_shared.so加載其未定義的__cxa_throw被解析到了liba2.so 的靜態(tài) libc。百度 SDK 的UrlEncode → locale::use_facet拋bad_cast時(shí)異常對象由 liba2.so 的 RTTI 表處理與 libcshared.so 的 typeinfo 完全不一致 →std::bad_cast→ SIGABRT。本質(zhì)延遲初始化改變了 native 庫的加載順序暴露了 AVM SDK 靜態(tài)鏈接 libc 且未隱藏符號的隱患。五、解決方案治標(biāo)立刻可用讓共享版 libc 永遠(yuǎn)第一個(gè)加載在Application.onCreate最開頭super.onCreate()之后第一行任何其他 native 庫加載之前預(yù)加載override fun onCreate() { super.onCreate() // 預(yù)加載 libc_shared.so讓共享版 libc 符號最先進(jìn)入全局符號表 // 避免 avmsdk 的 liba2.so靜態(tài)內(nèi)嵌 libc 且導(dǎo)出符號搶先加載后 // 百度地圖 SDK 拋異常時(shí)串到 liba2.so 的異常處理導(dǎo)致 std::bad_cast 崩潰。 System.loadLibrary(c_shared) ... }原理libc_shared.so的符號先進(jìn)全局表之后liba2.so無論何時(shí)加載其靜態(tài) libc 符號都搶不走_(dá)_cxa_throw的解析權(quán)百度 SDK 與 AVM 各自用自己的 libc互不干擾。實(shí)測編譯驗(yàn)證通過崩潰消失。代價(jià)預(yù)加載一個(gè) 1~3MB 的 so約 10~30ms對啟動(dòng)耗時(shí)影響很小。治本需要廠商配合修 liba2.so 的符號導(dǎo)出方案 A最優(yōu)liba2.so 改為動(dòng)態(tài)鏈接公共libc_shared.soDT_NEEDED全進(jìn)程只有一份 libc徹底無沖突。這也是 NDK 官方推薦做法。方案 B次優(yōu)靜態(tài)鏈接但隱藏符號-Wl,--exclude-libs,ALL或 version script或-fvisibilityhidden-Bsymbolic讓靜態(tài) libc 的符號只在本庫內(nèi)部可見、不進(jìn)全局符號表。此時(shí)即使與公共 libc 共存也互不干擾。??注意單獨(dú)把 libc改個(gè)名字如 libcext.so是不夠的——只要它還導(dǎo)出__cxa_throw等符號照樣進(jìn)全局符號表跟公共版重名沖突依舊只是變成了誰先加載誰贏。六、經(jīng)驗(yàn)總結(jié)延遲初始化第三方 SDK 前先想清楚它的 native 庫加載順序。同一個(gè) SDK 同步初始化沒問題、延遲必崩大概率不是 SDK 的問題而是加載順序/環(huán)境變了。runCatching、try-catch 攔不住 native 崩潰SIGABRT/SIGSEGV崩潰處理要放到崩潰棧層面分析。排查 native 崩潰先看崩潰棧里符號歸屬use_facet在 libcshared.so、__cxa_throw在別的 so基本就是兩份 libc 共存 符號搶占的典型特征。類似的還有std::__ndk1::locale相關(guān)崩潰。對廠商 AAR 里的 so 保持警惕接入新 AAR 時(shí)可以用llvm-readelf --dyn-syms xxx.so檢查是否導(dǎo)出了異常的 libc 符號--exclude-libs,ALL是第三方 prebuilt 庫的標(biāo)配要求寫入采購/對接 checklist。多進(jìn)程、多 SDK 共存的 Android 工程libc_shared.so建議在 Application 最早期統(tǒng)一預(yù)加載一勞永逸規(guī)避符號搶占類的玄學(xué)崩潰。