扣丁書屋

貨拉拉Android穩定性治理實踐!

App Crash對于用戶來講是一種最糟糕的體驗,它會導致流程中斷、app口碑變差、app卸載、用戶流失、訂單流失等。相關數據顯示,當Android App的崩潰率超過0.4%的時候,活躍用戶有明顯下降態勢。根據統計2021年初我們的Crash率為5%,大量的研發時間用于定位和解決用戶反饋、用戶投訴,crash治理刻不容緩。通過去年的大量實踐治理,我們app的Crash率降低并且穩定在0.02%,在此做一些總結和分享,希望能為其他團隊提供經驗和啟發。

行業標準

根據《2020移動應用性能管理白皮書 | 基調聽云》

我們對Crash率的治理目標設置為了0.03%。

治理前的整體狀況

App的Crash率高達5+%,隨著產品的不斷迭代,復雜度不斷提升,給Crash率降低帶來巨大挑戰,我們必須在處理好新生的Crash的同時針對遺留的Crahs進行清理。Crash部分分為兩部分,Native部分crash占比高達72%,這部分的Crash主要來源于Flutter、第三方的地圖和第三方的SDK,難以解決。Java部分的crash很多沒有得到及時解決,大量遺留的歷史債,多數有通用的解決方案。

Crash治理方法

常見Crash的處理方式

  • 根據Crash統計平臺的堆棧,用戶日志,操作路徑定位和解決
  • 尋找共性,機型、品牌、系統版本、所在頁面、用戶操作等輔助解決問題
  • 復現場景,能夠復現通常就很容易解決,可以線下復現或者云真機復現

其他處理方式

  • 對crash率較高的模塊進行業務梳理,排查,重構等
  • 與第三方sdk溝通升級解決問題,修改SDK的使用方式

Crash治理實踐

由于內部的統計平臺建設比較晚,我們只能獲取到近期的Crash率變化,可以看到Crash率明顯收斂。

1、代碼重構

為應對需求的頻繁變化、提高研發效率,貨拉拉首頁、確認下單頁等使用Flutter和小程序實現,而這部分是用戶使用率最高的頁面,代碼量龐大而且復雜。在線上環境中產生了大量的Crash,大部分的Crash為偶發,線下環境無法復現,從堆棧情況很難定位問題的根源,修改起來有可能涉及到多端,各端都需要進行回歸校驗。經過綜合分析,我們決定梳理邏輯,讓最重要的這部分代碼回歸原生,重構上線之后Crash率明顯下降,由原來的5%下降到0.5%,穩定性大幅提高。

2、三方SDK和Native崩潰處理

這部分的Crash處理使我們從Crash率由千分位降低到了0.05%。在初期我們很容易解決了java部分的Crash,然而第三方sdk和Native的Crash居高不下,有很長一段時間我們通過提交工單,咨詢對接方等形式并沒有辦法得到解決。對于有些crash,我們升級了sdk發現crash量反而增加,又需要降級回舊版本。對于有些crash,堆棧信息卻跟這個sdk沒有關系。針對這些部分的崩潰通常用以下兩種方式解決:

  • 查看Api的調用存在問題(調用時機,調用順序,傳入參數,調用的線程或者進程等)
  • 溝通解決,(在沒有一對一開發對接的情況下,嘗試提工單咨詢),將Crash的堆棧發送給第三方SDK進行處理

我們兩組Crash量最高的為例:

2.1 第三方VMP線程監控內存情況并關閉App導致

Crash統計平臺上有大量的Native崩潰SIGSEGV(SEGV_MAPERR)、SIGABRTSIGSEGV(SEGV_ACCERR)等,堆棧通常為系統級別的so內部(libgui.so、libGLES_mali.so和libc.so等),從抓取的堆棧中我們無法得知問題根源。我們已知的信息是Vivo手機Android10,Android11崩潰率極高,為此我們病急亂投醫式進行了幾種嘗試:

  • 錯誤堆棧中有出現硬件加速相關的so,我們嘗試關閉了整個的硬件加速功能
  • 進行大量的內存泄露治理、線程治理
  • 降低手機幀率
  • 聯系手機廠商

多次嘗試無果之后,在一次的用戶日志對比中發現部分bug存在相同的日志

咨詢第三方SDK后得知VMP監控,對應用進程的/proc/pid/mem/proc/self/task/pid/mem文件監控,如果文件被操作則會退出App,退出方式可能存在問題,導致地圖sdk中捕獲native崩潰時出現二次崩潰,在將這部分bug處理之后我們的崩潰率降低到0.1%左右,native部分的Crash堆棧沒有這部分Crash參雜,堆棧信息更加明確,問題也容易定位一些。

2.2 地圖使用

