美團外賣Android Crash治理之路

面試中經常問到的是Android的性能優化以及Crash處理。 今天咱們來學習一下啊美團App的Crash處理。更多參考《Android性能優化:手把手帶你全面實現內存優化html

原爲地址: https://blog.csdn.net/MeituanTech/article/details/80701773java

Crash率是衡量一個App好壞的重要指標之一,若是你忽略了它的存在,它就會愈演愈烈,最後形成大量用戶的流失,進而給公司帶來沒法估量的損失。本文講述美團外賣Android客戶端團隊在將App的Crash率從千分之三作到萬分之二過程當中所作的大量實踐工做,拋磚引玉,但願可以爲其餘團隊提供一些經驗和啓發。android

面臨的挑戰和成果

面對用戶使用頻率高,外賣業務增加快,Android碎片化嚴重這些問題,美團外賣Android App如何持續的下降Crash率,是一項極具挑戰的事情。經過團隊的全力全策,美團外賣Android App的平均Crash率從千分之三降到了萬分之二,最優值萬一左右(Crash率統計方式:Crash次數/DAU)。git

美團外賣自2013年建立以來,業務就以指數級的速度發展。美團外賣承載的業務,從單一的餐飲業務,發展到餐飲、超市、生鮮、果蔬、藥品、鮮花、蛋糕、跑腿等十多個大品類業務。目前美團外賣日完成訂單量已突破2000萬,成爲美團點評最重要的業務之一。美團外賣客戶端所承載的業務模塊愈來愈多,產品複雜度愈來愈高,團隊開發人員日益增長,這些都給App下降Crash率帶來了巨大的挑戰。程序員

Crash的治理實踐

對於Crash的治理,咱們儘可能遵照如下三點原則:github

  • 由點到面。一個Crash發生了,咱們不能只針對這個Crash的去解決,而要去考慮這一類Crash怎麼去解決和預防。只有這樣才能使得這一類Crash真正被解決。
  • 異常不能隨便吃掉。隨意的使用try-catch,只會增長業務的分支和隱蔽真正的問題,要了解Crash的本質緣由,根據本質緣由去解決。catch的分支,更要根據業務場景去兜底,保證後續的流程正常。
  • 預防勝於治理。當Crash發生的時候,損失已經形成了,咱們再怎麼治理也只是減小損失。儘量的提早預防Crash的發生,能夠將Crash消滅在萌芽階段。

常規的Crash治理

常規Crash發生的緣由主要是因爲開發人員編寫代碼不當心致使的。解決這類Crash須要由點到面,根據Crash引起的緣由和業務自己,統一集中解決。常見的Crash類型包括:空節點、角標越界、類型轉換異常、實體對象沒有序列化、數字轉換異常、Activity或Service找不到等。這類Crash是App中最爲常見的Crash,也是最容易反覆出現的。在獲取Crash堆棧信息後,解決這類Crash通常比較簡單,更多考慮的應該是如何避免。下面介紹兩個咱們治理的量比較大的Crash。面試

NullPointerException

NullPointerException是咱們遇到最頻繁的,形成這種Crash通常有兩種狀況:正則表達式

  • 對象自己沒有進行初始化就進行操做。
  • 對象已經初始化過,可是被回收或者手動置爲null,而後對其進行操做。

針對第一種狀況致使的緣由有不少,多是開發人員的失誤、API返回數據解析異常、進程被殺死後靜態變量沒初始化致使,咱們能夠作的有:算法

  • 對可能爲空的對象作判空處理。
  • 養成使用@NonNull和@Nullable註解的習慣。
  • 儘可能不使用靜態變量,萬不得已使用SharedPreferences來存儲。
  • 考慮使用Kotlin語言。

針對第二種狀況大部分是因爲Activity/Fragment銷燬或被移除後,在Message、Runnable、網絡等回調中執行了一些代碼致使的,咱們能夠作的有:編程

  • Message、Runnable回調時,判斷Activity/Fragment是否銷燬或被移除;加try-catch保護;Activity/Fragment銷燬時移除全部已發送的Runnable。
  • 封裝LifecycleMessage/Runnable基礎組件,並自定義Lint檢查,提示使用封裝好的基礎組件。
  • 在BaseActivity、BaseFragment的onDestory()裏把當前Activity所發的全部請求取消掉。
