做爲技術人員,咱們不該該盲目追求崩潰率這一個數字,應該以用戶體驗爲先,若是強行去掩蓋一些問題每每更加拔苗助長。咱們不該該隨意使用 try catch 去隱藏真正的問題,要從源頭入手,瞭解崩潰的本質緣由,保證後面的運行流程。在解決崩潰的過程,也要作到由點到面,不能只針對這個崩潰去解決,而應該要考慮這一類崩潰怎麼解決和預防。(附github項目demo參考項目)
java
1、Android 的兩種崩潰android
咱們都知道,Android 崩潰分爲 Java 崩潰和 Native崩潰。git
簡單來講,Java 崩潰就是在 Java 代碼中,出現了未捕獲異常,致使程序異常退出。那 Native 崩潰通常都是由於Native代碼中訪問非法地址或者是地址對齊出現問題,再或者是發生程序主動abort,這些都會產生對應的signal信號,致使程序異常退出。因此,「崩潰」就是程序出現異常,而一個產品的崩潰率,跟咱們如何捕獲、處理這些異常有比較大的關係。Java 崩潰的捕獲比較簡單,可是不少同窗對於如何捕獲Native 崩潰仍是隻知其一;不知其二,下面我就重點介紹 Native 崩潰的捕獲流程和難點。github
一、Native崩潰捕獲流程(Native 崩潰機制)瀏覽器
(1)編譯端。編譯 C/C++ 代碼時,須要將帶符號信息的文件保留下來。安全
(2)客戶端。捕獲到崩潰時候,將收集到儘量多的有用信息寫入日誌文件,而後選擇合適的時機上傳到服務器。服務器
(3)服務端,讀取客戶端上報的日誌文件,尋找適合的符號文件,生成可讀的C/C++調用棧。微信
二、Native崩潰捕獲的難點網絡
Chromium 的Breakpad是目前 Native崩潰捕獲中最成熟的方案,因此核心就是保證客戶端在各類極端狀況下依然能夠生成崩潰日誌,由於崩潰時,程序處於不安全狀態,極可能發生二次崩潰,那麼生成崩潰日誌主要一些棘手狀況:框架
狀況一:文件句柄泄露,致使建立文件日誌失敗怎麼辦?應對方式:咱們須要提早申請文件句柄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 調用棧信息以及崩潰時的其餘一些有用信息,在下一節咱們會有更加詳細的介紹。 若是想完全弄清楚 Native 崩潰捕獲,須要咱們對虛擬機運行、彙編這些內功有必定造詣。作一個高可用的崩潰收集 SDK 真的不是那麼容易,它須要通過多年的技術積累,要考慮的細節也很是多,每個失敗路徑或者二次崩潰場景都要有應對措施或備用方案。
三、選擇合適的崩潰服務
對於不少中小型公司來講,我並不建議本身去實現一套如此複雜的系統,能夠選擇一些第三方的服務。目前各類平臺也是百花齊放,包括騰訊的Bugly、阿里的啄木鳥平臺、網易雲捕、Google 的 Firebase 等等。
2、發現應用中的 ANR 異常(Application Not Responding,程序無響應)問題
一般兩種方法去發現應用中ANR
一、使用 FileObserver 監聽 /data/anr/traces.txt的變化。很是不幸的是,不少高版本的 ROM,已經沒有讀取這個文件的權限了。這個時候你可能只能思考其餘路徑,海外可使用 Google Play 服務,而國內微信利用Hardcoder框架(HC 框架是一套獨立於安卓系統實現的通訊框架,它讓 App 和廠商 ROM 可以實時「對話」了,目標就是充分調度系統資源來提高 App 的運行速度和畫質,切實提升你們的手機使用體驗)向廠商獲取了更大的權限。
二、 監控消息隊列的運行時間。這個方案沒法準確地判斷是否真正出現了 ANR 異常,也沒法獲得完整的 ANR 日誌。在我看來,更應該放到卡頓的性能範疇。
在討論什麼是異常退出以前,咱們先看看都有哪些應用退出的情形:
主動自殺、
Process.killProcess()或者exit() 等
崩潰。出現了 Java 或 Native 崩潰。
系統重啓;系統出現異常、斷電、用戶主動重啓等,咱們能夠經過比較應用開機運行時間是否比以前記錄的值更小。
被系統殺死。被 low memory killer 殺掉、從系統的任務管理器中劃掉等。
ANR。
咱們能夠在應用啓動的時候設定一個標誌,在主動自殺或崩潰後更新標誌,這樣下次啓動時經過檢測這個標誌就能確認運行期間是否發生過異常退出,對應上面五種情形除了主動自殺和崩潰(崩潰會單獨統計)但願監控剩下三種異常退出,理論上對於異常捕獲機制可百分之百覆蓋。
3、採集崩潰來源
一、從崩潰的基本信息。
好比進程名或線程名:崩潰的進程是前臺進程仍是後臺進程,崩潰是否是發生在 UI 線程。
崩潰堆棧和類型:崩潰是屬於 Java 崩潰、Native 崩潰,仍是 ANR,對於不一樣類型的崩潰咱們關注的點也不太同樣,特別須要看崩潰堆棧的棧頂,看具體崩潰在系統的代碼,仍是咱們本身的代碼裏面。
二、系統信息。
Logcat。這裏包括應用、系統的運行日誌。因爲系統權限問題,獲取到的 Logcat 可能只包含與當前 App 相關的。其中系統的 event logcat 會記錄 App 運行的一些基本狀況,記錄在文件/system/etc/event-log-tags 中。
機型、系統、廠商、CPU、ABI、Linux 版本等。咱們會採集多達幾十個維度,這對後面講到尋找共性問題會頗有幫助。
設備狀態:是否 root、是不是模擬器。一些問題是由 Xposed 或多開軟件形成,對這部分問題咱們要區別對待。
三、內存信息:OOM、ANR、虛擬內存耗盡等,不少崩潰都跟內存有直接關係。若是咱們把用戶的手機內存分爲「2GB 如下」和「2GB 以上「兩個桶,會發現「2GB 如下」用戶的崩潰率是「2GB 以上」用戶的幾倍。
系統剩餘內存:關於系統內存狀態,能夠直接讀取文件 /proc/meminfo,當系統可用內存很小(低於 MemTotal 的 10%)時,OOM、大量 GC、系統頻繁自殺拉起等問題都很是容易出現。
應用使用內存:包括 Java 內存、RSS(Resident Set Size)、PSS(Proportional Set Size),咱們能夠得出應用自己內存的佔用大小和分佈。PSS 和 RSS 經過 /proc/self/smap 計算,能夠進一步獲得例如 apk、dex、so 等更加詳細的分類統計。
虛擬內存:虛擬內存能夠經過 /proc/self/status 獲得,經過 /proc/self/maps 文件能夠獲得具體的分佈狀況。有時候咱們通常不過重視虛擬內存,可是不少相似 OOM、tgkill 等問題都是虛擬內存不足致使的。
四、資源信息:有的時候咱們會發現應用堆內存和設備內存都很是充足,仍是會出現內存分配失敗的狀況,這跟資源泄漏可能有比較大的關係。
文件句柄fd:文件句柄的限制能夠經過 /proc/self/limits得到,通常單個進程容許打開的最大文件句柄個數爲 1024。可是若是文件句柄超過 800 個就比較危險,須要將全部的 fd 以及對應的文件名輸出到日誌中,進一步排查是否出現了有文件或者線程的泄漏。
線程數:當前線程數大小能夠經過上面的 status 文件獲得,一個線程可能就佔 2MB 的虛擬內存,過多的線程會對虛擬內存和文件句柄帶來壓力。根據個人經驗來講,若是線程數超過 400 個就比較危險。須要將全部的線程 id 以及對應的線程名輸出到日誌中,進一步排查是否出現了線程相關的問題。
JNI:使用 JNI 時,若是不注意很容易出現引用失效、引用爆表等一些崩潰,咱們能夠經過 DumpReferenceTables 統計JNI的引用表,進一步分析是否出現了JNI泄露等問題。
五、應用信息:除了系統,其實咱們的應用更懂本身,能夠留下不少相關的信息。
崩潰場景:崩潰發生在哪一個 Activity 或 Fragment,發生在哪一個業務中。
關鍵操做路徑:不一樣於開發過程詳細的打點日誌,咱們能夠記錄關鍵的用戶操做路徑,這對咱們復現崩潰會有比較大的幫助。
其餘自定義信息:不一樣的應用關心的重點可能不太同樣,好比網易雲音樂會關注當前播放的音樂,QQ 瀏覽器會關注當前打開的網址或視頻。此外例如運行時間、是否加載了補丁、是不是全新安裝或升級等信息也很是重要。
除了上面這些通用的信息外,針對特定的一些崩潰,咱們可能還須要獲取相似磁盤空間、電量、網絡使用等特定信息。因此說一個好的崩潰獲取工具,會根據場景爲咱們採集足夠多的信息,讓咱們有更多的線索去分析和定位問題,固然數據的採集須要注意用戶隱私,作到足夠強度的加密和脫敏。
4、崩潰分析
第一步:肯定重點。
(1)確認嚴重程度:解決崩潰也要看性價比,咱們優先解決 Top 崩潰或者對業務有重大影響,例如啓動、支付過程的崩潰。
(2)崩潰基本信息:肯定崩潰的類型以及異常描述,對崩潰有大體判斷,通常來講大部分崩潰通過這一步已經能夠獲得結論。好比JAVA崩潰,類型比較明顯,像NullPointerException等。或者Native崩潰,須要觀察signal、code、fault addr等內容,以及崩潰時 Java 的堆棧。關於各 signal 含義的介紹,比較常見的是有 SIGSEGV 和 SIGABRT,前者通常是因爲空指針、非法指針形成,後者主要由於 ANR 和調用abort()退出所致使。ANR個人經驗是先看看主線程 堆棧,是不是由於鎖等待致使,接着看ANR日誌中iowait、CPU、GC、system server 等信息,進一步肯定是I/O問題亦或是CPU競爭問題,仍是大量的GC致使卡死。
(3)Logcat:Logcat 通常會存在一些有價值的線索,日誌級別是 Warning、Error 的須要特別注意。從 Logcat 中咱們能夠看到當時系統的一些行爲跟手機的狀態,例如出現 ANR 時,會有「am_anr」;App 被殺時,會有「am_kill」。不一樣的系統、廠商輸出的日誌有所差異,當從一條崩潰日誌中沒法看出問題的緣由,或者得不到有用信息時,不要放棄,建議查看相同崩潰點下的更多崩潰日誌。
(4)各個資源狀況:結合崩潰的基本信息,咱們接着看看是否是跟 「內存信息」 有關,是否是跟「資源信息」有關。好比是物理內存不足、虛擬內存不足,仍是文件句柄 fd 泄漏了。
第二步:尋找共性
若是使用了上面的方法仍是不能有效定位問題,咱們能夠嘗試查找這類崩潰有沒有什麼共性。找到了共性,也就能夠進一步找到差別,離解決問題也就更進一步。 機型、系統、ROM、廠商、ABI,這些採集到的系統信息均可以做爲維度聚合,共性問題例如是否是由於安裝了 Xposed,是否是隻出如今 x86 的手機,是否是隻有三星這款機型,是否是隻在 Android 5.0 的系統上。應用信息也能夠做爲維度來聚合,好比正在打開的連接、正在播放的視頻、國家、地區等。 找到了共性,能夠對你下一步復現問題有更明確的指引。
第三步:嘗試復現
若是咱們已經大概知道了崩潰的緣由,爲了進一步確認更多信息,就須要嘗試復現崩潰。若是咱們對崩潰徹底沒有頭緒,也但願經過用戶操做路徑來嘗試重現,而後再去分析崩潰緣由。 「只要能本地復現,我就能解」,相信這是不少開發跟測試說過的話。有這樣的底氣主要是由於在穩定的復現路徑上面,咱們能夠採用增長日誌或使用 Debugger、GDB 等各類各樣的手段或工具作進一步分析。 回想當時在開發 Tinker 的時候,咱們遇到了各類各樣的奇葩問題。好比某個廠商改了底層實現、新的 Android 系統實現有所更改,都須要去 Google、翻源碼,有時候還須要去摳廠商的 ROM 或手動刷 ROM。這個痛苦的經歷告訴我,不少疑難問題須要咱們耐得住寂寞,反覆猜想、反覆發灰度、反覆驗證。
疑難問題:系統崩潰 系統崩潰經常令咱們感到很是無助,它多是某個 Android 版本的 bug,也多是某個廠商修改 ROM 致使。這種狀況下的崩潰堆棧可能徹底沒有咱們本身的代碼,很難直接定位問題。針對這種疑難問題,我來談談個人解決思路。
1. 查找可能的緣由。經過上面的共性歸類,咱們先看看是某個系統版本的問題,仍是某個廠商特定 ROM 的問題。雖然崩潰日誌可能沒有咱們本身的代碼,但經過操做路徑和日誌,咱們能夠找到一些懷疑的點。
2. 嘗試規避。查看可疑的代碼調用,是否使用了不恰當的 API,是否能夠更換其餘的實現方式規避。
3. Hook 解決。這裏分爲 Java Hook 和 Native Hook。以我最近解決的一個系統崩潰爲例,咱們發現線上出現一個 Toast 相關的系統崩潰,它只出如今 Android 7.0 的系統中,看起來是在 Toast 顯示的時候窗口的 token 已經無效了。這有可能出如今 Toast 須要顯示時,窗口已經銷燬了。 android.view.WindowManager$BadTokenException: at android.view.ViewRootImpl.setView(ViewRootImpl.java) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java4) at android.widget.Toast$TN.handleShow(Toast.java)
爲何 Android 8.0 的系統不會有這個問題?在查看 Android 8.0 的源碼後咱們發現有如下修改: try { mWM.addView(mView, mParams); trySendAccessibilityEvent(); } catch (WindowManager.BadTokenException e) { /* ignore */ }
考慮再三,咱們決定參考 Android 8.0 的作法,直接 catch 住這個異常。這裏的關鍵在於尋找 Hook 點,這個案例算是相對比較簡單的。Toast 裏面有一個變量叫 mTN,它的類型爲 handler,咱們只須要代理它就能夠實現捕獲。 若是你作到了我上面說的這些,95% 以上的崩潰都能解決或者規避,大部分的系統崩潰也是如此。固然總有一些疑難問題須要依賴到用戶的真實環境,咱們但願具有相似動態跟蹤和調試的能力。專欄後面還會講到 xlog 日誌、遠程診斷、動態分析等高級手段,能夠幫助咱們進一步調試線上疑難問題,敬請期待。 崩潰攻防是一個長期的過程,咱們但願儘量地提早預防崩潰的發生,將它消滅在萌芽階段。這可能涉及咱們應用的整個流程,包括人員的培訓、編譯檢查、靜態掃描工做,還有規範的測試、灰度、發佈流程等。 而崩潰優化也不是孤立的,它跟咱們後面講到的內存、卡頓、I/O 等內容都有關。可能等你學完整個課程後,再回頭來看會有不一樣的理解。