地圖是貨拉拉應用中必不可少的組件,在用戶下單過程中都會用到,貫穿整個主流程。地圖部分的crash絕大部分為native,并且有些是特定品牌機型獨有的crash。我們將Crash反饋給SDK方,開發人員根據堆棧定位問題,經過多次分析導致Crash的原因一部分是sdk導致,另一部分是我們調用不合理的導致。SDK內部的原因通過升級解決,crash呈收斂趨勢。調用不合理部分主要是因為銷毀時機不正確導致,我們對各個地圖使用場景進行排查修復,在幾個版本修復之后總體Crash率降低到了0.05%。

3、OOM治理

OutOfMemoryError是指內存溢出,也是一類難以解決的Crash,通常上報的堆棧信息,用戶日志等都沒有太多的參考價值,往往內存已經在其他位置增加到內存溢出的臨界點,而上報的位置很可能不存在內存問題,而引起內存溢出主要的原因有:內存泄漏,引用大對象,內存抖動,線程使用不合理等。

3.1 內存泄漏

主要是用LeakCanary進行檢測,通常Activity的泄漏會顯得比較嚴重,因為它承載了整個頁面的控件、數據等,Activity無法回收也會導致這部分對象無法回收。

常見引起Activity泄漏的原因

  • Handler,Thread等匿名內部類隱式持有外部類導致
  • 注冊的監聽器未及時反注冊:EventBus,廣播
  • 單例對象持有Activity:貨拉拉App中有一個頁面管理會持有Activity對象,有一部分頁面添加之后沒有移除造成了大量內存泄漏的場景
  • 網絡請求,異步任務:由于早期網絡部分封裝的不好,網絡請求沒有與頁面的生命周期綁定,在弱網或者用戶關閉頁面較快的情況下會出現內存泄漏。
  • Context使用Application就可以的時候使用了Activity:例如,Toast,第三方SDK
3.2 線程治理

當線程超過系統設置的上限,也會導致OOM的發生,報錯:pthread_create (1040KB stack) failed: Out of memory。通常處理方式如下:

  • 對App內部線程池進行統一,對于隨意使用的異步任務統一改為使用線程池或者RxJava
  • 檢查App內Timer,HandlerThread類的合理使用
  • 溝通內部其他團隊的SDK中增加線程代理,統一使用App端的線程池,避免線程的隨意使用
  • 分析合理可以替換的線程或者線程池進行插樁替換處理
3.3 大對象處理

通常App中圖片都是占用內存的大戶,bitmap的管理是內存治理中非常重要的環節。對于這里的處理,我們首先是將圖片加載統一使用Glide,再將App中的原生加載圖片替換成Glide。

另外,通過MAT工具,篩選、排序大對象,對頭部大對象進行優化:

  • 原本在同一個SharePreference的數據拆分到多個,使用完畢進行回收。
  • 某個類中持有一個非常大的數據,而并不是經常用到,可以放入數據庫或者文件中。
3.4 內存抖動

其實針對這部分的優化,我們只是對檢測到的、比較嚴重的、不合理的內存增長進行了優化。通過AndroidStudio的Profiler,我們發現內存出現鋸齒狀的增長,或者GC頻率極高的情況,對著部分不合理的對象分配進行分析和解決。

這里舉兩個例子:

  • a.自定義View的onDraw方法中創建對象

  • b.設置監聽布局變化,再改變控件的大小,位置等。在改變這個view顯示狀態時又會出發監聽,這里就會不斷的執行,造成了內存的瘋狂增長,GC頻率提高,在一些測試機上十幾秒就回進行一次GC。
View.getViewTreeObserver().addOnGlobalLayoutListener(() -> {

    //改變view的大小,位置等

    int[] view1Location = new int[2];

    view1.getLocationOnScreen(view1Location);

    int[] view2Location = new int[2];

    view2.getLocationOnScreen(view2Location);

});

解決的方式主要有兩種:

  • 1.對象復用:對于onDraw中的使用場景可以將對象作為一個成員變量,其他情況考慮是否可以建立對象池
  • 2.增加條件:在達到某種條件下才允許創建對象的邏輯,比如第二個例子中,由于業務原因無法移除監聽,我們增加了一些條件,只有少量情況才會執行創建的邏輯。

4.常規Crash

這部分問題通常根據堆棧和用戶手機的相關情況很容易定位到問題,不過不能只是修改出問題的那行代碼,更不可以隨意的進行try catch,需要找到問題的本質,可能這個問題的還會導致其他的一系列問題。比方說:

點擊事件的某一行出現了空指針,發現是數據為空導致,數據是從前一個頁面的網絡接口傳遞過來,而接口數據為空是因為傳入的設備id為空。那么此時我們只在點擊事件這里做判空肯定是不夠的,需要將設備id為空的原因找到,否則這整個鏈路都會出現crash或者業務異常。