IndexOutOfBoundsException

這類Crash常見於對ListView的操做和多線程下對容器的操做。

針對ListView中形成的IndexOutOfBoundsException,常常是由於外部也持有了Adapter裏數據的引用(如在Adapter的構造函數裏直接賦值),這時若是外部引用對數據更改了,但沒有及時調用notifyDataSetChanged(),則有可能形成Crash,對此咱們封裝了一個BaseAdapter,數據統一由Adapter本身維護通知, 同時也極大的避免了The content of the adapter has changed but ListView did not receive a notification,這兩類Crash目前獲得了統一的解決。

另外,不少容器是線程不安全的,因此若是在多線程下對其操做就容易引起IndexOutOfBoundsException。經常使用的如JDK裏的ArrayList和Android裏的SparseArray、ArrayMap,同時也要注意有一些類的內部實現也是用的線程不安全的容器,如Bundle裏用的就是ArrayMap。

系統級Crash治理

衆所周知,Android的機型衆多,碎片化嚴重,各個硬件廠商可能會定製本身的ROM,更改系統方法,致使特定機型的崩潰。發現這類Crash,主要靠雲測平臺配合自動化測試,以及線上監控,這種狀況下的Crash堆棧信息很難直接定位問題。下面是常見的解決思路:

  1. 嘗試找到形成Crash的可疑代碼,看是否有特異的API或者調用方式不當致使的,嘗試修改代碼邏輯來進行規避。
  2. 經過Hook來解決,Hook分爲Java Hook和Native Hook。Java Hook主要靠反射或者動態代理來更改相應API的行爲,須要嘗試找到能夠Hook的點,通常Hook的點多爲靜態變量,同時須要注意Android不一樣版本的API,類名、方法名和成員變量名均可能不同,因此要作好兼容工做;Native Hook原理上是用更改後方法把舊方法在內存地址上進行替換,須要考慮到Dalvik和ART的差別;相對來講Native Hook的兼容性更差一點,因此用Native Hook的時候須要配合降級策略。
  3. 若是經過前兩種方式都沒法解決的話,咱們只能嘗試反編譯ROM,尋找解決的辦法。

咱們舉一個定製系統ROM致使Crash的例子,根據Crash平臺統計數據發現該Crash只發生在vivo V3Max這類機型上,Crash堆棧以下:

java.lang.RuntimeException: An error occured while executing doInBackground()
  at android.os.AsyncTask$3.done(AsyncTask.java:304)
  at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
  at java.util.concurrent.FutureTask.setException(FutureTask.java:222)
  at java.util.concurrent.FutureTask.run(FutureTask.java:242)
  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
  at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'int java.util.List.size()' on a null object reference
  at android.widget.AbsListView$UpdateBottomFlagTask.isSuperFloatViewServiceRunning(AbsListView.java:7689)
  at android.widget.AbsListView$UpdateBottomFlagTask.doInBackground(AbsListView.java:7665)
  at android.os.AsyncTask$2.call(AsyncTask.java:292)
  at java.util.concurrent.FutureTask.run(FutureTask.java:237)
  ... 4 more
複製代碼

咱們發現原生系統上對應系統版本的AbsListView裏並無UpdateBottomFlagTask類,所以能夠判定是vivo該版本定製的ROM修改了系統的實現。咱們在定位這個Crash的可疑點無果後決定經過Hook的方式解決,經過源碼發現AsyncTask$SerialExecutor是靜態變量,是一個很好的Hook的點,經過反射添加try-catch解決。由於修改的是final對象因此須要先反射修改accessFlags,須要注意ART和Dalvik下對應的Class不一樣,代碼以下:

