真正的移動開發者,該如何直面App的崩潰率
作者 | 張紹文
來源 | 極客時間「Android 開發高手課」
在各種場合遇到其他產品的開發人員時,大家總忍不住想在技術上切磋兩招。第一句問的通常都是「你們產品的崩潰率是多少?」
程序員 A 自豪地說: 「百分之一。」
旁邊的程序員 B 鄙視地看了一眼,然後喊到: 「千分之一!」
「萬分之一」 ,程序員 C 說完之後全場變得安靜起來。
崩潰率是衡量一個應用質量高低的基本指標,這一點是你我都比較認可的。不過你說的「萬分之一」就一定要比我說的「百分之一」 更好嗎?我覺得,這個問題其實並不僅僅是比較兩個數值這麼簡單。
今天我們就來聊一聊有關「崩潰」的那些事,我會從 Android 的兩種崩潰類型談起,再和你進一步討論到底該怎樣客觀地衡量崩潰這個指標,以及又該如何看待和崩潰相關的穩定性。
Android 的兩種崩潰
我們都知道,Android 崩潰分為 Java 崩潰和 Native 崩潰。
簡單來說,Java 崩潰就是在 Java 代碼中,出現了未捕獲異常,導致程序異常退出。那 Native 崩潰又是怎麼產生的呢?一般都是因為在 Native 代碼中訪問非法地址,也可能是地址對齊出現了問題,或者發生了程序主動 abort,這些都會產生相應的 signal 信號,導致程序異常退出。
所以,「崩潰」就是程序出現異常,而一個產品的崩潰率,跟我們如何捕獲、處理這些異常有比較大的關係。Java 崩潰的捕獲比較簡單,但是很多同學對於如何捕獲 Native 崩潰還是一知半解,下面我就重點介紹 Native 崩潰的捕獲流程和難點。
Native 崩潰的捕獲流程
如果你對 Native 崩潰機制的一些基本知識還不是很熟悉,建議你閱讀一下《Android 平台 Native 代碼的崩潰捕獲機制及實現》。這裡我著重給你講講一個完整的 Native 崩潰從捕獲到解析要經歷哪些流程。
編譯端。編譯 C/C++ 代碼時,需要將帶符號信息的文件保留下來。
客戶端。捕獲到崩潰時候,將收集到儘可能多的有用信息寫入日誌文件,然後選擇合適的時機上傳到伺服器。
服務端。讀取客戶端上報的日誌文件,尋找適合的符號文件,生成可讀的 C/C++ 調用棧。
Native 崩潰捕獲的難點
Chromium 的 Breakpad 是目前 Native 崩潰捕獲中最成熟的方案,但很多人都覺得 Breakpad 過於複雜。其實我認為 Native 崩潰捕獲這個事情本來就不容易,跟當初設計 Tinker 的時候一樣,如果只想在 90% 的情況可靠,那大部分的代碼的確可以砍掉;但如果想達到 99%,在各種惡劣條件下依然可靠,後面付出的努力會遠遠高於前期。
所以在上面的三個流程中,最核心的是怎麼樣保證客戶端在各種極端情況下依然可以生成崩潰日誌。因為在崩潰時,程序會處於一個不安全的狀態,如果處理不當,非常容易發生二次崩潰。
那麼,生成崩潰日誌時會有哪些比較棘手的情況呢?
情況一:文件句柄泄漏,導致創建日誌文件失敗,怎麼辦?
應對方式:我們需要提前申請文件句柄 fd 預留,防止出現這種情況。
情況二:因為棧溢出了,導致日誌生成失敗,怎麼辦?
應對方式:為了防止棧溢出導致進程沒有空間創建調用棧執行處理函數,我們通常會使用常見的 signalstack。在一些特殊情況,我們可能還需要直接替換當前棧,所以這裡也需要在堆中預留部分空間。
情況三:整個堆的內存都耗盡了,導致日誌生成失敗,怎麼辦?
應對方式:這個時候我們無法安全地分配內存,也不敢使用 stl 或者 libc 的函數,因為它們內部實現會分配堆內存。這個時候如果繼續分配內存,會導致出現堆破壞或者二次崩潰的情況。Breakpad 做的比較徹底,重新封裝了 Linux Syscall Support,來避免直接調用 libc。
情況四:堆破壞或二次崩潰導致日誌生成失敗,怎麼辦?
應對方式:Breakpad 會從原進程 fork 出子進程去收集崩潰現場,此外涉及與 Java 相關的,一般也會用子進程去操作。這樣即使出現二次崩潰,只是這部分的信息丟失,我們的父進程後面還可以繼續獲取其他的信息。在一些特殊的情況,我們還可能需要從子進程 fork 出孫進程。
當然 Breakpad 也存在著一些問題,例如生成的 minidump 文件是二進位格式的,包含了太多不重要的信息,導致文件很容易達到幾 MB。但是 minidump 也不是毫無用處,它有一些比較高級的特性,比如使用 gdb 調試、可以看到傳入參數等。Chromium 未來計劃使用 Crashpad 全面替代 Breakpad,但目前來說還是 「too early to mobile」。
我們有時候想遵循 Android 的文本格式,並且添加更多我們認為重要的信息,這個時候就要去改造 Breakpad 的實現。比較常見的例如增加 Logcat 信息、Java 調用棧信息以及崩潰時的其他一些有用信息,在「Android 開發高手課」里我們會有更加詳細的介紹。
如果想徹底弄清楚 Native 崩潰捕獲,需要我們對虛擬機運行、彙編這些內功有一定造詣。做一個高可用的崩潰收集 SDK 真的不是那麼容易,它需要經過多年的技術積累,要考慮的細節也非常多,每一個失敗路徑或者二次崩潰場景都要有應對措施或備用方案。
選擇合適的崩潰服務
對於很多中小型公司來說,我並不建議自己去實現一套如此複雜的系統,可以選擇一些第三方的服務。目前各種平台也是百花齊放,包括騰訊的 Bugly、阿里的啄木鳥平台、網易雲捕、Google 的 Firebase 等等。
當然,在平台的選擇方面,我認為,從產品化跟社區維護來說,Bugly 在國內做的最好;從技術深度跟捕獲能力來說,阿里 UC 瀏覽器內核團隊打造的啄木鳥平台最佳。
如何客觀地衡量崩潰
對崩潰有了更多了解以後,我們怎樣才能客觀地衡量崩潰呢?
要衡量一個指標,首先要統一計算口徑。如果想評估崩潰造成的用戶影響範圍,我們會先去看UV 崩潰率。
UV 崩潰率 = 發生崩潰的 UV / 登錄 UV
只要用戶出現過一次崩潰就會被計算到,所以 UV 崩潰率的高低會跟應用的使用時長有比較大的關係,這也是微信 UV 崩潰率在業界不算低的原因(強行甩鍋)。當然這個時候,我們還可以去看應用 PV 崩潰率、啟動崩潰率、重複崩潰率這些指標,計算方法都大同小異。
這裡為什麼要單獨統計啟動崩潰率呢?因為啟動崩潰對用戶帶來的傷害最大,應用無法啟動往往通過熱修復也無法拯救。閃屏廣告、運營活動,很多應用啟動過程異常複雜,又涉及各種資源、配置下發,極其容易出現問題。微信讀書、蘑菇街、淘寶、天貓這些「重運營」的應用都有使用一種叫作「安全模式」的技術來保障客戶端的啟動流程,在監控到客戶端啟動失敗後,給用戶自救的機會。
現在回到文章開頭程序員「華山論劍」的小故事,我來揭秘他們解決崩潰率的「獨家秘笈」。
程序員 B 對所有線程、任務都封裝了一層 try catch,「消化」掉了所有 Java 崩潰。至於程序是否會出現其他異常表現,這是上帝要管的事情,反正我是實現了「千分之一」的目標。
程序員 C 認為 Native 崩潰太難解決,所以他想了一個「好方法」,就是不採集所有的 Native 崩潰,美滋滋地跟老闆彙報「萬分之一」的工作成果。
了解了美好數字產生的「秘笈」後,不知道你有何感想?其實程序員 B 和 C 都是真實的案例,而且他們的用戶體量都還不算小。技術指標過於 KPI 化,是國內比較明顯的一個現象。崩潰率只是一個數字,我們的出發點應該是讓用戶有更好的體驗。
如何客觀地衡量穩定性
到此,我們討論了崩潰是怎麼回事兒,以及怎麼客觀地衡量崩潰。那崩潰率是不是就能完全等價於應用的穩定性呢?答案是肯定不行。處理了崩潰,我們還會經常遇到 ANR(Application Not Responding,程序沒有響應)這個問題。
出現 ANR 的時候,系統還會彈出對話框打斷用戶的操作,這是用戶非常不能忍受的。這又帶來另外一個問題,我們怎麼去發現應用中的 ANR 異常呢?總結一下,通常有兩種做法。
使用 FileObserver 監聽 /data/anr/traces.txt 的變化。非常不幸的是,很多高版本的 ROM,已經沒有讀取這個文件的許可權了。這個時候你可能只能思考其他路徑,海外可以使用 Google Play 服務,而國內微信利用 Hardcoder 框架(HC 框架是一套獨立於安卓系統實現的通信框架,它讓 App 和廠商 ROM 能夠實時「對話」了,目標就是充分調度系統資源來提升 App 的運行速度和畫質,切實提高大家的手機使用體驗)向廠商獲取了更大的許可權。
監控消息隊列的運行時間。這個方案無法準確地判斷是否真正出現了 ANR 異常,也無法得到完整的 ANR 日誌。在我看來,更應該放到卡頓的性能範疇。
回想我當時在設計 Tinker 的時候,為了保證熱修復不會影響應用的啟動,Tinker 在補丁的載入流程也設計了簡單的「安全模式」,在啟動時會檢查上次應用的退出類型,如果檢查連續三次異常退出,將會自動清除補丁。所以除了常見的崩潰,還有一些會導致應用異常退出的情況。
在討論什麼是異常退出之前,我們先看看都有哪些應用退出的情形。
主動自殺。Process.killProcess()、exit() 等。
崩潰。出現了 Java 或 Native 崩潰。
系統重啟;系統出現異常、斷電、用戶主動重啟等,我們可以通過比較應用開機運行時間是否比之前記錄的值更小。
被系統殺死。被 low memory killer 殺掉、從系統的任務管理器中劃掉等。
ANR。
我們可以在應用啟動的時候設定一個標誌,在主動自殺或崩潰後更新標誌,這樣下次啟動時通過檢測這個標誌就能確認運行期間是否發生過異常退出。對應上面的五種退出場景,我們排除掉主動自殺和崩潰(崩潰會單獨的統計)這兩種場景,希望可以監控到剩下三種的異常退出,理論上這個異常捕獲機制是可以達到 100% 覆蓋的。
通過這個異常退出的檢測,可以反映如 ANR、low memory killer、系統強殺、死機、斷電等其他無法正常捕獲到的問題。當然異常率會存在一些誤報,比如用戶從系統的任務管理器中劃掉應用。對於線上的大數據來說,還是可以幫助我們發現代碼中的一些隱藏問題。
所以就得到了一個新的指標來衡量應用的穩定性,即異常率。
UV 異常率 = 發生異常退出或崩潰的 UV / 登錄 UV
前不久我們的一個應用灰度版本發現異常退出的比例增長不少,最後排查發現由於視頻播放存在一個巨大 bug,會導致可能有用戶手機卡死甚至重啟,這是傳統崩潰收集很難發現的問題。
根據應用的前後台狀態,我們可以把異常退出分為前台異常退出和後台異常退出。「被系統殺死」是後台異常退出的主要原因,當然我們會更關注前台的異常退出的情況,這會跟 ANR、OOM 等異常情況有更大的關聯。
通過異常率我們可以比較全面的評估應用的穩定性,對於線上監控還需要完善崩潰的報警機制。在微信我們可以做到 5 分鐘級別的崩潰預警,確保能在第一時間發現線上重大問題,儘快決定是通過發版還是動態熱修復解決問題。
總 結
今天,我講了 Android 的兩種崩潰,重點介紹了 Native 崩潰的捕獲流程和一些難點。做一個高可用的崩潰收集 SDK 並不容易,它背後涉及 Linux 信號處理以及內存分配、彙編等知識,當你內功修鍊得越深厚,學習這些底層知識就越得心應手。
接著,我們討論了崩潰率應該如何去計算,崩潰率的高低跟應用時長、複雜度、收集 SDK 有關。不僅僅是崩潰率,我們還學習了目前 ANR 採集的方式以及遇到的問題,最後提出了異常率這一個新的穩定性監控指標。
作為技術人員,我們不應該盲目追求崩潰率這一個數字,應該以用戶體驗為先,如果強行去掩蓋一些問題往往更加適得其反。我們不應該隨意使用 try catch 去隱藏真正的問題,要從源頭入手,了解崩潰的本質原因,保證後面的運行流程。在解決崩潰的過程,也要做到由點到面,不能只針對這個崩潰去解決,而應該要考慮這一類崩潰怎麼解決和預防。
崩潰的治理是一個長期的過程,我在極客時間「Android 開發高手課」專欄里,還會重點講一些分析應用崩潰的方法論。
訂閱福利
福利一:原價¥99。上新期間,限時優惠僅需¥68 元,12 月 8 日恢復原價。
福利二:訂閱後可邀請朋友閱讀,好友成功購買,可獲得¥16 返現,好友可獲得¥8 返現,隨時可取,上不封頂。
福利三:訂閱後,按要求完成課後互動,即有機會獲得 2019 年全球移動開發 GMTC 大會門票,價值 3600 元。
訂閱方式
※菜鳥下一代分散式體系架構的設計理念
※10道軟體測試題,檢測你是否真的懂測試
TAG:InfoQ |