4.1 空指針NullPointerException

空指針雖然上報的量不大,但卻是我們統計到出現次數最多的bug。通常是對象本身沒做初始化或者對象被回收的情況下使用該對象導致:

  • 代碼邏輯分支引起,沒走到初始化的位置,卻走了相應的調用邏輯。
  • 服務端接口返回數據異常,數據庫查詢數據異常
  • 一些postDelay或者異步回調,回調時也能被回收,可能導致空指針
  • 靜態變量被回收

通常解決空指針不能直接在代碼基礎上增加判空處理,應當尋找真正導致對象為空的原因進行規避,常見的解決方式有:

  • 做好判空或者使用kotlin語言
  • 使用注解@NonNull@Nullable
  • 存在異步任務應當適時停止異步任務或者移除監聽回調(例如,頁面銷毀停止網絡請求,移除頁面內的postDelay任務)
  • 靜態變量做好管理,可以本地持久化
4.2 索引越界IndexOutofBoundsException

常見原因:

  • 字符串拼接,截取索引不正確,SpannableString 索引設置異常
  • ListView,RecyclerView或者ViewPager中數據變化沒有通知Adapter數據發生變化
  • 多線程情況下使用不安全的集合

解決方式:

  • 字符串,SpannableString有關索引操作,應當先判斷好字符串長度
  • 多線程場景下應當用安全的形式訪問集合或者使用線程安全的集合
4.3 系統原因產生的bug

對于這類bug,觸發的原因很難查到,我們需要找到堆棧中的關鍵字、系統版本、機型進行分析,解決方案通常是規避調用或者hook的方式解決。例如下面的bug,關鍵字“autofill”,版本Android10,對應于Android10的自動填充功能。

錯誤名稱和堆棧信息

android.os.RemoteException Remote stack trace: at com.android.internal.util.Preconditions.checkCollectionElementsNotNull

簡要分析

上面的錯誤日志涉及到跨進程,分為三段來看會清晰一些。

App進程中ActivityThread的mH接收到Message進行處理handleRequestAssistContextExtras,再調用ActivityTaskManagerService的aidl方法reportAssistContextExtras

進入到ActivityTaskManagerService.reportAssistContextExtras新建FillRequest的時構造中調用Preconditions.checkCollectionElementsNotNull拋出異常,異常信息發送回我們App進程。Preconditions類異常拋出的位置

調用驗證

通過對源碼查看,mH中handleRequestAssistContextExtras的調用只有一處,發出這個message的也只有一處。通過hook,確定用戶在使用自動填充功能時,會調用這里。