public static void setFinalStatic(Field field, Object newValue) throws Exception {
        field.setAccessible(true);
        Field artField = Field.class.getDeclaredField("artField");
        artField.setAccessible(true);
        Object artFieldValue = artField.get(field);
        Field accessFlagsFiled = artFieldValue.getClass().getDeclaredField("accessFlags");
        accessFlagsFiled.setAccessible(true);
        accessFlagsFiled.setInt(artFieldValue, field.getModifiers() & ~Modifier.FINAL);
        field.set(null, newValue);
    }

複製代碼
private void initVivoV3MaxCrashHander() {
    if (!isVivoV3()) {
        return;
    }
    try {
        setFinalStatic(AsyncTask.class.getDeclaredField("SERIAL_EXECUTOR"), new SafeSerialExecutor());
        Field defaultfield = AsyncTask.class.getDeclaredField("sDefaultExecutor");
        defaultfield.setAccessible(true);
        defaultfield.set(null, AsyncTask.SERIAL_EXECUTOR);
    } catch (Exception e) {
        L.e(e);
    }
}
複製代碼

美團外賣App用上述方法解決了對應的Crash,可是美團App裏的外賣頻道由於平臺的限制沒法經過這種方式,因而咱們嘗試反編譯ROM。  Android ROM編譯時會將framework、app、bin等目錄打入system.img中,system.img是Android系統中用來存放系統文件的鏡像 (image),文件格式通常爲yaffs2或ext。但Android 5.0開始支持dm-verity後,system.img再也不提供,而是提供了三個文件system.new.dat,system.patch.dat,system.transfer.list,所以咱們首先須要經過上述的三個文件獲得system.img。但咱們將vivo ROM解壓後發現廠商將system.new.dat進行了分片,以下圖所示:

image

通過對system.transfer.list中的信息和system.new.dat 1 2 3 … 文件大小對比研究,發現一些共同點,system.transfer.list中的每個block數*4KB 與對應的分片文件的大小大體相同,故大膽猜想,vivo ROM對system.patch.dat分片也只是單純的按block前後順序進行了分片處理。因此咱們只須要在轉化img前將這些分片文件合成一個system.patch.dat文件就能夠了。最後根據system.img的文件系統格式進行解包,拿到framework目錄,其中有framework.jar和boot.oat等文件,由於Android4.4以後引入了ART虛擬機,會預先把system/framework中的一些jar包轉換爲oat格式,因此咱們還須要將對應的oat文件經過ota2dex將其解包得到dex文件,以後經過dex2jarjd-gui查看源碼。

OOM

OOM是OutOfMemoryError的簡稱,在常見的Crash疑難排行榜上,OOM絕對能夠名列前茅而且經久不衰。由於它發生時的Crash堆棧信息每每不是致使問題的根本緣由,而只是壓死駱駝的最後一根稻草。  致使OOM的緣由大部分以下:

  • 內存泄漏,大量無用對象沒有被及時回收致使後續申請內存失敗。
  • 大內存對象過多,最多見的大對象就是Bitmap,幾個大圖同時加載很容易觸發OOM。

內存泄漏  內存泄漏指系統未能及時釋放已經再也不使用的內存對象,通常是由錯誤的程序代碼邏輯引發的。在Android平臺上,最多見也是最嚴重的內存泄漏就是Activity對象泄漏。Activity承載了App的整個界面功能,Activity的泄漏同時也意味着它持有的大量資源對象都沒法被回收,極其容易形成OOM。  常見的可能會形成Activity泄漏的緣由有:

  • 匿名內部類實現Handler處理消息,可能致使隱式持有的Activity對象沒法回收。
  • Activity和Context對象被混淆和濫用,在許多隻須要Application Context而不須要使用Activity對象的地方使用了Activity對象,好比註冊各種Receiver、計算屏幕密度等等。
  • View對象處理不當,使用Activity的LayoutInflater建立的View自身持有的Context對象其實就是Activity,這點常常被忽略,在本身實現View重用等場景下也會致使Activity泄漏。

對於Activity泄漏,目前已經有了一個很是好用的檢測工具:LeakCanary,它能夠自動檢測到全部Activity的泄漏狀況,而且在發生泄漏時給出十分友好的界面提示,同時爲了防止開發人員的疏漏,咱們也會將其上報到服務器,統一檢查解決。另外咱們能夠在debug下使用StrictMode來檢查Activity的泄露、Closeable對象沒有被關閉等問題。

