摘要:通過團隊的全力全策,美團外賣的平均率從千分之三降到了萬分之二,最優值萬一左右率統計方式次數。美團外賣自年創建以來,業務就以指數級的速度發展。目前美團外賣日完成訂單量已突破萬,成為美團點評最重要的業務之一。
面試中常常問到的是Android的性能優化以及Crash處理。 今天我們來學習一下啊美團App的Crash處理。更多參考《Android性能優化:手把手帶你全面實現內存優化》
原為地址: https://blog.csdn.net/Meituan...
Crash率是衡量一個App好壞的重要指標之一,如果你忽略了它的存在,它就會愈演愈烈,最后造成大量用戶的流失,進而給公司帶來無法估量的損失。本文講述美團外賣Android客戶端團隊在將App的Crash率從千分之三做到萬分之二過程中所做的大量實踐工作,拋磚引玉,希望能夠為其他團隊提供一些經驗和啟發。
面臨的挑戰和成果面對用戶使用頻率高,外賣業務增長快,Android碎片化嚴重這些問題,美團外賣Android App如何持續的降低Crash率,是一項極具挑戰的事情。通過團隊的全力全策,美團外賣Android App的平均Crash率從千分之三降到了萬分之二,最優值萬一左右(Crash率統計方式:Crash次數/DAU)。
美團外賣自2013年創建以來,業務就以指數級的速度發展。美團外賣承載的業務,從單一的餐飲業務,發展到餐飲、超市、生鮮、果蔬、藥品、鮮花、蛋糕、跑腿等十多個大品類業務。目前美團外賣日完成訂單量已突破2000萬,成為美團點評最重要的業務之一。美團外賣客戶端所承載的業務模塊越來越多,產品復雜度越來越高,團隊開發人員日益增加,這些都給App降低Crash率帶來了巨大的挑戰。
Crash的治理實踐對于Crash的治理,我們盡量遵守以下三點原則:?
由點到面。一個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是我們遇到最頻繁的,造成這種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所發的所有請求取消掉。
這類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堆棧信息很難直接定位問題。下面是常見的解決思路:
嘗試找到造成Crash的可疑代碼,看是否有特異的API或者調用方式不當導致的,嘗試修改代碼邏輯來進行規避。
通過Hook來解決,Hook分為Java Hook和Native Hook。Java Hook主要靠反射或者動態代理來更改相應API的行為,需要嘗試找到可以Hook的點,一般Hook的點多為靜態變量,同時需要注意Android不同版本的API,類名、方法名和成員變量名都可能不一樣,所以要做好兼容工作;Native Hook原理上是用更改后方法把舊方法在內存地址上進行替換,需要考慮到Dalvik和ART的差異;相對來說Native Hook的兼容性更差一點,所以用Native Hook的時候需要配合降級策略。
如果通過前兩種方式都無法解決的話,我們只能嘗試反編譯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進行了分片,如下圖所示:
經過對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文件,之后通過dex2jar和jd-gui查看源碼。
OOMOOM是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。
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。路由設計示意圖如下:
網絡層統一處理API臟數據?
客戶端的很大一部分的Crash是因為API返回的臟數據。比如當API返回空值、空數組或返回不是約定類型的數據,App收到這些數據,就極有可能發生空指針、數組越界和類型轉換錯誤等Crash。而且這樣的臟數據,特別容易引起線上大面積的崩潰。?
最早我們的工程的網絡層用法是:頁面監聽網絡成功和失敗的回調,網絡成功后,將JSON數據傳遞給頁面,頁面解析Model,初始化View,如圖所示。這樣的問題就是,網絡雖然請求成功了,但是JSON解析Model這個過程可能存在問題,例如沒有返回數據或者返回了類型不對的數據,而這個臟數據導致問題會出現在UI層,直接反應給用戶。
根據上圖,我們可以看到由于網絡層只承擔了請求網絡的職責,沒有承擔數據解析的職責,數據解析的職責交給了頁面去處理。這樣使得我們一旦發現臟數據導致的Crash,就只能在網絡請求的回調里面增加各種判斷去兼容臟數據。我們有幾百個頁面,補漏完全補不過來。通過幾個版本的重構,我們重新劃分了網絡層的職責,如圖所示:
從圖上可以看出,重構后的網絡層負責請求網絡和數據解析,如果存在臟數據的話,在網絡層就會發現問題,不會影響到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環境下可以將報警信息上報到服務器,實時觀察數據,有問題及時處理。?
我們發現線上的很多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代碼檢查實踐》。
資源重復檢查在之前的文章《美團外賣Android平臺化架構演進實踐》中講述了我們的平臺化演進過程,在這個過程中大家很大的一部分工作是下沉,但是下沉不完全就會導致一些類和資源的重復,類因為有包名的限制不會出現問題。但是一些資源文件如layout、drawable等如果同名則下層會被上層覆蓋,這時layout里view的id發生了變化就可能導致空指針的問題。為了避免這種問題,我們寫了一個Gradle插件通過hook MergeResource這個Task,拿到所有library和主庫的資源文件,如果檢查到重復則會中斷編譯過程,輸出重復的資源名及對應的library name,同時避免有些資源因為樣式等原因確實需要覆蓋,因此我們設置了白名單。同時在這個過程中我們也拿到了所有的的圖片資源,可以順手做圖片大小的本地監控,如下圖所示:?
在經過前面提到的各種檢查和測試之后,應用便開始發布了。我們建立了如下圖的監控流程,來保證異常發生時能夠及時得到反饋并處理。首先是灰度監控,灰度階段是增量Crash最容易暴露的階段,如果這個階段沒有很好的把握住,會使得增量變存量,從而導致Crash率上升。如果條件允許的話,可以在灰度期間制定一些灰度策略去提高這個階段Crash的暴露。例如分渠道灰度、分城市灰度、分業務場景灰度、新裝用戶的灰度等等,盡量覆蓋所有的分支。灰度結束之后便開始全量,在全量的過程中我們還需要一些日常Crash監控和Crash率的異常報警來防止突發情況的發生,例如因為后臺上線或者運營配置錯誤導致的線上Crash。除此之外還需要一些其他的監控,例如,之前提到的大圖監控,來避免因為大圖導致的OOM。具體的輸出形式主要有郵件通知、IM通知、報表。
止損盡管我們在前面做了那么多,但是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治理之路上離目標越來越近。
參考資料Crash率從2.2%降至0.2%,這個團隊是怎么做到的?
Android運行時ART加載OAT文件的過程分析
Android動態日志系統Holmes
Android Hook技術防范漫談
美團外賣Android Lint代碼檢查實踐
面試必備之UI刷新大解密
Flutter基礎-環境搭建及demo運行
我的Android重構之旅:框架篇
MVC,MVP 和 MVVM 模式如何選擇?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/71378.html
摘要:不努力不奮斗,可能就會在基層一輩子止步不前。不過,只一句,如果你還在做這一行,還是一名程序猿媛,想走上坡路的你,也許我這到手的十幾家一線互聯網公司性能優化項目實戰可能會對你有所幫助。 ...
摘要:由于長期苦惱于第三方庫選擇的廣大開發者而言,這也是谷歌為我們提供的一盞明燈。手機淘寶構架演化實踐淘寶相信都不陌生了從年開始,從萬增長到超過億,面臨的問題包括研發支撐所需要解決的事情各不相同。 ...
閱讀 2335·2021-11-17 09:33
閱讀 860·2021-10-13 09:40
閱讀 587·2019-08-30 15:54
閱讀 792·2019-08-29 15:38
閱讀 2426·2019-08-28 18:15
閱讀 2489·2019-08-26 13:38
閱讀 1856·2019-08-26 13:36
閱讀 2142·2019-08-26 11:36