解決方案
  • 設置關閉EditText控件的自動填充功能
  • Edittext的自動填充引起的Bug_乂星人的博客-CSDN博客_android edittext 自動填充(https://blog.csdn.net/li0978/article/details/105014121)
4.4 其他Crash較多的bug
android.app.RemoteServiceException Context.startForegroundService() did not then call Service.startForeground()

Android8.0啟動服務適配問題,App內部和SDK中都有閃退出現,排查App中所有的Service進行適配處理。

android.content.ActivityNotFoundException No Activity found to handle Intent

這個是我們App中js回調安卓跳轉頁面的方法,由于頁面未找到導致的閃退。另外我們發現業務中由

JavaScriptInterface回調安卓端再進行頁面的一些操作和發出的Eventbus事件引發了許多莫名其妙的小bug,為此我們將JavaScriptInterface中的回調盡可能都拋回主線程中執行,減少了因子線程操作UI和多線程執行不安全的問題。

com.google.gson.stream.MalformedJsonException

由于后端數據或者本地數據庫問題導致數據的解析異常,統一使用了Json解析工具,對異常進行捕獲處理

Crash預防和輔助工具

灰度發布

貨拉拉的打包發布系統支持持續集成、灰度發布、構建統計、配置下發、強升普升弱升、內測分發等功能,灰度發布支持設置設備ID、數量、百分比、版本號、品牌型號、網絡制式、時間、系統版本、業務城市、定位城市等非常靈活。

灰度系統的主要作用:

  • 盡早的獲得用戶的反饋,完善產品功能
  • 暴露存在的Crash問題、產品設計問題,及時修改,縮小影響面

貨拉拉目前的版本發布會嚴格按照灰度策略進行逐步放量,在業務需要時會增加對灰度的范圍設置例如:城市、品牌型號、版本號等。對于風險較大的需求,我們可能會專門安排一個灰度版本進行發布,最大可能的縮小影響范圍。

應用配置系統

通過貨拉拉的打包發布系統進行配置下發,同時這個配置支持灰度發布,可以像App發布灰度一樣下發,對于一些業務可以通過配置下發,控制功能的參數,功能的開關,進行業務降級,修改AB實驗等。

例如,我們在線上環境中有些H5頁面使用了離線包,其他部門對網關進行切換,導致離線包出現跨域問題,無法訪問頁面。我們通過配置下發及時關閉離線包功能,業務降級為在線網頁。

代碼質量相關措施

建立Code Review 制度

  • 保證代碼的可讀性和風格一致性:便于其他成員對代碼的理解,增強維護性
  • 發現錯誤:代碼中出現錯誤很難避免,而這些錯誤在另外一個人眼中可能很容易被發現
  • 發現設計問題:檢查代碼中的設計不合理,避免后續維護、迭代困難
  • 健壯性檢查:檢查代碼的健壯性,是否有潛在的安全問題,性能風險等
  • 互相學習:閱讀他人的代碼也可以從中吸取經驗

增加模塊Owner

  • 模塊內代碼主要由Owner開發實現,Owner會比較熟悉,并且能夠保證代碼的一致性。
  • Owner可以更好的review其他開發者的代碼

業務串講、思維導圖

  • 讓團隊內部成員熟悉業務模塊和流程
  • 利于團隊內溝通、學習

代碼掃描:不符合團隊規范的代碼無法提交

  • 保證代碼的規范
  • 保證代碼的一致性
熱修復

當App已經大面積發布,而Crash會導致大量用戶無法使用、閃退、主流程無法執行等,我們會考慮熱修復。如果沒有熱修復技術,那么我們只能重新發布App版本,甚至需要進行強制升級,會造成極差的用戶體驗。

日志系統

貨拉拉的日志系統分為兩類:

  • 實時日志:用戶在使用App時產生日志通過上傳策略上報到我們平臺,支持日志等級篩選、TAG篩選和搜索等常用功能。
  • 離線日志:根據用戶ID等進行推送用戶進行上報,或者用戶主動進行上報。

通過日志系統,我們可以根據日志等級、日志時間等得知當前用戶的使用場景和操作路徑,便于場景復現,是解決Crash的利器之一。

總結

過去的一年我們對App的重點模塊進行重構、第三方SDK治理、多次的專項治理,Crash率由5+%降低到了0.02%,穩定性的提升使我們用戶反饋問題和用戶投訴明顯減少,安卓app端相關的投訴反饋由去年4月的12個降低到今年4月的2個,讓我們更多地留存用戶,保證用戶順利完成訂單。我們總結出幾點治理原則:

  • 舉一反三,由點及面,由一個問題引入或者歸類出一類問題,進行專項的治理。
  • 尋找本質,找到問題的根因而不是一味地try catch,判斷繞過。
  • 及時解決,Crash問題盡可能在開發和測試環節已經出現應當及時解決,而不是灰度和全量之后再處理。
  • 著重預防,將Crash消滅在萌芽階段,進行代碼review、模塊劃分、技術串講等措施。
  • 控制準入,嚴格控制模塊、sdk、新技術的引入,減少引入風險

Crash治理并非一勞永逸,需要長期治理,建立長效機制。另外一方面,對于新技術和跨平臺技術,在業務允許的情況下我們應當需要勇敢的去嘗試,利用現有的預防措施做好防范工作,而不會是直接摒棄這些技術的應用。

參考鏈接:
  • 友盟+U-APM 移動應用性能體驗報告:Android崩潰率達0.32%,OPPO 、華為、VIVO 崩潰表現良好
  • 移動應用性能報告 歸檔 | 基調聽云
  • 移動端性能標準值及定義整理 - 掘金

https://mp.weixin.qq.com/s/hvYHEZ_BYwMQUggiNwXnOw

最多閱讀

簡化Android的UI開發 3年以前  |  515815次閱讀
Android 深色模式適配原理分析 2年以前  |  27409次閱讀
Android 樣式系統 | 主題背景覆蓋 2年以前  |  8700次閱讀
Android Studio 生成so文件 及調用 2年以前  |  6686次閱讀
30分鐘搭建一個android的私有Maven倉庫 4年以前  |  5469次閱讀
Android設計與開發工作流 3年以前  |  5179次閱讀
移動端常見崩潰指標 2年以前  |  5014次閱讀
Google Enjarify:可代替dex2jar的dex反編譯 4年以前  |  4982次閱讀
Android內存異常機制(用戶空間)_NE 2年以前  |  4692次閱讀
Android-模塊化-面向接口編程 2年以前  |  4622次閱讀
Android多渠道打包工具:apptools 4年以前  |  4539次閱讀
Google Java編程風格規范(中文版) 4年以前  |  4396次閱讀
Android死鎖初探 2年以前  |  4364次閱讀
Android UI基本技術點 4年以前  |  4177次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>