大對象  在Android平臺上,咱們分析任一應用的內存信息,幾乎均可以得出一樣的結論:佔用內存最多的對象大都是Bitmap對象。隨着手機屏幕尺寸愈來愈大,屏幕分辨率也愈來愈高,1080p和更高的2k屏已經佔了大半份額,爲了達到更好的視覺效果,咱們每每須要使用大量高清圖片,同時也爲OOM埋下了禍根。  對於圖片內存優化,咱們有幾個經常使用的思路:

  • 儘可能使用成熟的圖片庫,好比Glide,圖片庫會提供不少通用方面的保障,減小沒必要要的人爲失誤。
  • 根據實際須要,也就是View尺寸來加載圖片,能夠在分辨率較低的機型上儘量少地佔用內存。除了經常使用的BitmapFactory.Options#inSampleSize和Glide提供的BitmapRequestBuilder#override以外,咱們的圖片CDN服務器也支持圖片的實時縮放,能夠在服務端進行圖片縮放處理,從而減輕客戶端的內存壓力。  分析App內存的詳細狀況是解決問題的第一步,咱們須要對App運行時到底佔用了多少內存、哪些類型的對象有多少個有大體瞭解,並根據實際狀況作出預測,這樣才能在分析時作到有的放矢。Android Studio也提供了很是好用的Memory Profiler堆轉儲分配跟蹤器功能能夠幫咱們迅速定位問題。

AOP加強輔助

AOP是面向切面編程的簡稱,在Android的Gradle插件1.5.0中新增了Transform API以後,編譯時修改字節碼來實現AOP也由於有了官方支持而變得很是方便。  在一些特定狀況下,能夠經過AOP的方式自動處理未捕獲的異常:

  • 拋異常的方法很是明確,調用方式比較固定。
  • 異常處理方式比較統一。
  • 和業務邏輯無關,即自動處理異常後不會影響正常的業務邏輯。典型的例子有讀取Intent Extras參數、讀取SharedPreferences、解析顏色字符串值和顯示隱藏Window等等。

這類問題的解決原理大體相同,咱們以Intent Extras爲例詳細介紹一下。讀取Intent Extras的問題在於咱們很是經常使用的方法 Intent#getStringExtra 在代碼邏輯出錯或者惡意攻擊的狀況下可能會拋出ClassNotFoundException異常,而咱們平時在寫代碼時又不太可能給全部調用都加上try-catch語句,因而一個更安全的Intent工具類應運而生,理論上只要全部人都使用這個工具類來訪問Intent Extras參數就能夠防止此類型的Crash。可是面對龐大的舊代碼倉庫和諸多的業務部門,修改現有代碼須要極大成本,還有更多的外部依賴SDK基本不可能使用咱們本身的工具類,此時就須要AOP大展身手了。  咱們專門製做了一個Gradle插件,只須要配置一下參數就能夠將某個特定方法的調用替換成另外一個方法:

WaimaiBytecodeManipulator {
     replacements(
         "android/content/Intent.getIntExtra(Ljava/lang/String;I)I=com/waimai/IntentUtil.getInt(Landroid/content/Intent;Ljava/lang/String;I)I",
         "android/content/Intent.getStringExtra(Ljava/lang/String;)Ljava/lang/String;=com/waimai/IntentUtil.getString(Landroid/content/Intent;Ljava/lang/String;)Ljava/lang/String;",
         "android/content/Intent.getBooleanExtra(Ljava/lang/String;Z)Z=com/waimai/IntentUtil.getBoolean(Landroid/content/Intent;Ljava/lang/String;Z)Z",
         ...)
    }
}
複製代碼

上面的配置就能夠將App代碼(包括第三方庫)裏全部的Intent.getXXXExtra調用替換成IntentUtil類中的安全版實現。固然,並非全部的異常都只須要catch住就萬事大吉,若是真的有邏輯錯誤確定須要在開發和測試階段及時暴露出來,因此在IntentUtil中會對App的運行環境作判斷,Debug下會將異常直接拋出,開發同窗能夠根據Crash堆棧分析問題,Release環境下則在捕獲到異常時返回對應的默認值而後將異常上報到服務器。

依賴庫的問題

Android App常常會依賴不少AAR, 每一個AAR可能有多個版本,打包時Gradle會根據規則肯定使用的最終版本號(默認選擇最高版本或者強制指定的版本),而其餘版本的AAR將被丟棄。若是互相依賴的AAR中有不兼容的版本,存在的問題在打包時是不能發現的,只有在相關代碼執行時纔會出現,會形成NoClassDefFoundError、NoSuchFieldError、NoSuchMethodError等異常。如圖所示,order和store兩個業務庫都依賴了platform.aar,一個是1.0版本,一個是2.0版本,默認最終打進APK的只有platform 2.0版本,這時若是order庫裏用到的platform庫裏的某個類或者方法在2.0版本中被刪除了,運行時就可能發生異常,雖然SDK在升級時會盡可能作到向下兼容,但不少時候尤爲是第三方SDK是無法獲得保證的,在美團外賣Android App v6.0版本時由於這個緣由致使熱修復功能喪失,所以爲了提早發現問題,咱們接入了依賴檢查插件Defensor。

image

Defensor在編譯時經過DexTask獲取到全部的輸入文件(也就是被編譯過的class文件),而後檢查每一個文件裏引用的類、字段、方法等是否存在。

除此以外咱們寫了一個Gradle插件SVD(strict version dependencies)來對那些重要的SDK的版本進行統一管理。插件會在編譯時檢查Gradle最終使用的SDK版本是否和配置中的一致,若是不一致插件會終止編譯並報錯,並同時會打印出發生衝突的SDK的全部依賴關係。

Crash的預防實踐

單純的靠約定或規範去減小Crash的發生是不現實的。約定和規範受限於組織架構和具體執行的我的,很容易被忽略,只有靠工程架構和工具才能保證Crash的預防長久的執行下去。

工程架構對Crash率的影響

在治理Crash的實踐中,咱們每每忽略了工程架構對Crash率的影響。Crash的發生大部分緣由是源於程序員的不合理的代碼,而程序員工做中最直接的接觸的就是工程架構。對於一個邊界模糊,層級混亂的架構,程序員是更加容易寫出引發Crash的代碼。在這樣的架構裏面,即便程序員意識到致使某種寫法存在問題,想要去改善這樣不合理的代碼,也是很是困難的。相反,一個層級清晰,邊界明確的架構,是可以大大減小Crash發生的機率,治理和預防Crash也是相對更容易。這裏咱們能夠舉幾個咱們實踐過的例子闡述。

業務模塊的劃分  原來咱們的Crash基本上都是由個別同窗關注解決的,團隊裏的每一個同窗都會提交可能引發Crash的代碼,若是負責Crash的同窗由於某些事情,暫時沒有關注App的Crash率,那麼形成Crash的同窗也不會知道他的代碼引發了Crash。

對於這個問題,咱們的作法是App的業務模塊化。業務模塊化後,每一個業務都有都有惟一包名和對應的負責人。當某個模塊發生了Crash,能夠根據包名提交問題給這個模塊的負責人,讓他第一時間進行處理。業務模塊化自己也是工程架構優先須要考慮的事情之一。

頁面跳轉路由統一處理頁面跳轉  對外賣App而言,使用過程當中最多的就是頁面間的跳轉,而頁面間跳轉常常會形成ActivityNotFoundException,例如咱們配了一個scheme,但對方的scheme路徑已經發生了變化;又例如,咱們調用手機上相冊的功能,而相冊應用已被用戶本身禁用或移除了。解決這一類Crash,其實也很簡單,只須要在startActivity增長ActivityNotFoundException異常捕獲便可。但一個App裏,啓動Activity的地方,幾乎是隨處可見,沒法預測哪一處會形成ActivityNotFoundException。  咱們的作法是將頁面的跳轉,都經過咱們封裝的scheme路由去分發。這樣的好處是,經過scheme路由,在工程架構上全部業務都是解耦,模塊間不須要相互依賴就能夠實現頁面的跳轉和基本類型參數的傳遞;同時,因爲全部的頁面跳轉都會走scheme路由,咱們只須要在scheme路由裏一處加上ActivityNotFoundException異常捕獲便可解決這種類型的Crash。路由設計示意圖以下:

image

網絡層統一處理API髒數據  客戶端的很大一部分的Crash是由於API返回的髒數據。好比當API返回空值、空數組或返回不是約定類型的數據,App收到這些數據,就極有可能發生空指針、數組越界和類型轉換錯誤等Crash。並且這樣的髒數據,特別容易引發線上大面積的崩潰。  最先咱們的工程的網絡層用法是:頁面監聽網絡成功和失敗的回調,網絡成功後,將JSON數據傳遞給頁面,頁面解析Model,初始化View,如圖所示。這樣的問題就是,網絡雖然請求成功了,可是JSON解析Model這個過程可能存在問題,例如沒有返回數據或者返回了類型不對的數據,而這個髒數據致使問題會出如今UI層,直接反應給用戶。

image

根據上圖,咱們能夠看到因爲網絡層只承擔了請求網絡的職責,沒有承擔數據解析的職責,數據解析的職責交給了頁面去處理。這樣使得咱們一旦發現髒數據致使的Crash,就只能在網絡請求的回調裏面增長各類判斷去兼容髒數據。咱們有幾百個頁面,補漏徹底補不過來。經過幾個版本的重構,咱們從新劃分了網絡層的職責,如圖所示:

image

從圖上能夠看出,重構後的網絡層負責請求網絡和數據解析,若是存在髒數據的話,在網絡層就會發現問題,不會影響到UI層,返回給UI層的都是校驗成功的數據。這樣改造後,咱們發現這類的Crash率有了極大的改善。

大圖監控

上面講到大對象是致使OOM的主要緣由之一,而Bitmap是App裏最多見的大對象類型,所以對佔用內存過大的Bitmap對象的監控就頗有必要了。  咱們用AOP方式Hook了三種常見圖片庫的加載圖片回調方法,同時監控圖片庫加載圖片時的兩個維度:  1. 加載圖片使用的URL。外賣App中除靜態資源外,全部圖片都要求發佈到專用的圖片CDN服務器上,加載圖片時使用正則表達式匹配URL,除了限定CDN域名以外還要求全部圖片加載時都要添加對應的動態縮放參數。  2. 最終加載出的圖片結果(也就是Bitmap對象)。咱們知道Bitmap對象所佔內存和其分辨率大小成正比,而通常狀況下在ImageView上設置超過自身尺寸的圖片是沒有意義的,因此咱們要求顯示在ImageView中的Bitmap分辨率不容許超過View自身的尺寸(爲了下降誤報率也能夠設定一個報警閾值)。

開發過程當中,在App裏檢測到不合規的圖片時會當即高亮出錯的ImageView所在的位置並彈出對話框提示ImageView所在的Activity、XPath和加載圖片使用的URL等信息,以下圖,輔助開發同窗定位並解決問題。在Release環境下能夠將報警信息上報到服務器,實時觀察數據,有問題及時處理。 

image

Lint檢查

咱們發現線上的不少Crash其實能夠在開發過程當中經過Lint檢查來避免。Lint是Google提供的Android靜態代碼檢查工具,能夠掃描並發現代碼中潛在的問題,提醒開發人員及早修正,提升代碼質量。

可是Android原生提供的Lint規則(如是否使用了高版本API)遠遠不夠,缺乏一些咱們認爲有必要的檢測,也不能檢查代碼規範。所以咱們開始開發自定義Lint,目前咱們經過自定義Lint規則已經實現了Crash預防、Bug預防、提高性能/安全和代碼規範檢查這些功能。如檢查實現了Serializable接口的類,其成員變量(包括從父類繼承的)所聲明的類型都要實現Serializable接口,能夠有效的避免NotSerializableException;強制使用封裝好的工具類如ColorUtil、WindowUtil等能夠有效的避免由於參數不正確產生的IllegalArgumentException和由於Activity已經finish致使的BadTokenException。

Lint檢查能夠在多個階段執行,包括在本地手動檢查、編碼實時檢查、編譯時檢查、commit時檢查,以及在CI系統中提Pull Request時檢查、打包時檢查等,以下圖所示。更詳細的內容可參考《美團外賣Android Lint代碼檢查實踐》

image

資源重複檢查

在以前的文章《美團外賣Android平臺化架構演進實踐》中講述了咱們的平臺化演進過程,在這個過程當中你們很大的一部分工做是下沉,可是下沉不徹底就會致使一些類和資源的重複,類由於有包名的限制不會出現問題。可是一些資源文件如layout、drawable等若是同名則下層會被上層覆蓋,這時layout裏view的id發生了變化就可能致使空指針的問題。爲了不這種問題,咱們寫了一個Gradle插件經過hook MergeResource這個Task,拿到全部library和主庫的資源文件,若是檢查到重複則會中斷編譯過程,輸出重複的資源名及對應的library name,同時避免有些資源由於樣式等緣由確實須要覆蓋,所以咱們設置了白名單。同時在這個過程當中咱們也拿到了全部的的圖片資源,能夠順手作圖片大小的本地監控,以下圖所示: 

image

Crash的監控&止損的實踐

監控

在通過前面提到的各類檢查和測試以後,應用便開始發佈了。咱們創建了以下圖的監控流程,來保證異常發生時可以及時獲得反饋並處理。首先是灰度監控,灰度階段是增量Crash最容易暴露的階段,若是這個階段沒有很好的把握住,會使得增量變存量,從而致使Crash率上升。若是條件容許的話,能夠在灰度期間制定一些灰度策略去提升這個階段Crash的暴露。例如分渠道灰度、分城市灰度、分業務場景灰度、新裝用戶的灰度等等,儘可能覆蓋全部的分支。灰度結束以後便開始全量,在全量的過程當中咱們還須要一些平常Crash監控和Crash率的異常報警來防止突發狀況的發生,例如由於後臺上線或者運營配置錯誤致使的線上Crash。除此以外還須要一些其餘的監控,例如,以前提到的大圖監控,來避免由於大圖致使的OOM。具體的輸出形式主要有郵件通知、IM通知、報表。

image

止損

儘管咱們在前面作了那麼多,可是Crash仍是沒法避免的,例如,在灰度階段由於量級不夠,有些Crash沒有被暴露出來;又或者某些功能客戶端比後臺更早上線,而這些功能在灰度階段沒有被覆蓋到;這些狀況下,若是出現問題就須要考慮如何止損了。

問題發生時首先須要評估重要性,若是問題不是很嚴重並且修復成本較高能夠考慮在下個版本再修復,相反若是問題比較嚴重,對用戶體驗或下單有影響時就必需要修復。修復時首先考慮業務降級,主要看該部分異常的業務是否有兜底或者A/B策略,這樣是最穩妥也是最有效的方式。若是業務不能降級就須要考慮熱修復了,目前美團外賣Android App接入的熱修復框架是自研的Robust,能夠修復90%以上的場景,熱修成功率也達到了99%以上。若是問題發生在熱修復沒法覆蓋的場景,就只能強制用戶升級。強制升級由於覆蓋週期長,同時影響用戶的體驗,只在萬不得已的狀況下才會使用。

展望

Crash的自我修復

咱們在作新技術選型時除了要考慮是否能知足業務需求、是否比現有技術更優秀和團隊學習成本等因素以外,兼容性和穩定性也很是重要。但面對國內非富多彩的Android系統環境,在體量百萬級以上的的App中幾乎不可能實現毫無瑕疵的技術方案和組件,因此通常狀況下若是某個技術實現方案能夠達到0.01‰如下的崩潰率,而其餘方案也沒有更好的表現,咱們就認爲它是能夠接受的。可是哪怕僅僅十萬分之一的崩潰率,也表明還有用戶受到影響,而咱們認爲Crash對用戶來講是最糟糕的體驗,尤爲是涉及到交易的場景,因此咱們必須本着每一單都很重要的原則,盡最大努力保證用戶順利執行流程。

實際狀況中有一些技術方案在兼容性和穩定性上作了必定妥協的場景,每每是由於考慮到性能或擴展性等方面的優點。這種狀況下咱們其實能夠再多作一些,進一步提升App的可用性。就像不少操做系統都有「兼容模式」或者「安全模式」,不少自動化機械機器都配套有手動操做模式同樣,App裏也能夠實現備用的降級方案,而後設置特定條件的觸發策略,從而達到自動修復Crash的目的。

舉例來說,Android 3.0中引入了硬件加速機制,雖然能夠提升繪製幀率而且下降CPU佔用率,可是在某些機型上仍是會有繪製錯亂甚至Crash的狀況,這時咱們就能夠在App中記錄硬件加速相關的Crash問題或者使用檢測代碼主動檢測硬件加速功能是否正常工做,而後主動選擇是否開啓硬件加速,這樣既可讓絕大部分用戶享受硬件加速帶來的優點,也能夠保障硬件加速功能不完善的機型不受影響。  還有一些相似的能夠作自動降級的場景,好比:

  • 部分使用JNI實現的模塊,在SO加載失敗或者運行時發生異常則能夠降級爲Java版實現。
  • RenderScript實現的圖片模糊效果,也能夠在失敗後降級爲普通的Java版高斯模糊算法。
  • 在使用Retrofit網絡庫時發現OkHttp3或者HttpURLConnection網絡通道失敗率高,能夠主動切換到另外一種通道。

這類問題都須要根據具體狀況具體分析,若是能夠找到準確的斷定條件和穩定的修復方案,就可讓App穩定性再上一個臺階。

特定Crash類型日誌自動回撈

外賣業務發展迅速,即便咱們在開發時使用各類工具、措施來避免Crash的發生,但Crash仍是不可避免。線上某些怪異的Crash發生後,咱們除了分析Crash堆棧信息以外,還可使用離線日誌回撈、下發動態日誌等工具來還原Crash發生時的場景,幫助開發同窗定位問題,可是這兩種方式都有它們各自的問題。離線日誌顧名思義,它的內容都是預先記錄好的,有時候可能會漏掉一些關鍵信息,由於在代碼中加日誌通常只是在業務關鍵點,在大量的普通方法中不可能都加上日誌。動態日誌(Holmes)存在的問題是每次下發只能針對已知UUID的一個用戶的一臺設備,對於大量線上Crash的狀況這種操做並不合適,由於咱們並不能知道哪一個發生Crash的用戶還會再次復現此次操做,下發配置充滿了不肯定性。

咱們能夠改造Holmes使其支持批量甚至全量下發動態日誌,記錄的日誌等到發生特定類型的Crash時才上報,這樣一來能夠減小日誌服務器壓力,同時也能夠極大提升定位問題的效率,由於咱們能夠肯定上報日誌的設備最後都真正發生了該類型Crash,再來分析日誌就能夠作到事半功倍。

總結

業務的快速發展,每每不可能給團隊充足的時間去治理Crash,而Crash又是App最重要的指標之一。團隊須要由一個個Crash個例,去探究每個Crash發生的最本質緣由,找到最合理解決這類Crash的方案,創建解決這一類Crash的長效機制,而不能飲鴆止渴。只有這樣,隨着版本的不斷迭代,咱們才能在Crash治理之路上離目標愈來愈近。

參考資料

  1. Crash率從2.2%降至0.2%,這個團隊是怎麼作到的?
  2. Android運行時ART加載OAT文件的過程分析
  3. Android動態日誌系統Holmes
  4. Android Hook技術防範漫談
  5. 美團外賣Android Lint代碼檢查實踐

面試必備之UI刷新大解密

Flutter基礎-環境搭建及demo運行

個人Android重構之旅:框架篇

MVC,MVP 和 MVVM 模式如何選擇?

微信公衆號:終端研發部

技術
相關文章
相關標籤/搜索