摘要:通過團(tuán)隊的全力全策,美團(tuán)外賣的平均率從千分之三降到了萬分之二,最優(yōu)值萬一左右率統(tǒng)計方式次數(shù)。美團(tuán)外賣自年創(chuàng)建以來,業(yè)務(wù)就以指數(shù)級的速度發(fā)展。目前美團(tuán)外賣日完成訂單量已突破萬,成為美團(tuán)點評最重要的業(yè)務(wù)之一。
面試中常常問到的是Android的性能優(yōu)化以及Crash處理。 今天我們來學(xué)習(xí)一下啊美團(tuán)App的Crash處理。更多參考《Android性能優(yōu)化:手把手帶你全面實現(xiàn)內(nèi)存優(yōu)化》
原為地址: https://blog.csdn.net/Meituan...
Crash率是衡量一個App好壞的重要指標(biāo)之一,如果你忽略了它的存在,它就會愈演愈烈,最后造成大量用戶的流失,進(jìn)而給公司帶來無法估量的損失。本文講述美團(tuán)外賣Android客戶端團(tuán)隊在將App的Crash率從千分之三做到萬分之二過程中所做的大量實踐工作,拋磚引玉,希望能夠為其他團(tuán)隊提供一些經(jīng)驗和啟發(fā)。
面臨的挑戰(zhàn)和成果面對用戶使用頻率高,外賣業(yè)務(wù)增長快,Android碎片化嚴(yán)重這些問題,美團(tuán)外賣Android App如何持續(xù)的降低Crash率,是一項極具挑戰(zhàn)的事情。通過團(tuán)隊的全力全策,美團(tuán)外賣Android App的平均Crash率從千分之三降到了萬分之二,最優(yōu)值萬一左右(Crash率統(tǒng)計方式:Crash次數(shù)/DAU)。
美團(tuán)外賣自2013年創(chuàng)建以來,業(yè)務(wù)就以指數(shù)級的速度發(fā)展。美團(tuán)外賣承載的業(yè)務(wù),從單一的餐飲業(yè)務(wù),發(fā)展到餐飲、超市、生鮮、果蔬、藥品、鮮花、蛋糕、跑腿等十多個大品類業(yè)務(wù)。目前美團(tuán)外賣日完成訂單量已突破2000萬,成為美團(tuán)點評最重要的業(yè)務(wù)之一。美團(tuán)外賣客戶端所承載的業(yè)務(wù)模塊越來越多,產(chǎn)品復(fù)雜度越來越高,團(tuán)隊開發(fā)人員日益增加,這些都給App降低Crash率帶來了巨大的挑戰(zhàn)。
Crash的治理實踐對于Crash的治理,我們盡量遵守以下三點原則:?
由點到面。一個Crash發(fā)生了,我們不能只針對這個Crash的去解決,而要去考慮這一類Crash怎么去解決和預(yù)防。只有這樣才能使得這一類Crash真正被解決。?
異常不能隨便吃掉。隨意的使用try-catch,只會增加業(yè)務(wù)的分支和隱蔽真正的問題,要了解Crash的本質(zhì)原因,根據(jù)本質(zhì)原因去解決。catch的分支,更要根據(jù)業(yè)務(wù)場景去兜底,保證后續(xù)的流程正常。?
預(yù)防勝于治理。當(dāng)Crash發(fā)生的時候,損失已經(jīng)造成了,我們再怎么治理也只是減少損失。盡可能的提前預(yù)防Crash的發(fā)生,可以將Crash消滅在萌芽階段。
常規(guī)的Crash治理常規(guī)Crash發(fā)生的原因主要是由于開發(fā)人員編寫代碼不小心導(dǎo)致的。解決這類Crash需要由點到面,根據(jù)Crash引發(fā)的原因和業(yè)務(wù)本身,統(tǒng)一集中解決。常見的Crash類型包括:空節(jié)點、角標(biāo)越界、類型轉(zhuǎn)換異常、實體對象沒有序列化、數(shù)字轉(zhuǎn)換異常、Activity或Service找不到等。這類Crash是App中最為常見的Crash,也是最容易反復(fù)出現(xiàn)的。在獲取Crash堆棧信息后,解決這類Crash一般比較簡單,更多考慮的應(yīng)該是如何避免。下面介紹兩個我們治理的量比較大的Crash。
NullPointerException是我們遇到最頻繁的,造成這種Crash一般有兩種情況:?
對象本身沒有進(jìn)行初始化就進(jìn)行操作。?
對象已經(jīng)初始化過,但是被回收或者手動置為null,然后對其進(jìn)行操作。
針對第一種情況導(dǎo)致的原因有很多,可能是開發(fā)人員的失誤、API返回數(shù)據(jù)解析異常、進(jìn)程被殺死后靜態(tài)變量沒初始化導(dǎo)致,我們可以做的有:?
對可能為空的對象做判空處理。?
養(yǎng)成使用@NonNull和@Nullable注解的習(xí)慣。?
盡量不使用靜態(tài)變量,萬不得已使用SharedPreferences來存儲。?
考慮使用Kotlin語言。
針對第二種情況大部分是由于Activity/Fragment銷毀或被移除后,在Message、Runnable、網(wǎng)絡(luò)等回調(diào)中執(zhí)行了一些代碼導(dǎo)致的,我們可以做的有:?
Message、Runnable回調(diào)時,判斷Activity/Fragment是否銷毀或被移除;加try-catch保護(hù);Activity/Fragment銷毀時移除所有已發(fā)送的Runnable。?
封裝LifecycleMessage/Runnable基礎(chǔ)組件,并自定義Lint檢查,提示使用封裝好的基礎(chǔ)組件。?
在BaseActivity、BaseFragment的onDestory()里把當(dāng)前Activity所發(fā)的所有請求取消掉。
這類Crash常見于對ListView的操作和多線程下對容器的操作。
針對ListView中造成的IndexOutOfBoundsException,經(jīng)常是因為外部也持有了Adapter里數(shù)據(jù)的引用(如在Adapter的構(gòu)造函數(shù)里直接賦值),這時如果外部引用對數(shù)據(jù)更改了,但沒有及時調(diào)用notifyDataSetChanged(),則有可能造成Crash,對此我們封裝了一個BaseAdapter,數(shù)據(jù)統(tǒng)一由Adapter自己維護(hù)通知, 同時也極大的避免了The content of the adapter has changed but ListView did not receive a notification,這兩類Crash目前得到了統(tǒng)一的解決。
另外,很多容器是線程不安全的,所以如果在多線程下對其操作就容易引發(fā)IndexOutOfBoundsException。常用的如JDK里的ArrayList和Android里的SparseArray、ArrayMap,同時也要注意有一些類的內(nèi)部實現(xiàn)也是用的線程不安全的容器,如Bundle里用的就是ArrayMap。
系統(tǒng)級Crash治理眾所周知,Android的機型眾多,碎片化嚴(yán)重,各個硬件廠商可能會定制自己的ROM,更改系統(tǒng)方法,導(dǎo)致特定機型的崩潰。發(fā)現(xiàn)這類Crash,主要靠云測平臺配合自動化測試,以及線上監(jiān)控,這種情況下的Crash堆棧信息很難直接定位問題。下面是常見的解決思路:
嘗試找到造成Crash的可疑代碼,看是否有特異的API或者調(diào)用方式不當(dāng)導(dǎo)致的,嘗試修改代碼邏輯來進(jìn)行規(guī)避。
通過Hook來解決,Hook分為Java Hook和Native Hook。Java Hook主要靠反射或者動態(tài)代理來更改相應(yīng)API的行為,需要嘗試找到可以Hook的點,一般Hook的點多為靜態(tài)變量,同時需要注意Android不同版本的API,類名、方法名和成員變量名都可能不一樣,所以要做好兼容工作;Native Hook原理上是用更改后方法把舊方法在內(nèi)存地址上進(jìn)行替換,需要考慮到Dalvik和ART的差異;相對來說Native Hook的兼容性更差一點,所以用Native Hook的時候需要配合降級策略。
如果通過前兩種方式都無法解決的話,我們只能嘗試反編譯ROM,尋找解決的辦法。
我們舉一個定制系統(tǒng)ROM導(dǎo)致Crash的例子,根據(jù)Crash平臺統(tǒng)計數(shù)據(jù)發(fā)現(xiàn)該Crash只發(fā)生在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
我們發(fā)現(xiàn)原生系統(tǒng)上對應(yīng)系統(tǒng)版本的AbsListView里并沒有UpdateBottomFlagTask類,因此可以斷定是vivo該版本定制的ROM修改了系統(tǒng)的實現(xiàn)。我們在定位這個Crash的可疑點無果后決定通過Hook的方式解決,通過源碼發(fā)現(xiàn)AsyncTask$SerialExecutor是靜態(tài)變量,是一個很好的Hook的點,通過反射添加try-catch解決。因為修改的是final對象所以需要先反射修改accessFlags,需要注意ART和Dalvik下對應(yīng)的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); } }
美團(tuán)外賣App用上述方法解決了對應(yīng)的Crash,但是美團(tuán)App里的外賣頻道因為平臺的限制無法通過這種方式,于是我們嘗試反編譯ROM。?
Android ROM編譯時會將framework、app、bin等目錄打入system.img中,system.img是Android系統(tǒng)中用來存放系統(tǒng)文件的鏡像 (image),文件格式一般為yaffs2或ext。但Android 5.0開始支持dm-verity后,system.img不再提供,而是提供了三個文件system.new.dat,system.patch.dat,system.transfer.list,因此我們首先需要通過上述的三個文件得到system.img。但我們將vivo ROM解壓后發(fā)現(xiàn)廠商將system.new.dat進(jìn)行了分片,如下圖所示:
經(jīng)過對system.transfer.list中的信息和system.new.dat 1 2 3 … 文件大小對比研究,發(fā)現(xiàn)一些共同點,system.transfer.list中的每一個block數(shù)*4KB 與對應(yīng)的分片文件的大小大致相同,故大膽猜測,vivo ROM對system.patch.dat分片也只是單純的按block先后順序進(jìn)行了分片處理。所以我們只需要在轉(zhuǎn)化img前將這些分片文件合成一個system.patch.dat文件就可以了。最后根據(jù)system.img的文件系統(tǒng)格式進(jìn)行解包,拿到framework目錄,其中有framework.jar和boot.oat等文件,因為Android4.4之后引入了ART虛擬機,會預(yù)先把system/framework中的一些jar包轉(zhuǎn)換為oat格式,所以我們還需要將對應(yīng)的oat文件通過ota2dex將其解包獲得dex文件,之后通過dex2jar和jd-gui查看源碼。
OOMOOM是OutOfMemoryError的簡稱,在常見的Crash疑難排行榜上,OOM絕對可以名列前茅并且經(jīng)久不衰。因為它發(fā)生時的Crash堆棧信息往往不是導(dǎo)致問題的根本原因,而只是壓死駱駝的最后一根稻草。?
導(dǎo)致OOM的原因大部分如下:?
內(nèi)存泄漏,大量無用對象沒有被及時回收導(dǎo)致后續(xù)申請內(nèi)存失敗。?
大內(nèi)存對象過多,最常見的大對象就是Bitmap,幾個大圖同時加載很容易觸發(fā)OOM。
內(nèi)存泄漏?
內(nèi)存泄漏指系統(tǒng)未能及時釋放已經(jīng)不再使用的內(nèi)存對象,一般是由錯誤的程序代碼邏輯引起的。在Android平臺上,最常見也是最嚴(yán)重的內(nèi)存泄漏就是Activity對象泄漏。Activity承載了App的整個界面功能,Activity的泄漏同時也意味著它持有的大量資源對象都無法被回收,極其容易造成OOM。?
常見的可能會造成Activity泄漏的原因有:?
匿名內(nèi)部類實現(xiàn)Handler處理消息,可能導(dǎo)致隱式持有的Activity對象無法回收。?
Activity和Context對象被混淆和濫用,在許多只需要Application Context而不需要使用Activity對象的地方使用了Activity對象,比如注冊各類Receiver、計算屏幕密度等等。?
View對象處理不當(dāng),使用Activity的LayoutInflater創(chuàng)建的View自身持有的Context對象其實就是Activity,這點經(jīng)常被忽略,在自己實現(xiàn)View重用等場景下也會導(dǎo)致Activity泄漏。
對于Activity泄漏,目前已經(jīng)有了一個非常好用的檢測工具:LeakCanary,它可以自動檢測到所有Activity的泄漏情況,并且在發(fā)生泄漏時給出十分友好的界面提示,同時為了防止開發(fā)人員的疏漏,我們也會將其上報到服務(wù)器,統(tǒng)一檢查解決。另外我們可以在debug下使用StrictMode來檢查Activity的泄露、Closeable對象沒有被關(guān)閉等問題。
大對象?
在Android平臺上,我們分析任一應(yīng)用的內(nèi)存信息,幾乎都可以得出同樣的結(jié)論:占用內(nèi)存最多的對象大都是Bitmap對象。隨著手機屏幕尺寸越來越大,屏幕分辨率也越來越高,1080p和更高的2k屏已經(jīng)占了大半份額,為了達(dá)到更好的視覺效果,我們往往需要使用大量高清圖片,同時也為OOM埋下了禍根。?
對于圖片內(nèi)存優(yōu)化,我們有幾個常用的思路:?
盡量使用成熟的圖片庫,比如Glide,圖片庫會提供很多通用方面的保障,減少不必要的人為失誤。?
根據(jù)實際需要,也就是View尺寸來加載圖片,可以在分辨率較低的機型上盡可能少地占用內(nèi)存。除了常用的BitmapFactory.Options#inSampleSize和Glide提供的BitmapRequestBuilder#override之外,我們的圖片CDN服務(wù)器也支持圖片的實時縮放,可以在服務(wù)端進(jìn)行圖片縮放處理,從而減輕客戶端的內(nèi)存壓力。?
分析App內(nèi)存的詳細(xì)情況是解決問題的第一步,我們需要對App運行時到底占用了多少內(nèi)存、哪些類型的對象有多少個有大致了解,并根據(jù)實際情況做出預(yù)測,這樣才能在分析時做到有的放矢。Android Studio也提供了非常好用的Memory Profiler,堆轉(zhuǎn)儲和分配跟蹤器功能可以幫我們迅速定位問題。
AOP增強輔助AOP是面向切面編程的簡稱,在Android的Gradle插件1.5.0中新增了Transform API之后,編譯時修改字節(jié)碼來實現(xiàn)AOP也因為有了官方支持而變得非常方便。?
在一些特定情況下,可以通過AOP的方式自動處理未捕獲的異常:?
拋異常的方法非常明確,調(diào)用方式比較固定。?
異常處理方式比較統(tǒng)一。?
和業(yè)務(wù)邏輯無關(guān),即自動處理異常后不會影響正常的業(yè)務(wù)邏輯。典型的例子有讀取Intent Extras參數(shù)、讀取SharedPreferences、解析顏色字符串值和顯示隱藏Window等等。
這類問題的解決原理大致相同,我們以Intent Extras為例詳細(xì)介紹一下。讀取Intent Extras的問題在于我們非常常用的方法 Intent#getStringExtra 在代碼邏輯出錯或者惡意攻擊的情況下可能會拋出ClassNotFoundException異常,而我們平時在寫代碼時又不太可能給所有調(diào)用都加上try-catch語句,于是一個更安全的Intent工具類應(yīng)運而生,理論上只要所有人都使用這個工具類來訪問Intent Extras參數(shù)就可以防止此類型的Crash。但是面對龐大的舊代碼倉庫和諸多的業(yè)務(wù)部門,修改現(xiàn)有代碼需要極大成本,還有更多的外部依賴SDK基本不可能使用我們自己的工具類,此時就需要AOP大展身手了。?
我們專門制作了一個Gradle插件,只需要配置一下參數(shù)就可以將某個特定方法的調(diào)用替換成另一個方法:
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調(diào)用替換成IntentUtil類中的安全版實現(xiàn)。當(dāng)然,并不是所有的異常都只需要catch住就萬事大吉,如果真的有邏輯錯誤肯定需要在開發(fā)和測試階段及時暴露出來,所以在IntentUtil中會對App的運行環(huán)境做判斷,Debug下會將異常直接拋出,開發(fā)同學(xué)可以根據(jù)Crash堆棧分析問題,Release環(huán)境下則在捕獲到異常時返回對應(yīng)的默認(rèn)值然后將異常上報到服務(wù)器。
依賴庫的問題Android App經(jīng)常會依賴很多AAR, 每個AAR可能有多個版本,打包時Gradle會根據(jù)規(guī)則確定使用的最終版本號(默認(rèn)選擇最高版本或者強制指定的版本),而其他版本的AAR將被丟棄。如果互相依賴的AAR中有不兼容的版本,存在的問題在打包時是不能發(fā)現(xiàn)的,只有在相關(guān)代碼執(zhí)行時才會出現(xiàn),會造成NoClassDefFoundError、NoSuchFieldError、NoSuchMethodError等異常。如圖所示,order和store兩個業(yè)務(wù)庫都依賴了platform.aar,一個是1.0版本,一個是2.0版本,默認(rèn)最終打進(jìn)APK的只有platform 2.0版本,這時如果order庫里用到的platform庫里的某個類或者方法在2.0版本中被刪除了,運行時就可能發(fā)生異常,雖然SDK在升級時會盡量做到向下兼容,但很多時候尤其是第三方SDK是沒法得到保證的,在美團(tuán)外賣Android App v6.0版本時因為這個原因?qū)е聼嵝迯?fù)功能喪失,因此為了提前發(fā)現(xiàn)問題,我們接入了依賴檢查插件Defensor。
Defensor在編譯時通過DexTask獲取到所有的輸入文件(也就是被編譯過的class文件),然后檢查每個文件里引用的類、字段、方法等是否存在。
除此之外我們寫了一個Gradle插件SVD(strict version dependencies)來對那些重要的SDK的版本進(jìn)行統(tǒng)一管理。插件會在編譯時檢查Gradle最終使用的SDK版本是否和配置中的一致,如果不一致插件會終止編譯并報錯,并同時會打印出發(fā)生沖突的SDK的所有依賴關(guān)系。
Crash的預(yù)防實踐單純的靠約定或規(guī)范去減少Crash的發(fā)生是不現(xiàn)實的。約定和規(guī)范受限于組織架構(gòu)和具體執(zhí)行的個人,很容易被忽略,只有靠工程架構(gòu)和工具才能保證Crash的預(yù)防長久的執(zhí)行下去。
工程架構(gòu)對Crash率的影響在治理Crash的實踐中,我們往往忽略了工程架構(gòu)對Crash率的影響。Crash的發(fā)生大部分原因是源于程序員的不合理的代碼,而程序員工作中最直接的接觸的就是工程架構(gòu)。對于一個邊界模糊,層級混亂的架構(gòu),程序員是更加容易寫出引起Crash的代碼。在這樣的架構(gòu)里面,即使程序員意識到導(dǎo)致某種寫法存在問題,想要去改善這樣不合理的代碼,也是非常困難的。相反,一個層級清晰,邊界明確的架構(gòu),是能夠大大減少Crash發(fā)生的概率,治理和預(yù)防Crash也是相對更容易。這里我們可以舉幾個我們實踐過的例子闡述。
業(yè)務(wù)模塊的劃分?
原來我們的Crash基本上都是由個別同學(xué)關(guān)注解決的,團(tuán)隊里的每個同學(xué)都會提交可能引起Crash的代碼,如果負(fù)責(zé)Crash的同學(xué)因為某些事情,暫時沒有關(guān)注App的Crash率,那么造成Crash的同學(xué)也不會知道他的代碼引起了Crash。
對于這個問題,我們的做法是App的業(yè)務(wù)模塊化。業(yè)務(wù)模塊化后,每個業(yè)務(wù)都有都有唯一包名和對應(yīng)的負(fù)責(zé)人。當(dāng)某個模塊發(fā)生了Crash,可以根據(jù)包名提交問題給這個模塊的負(fù)責(zé)人,讓他第一時間進(jìn)行處理。業(yè)務(wù)模塊化本身也是工程架構(gòu)優(yōu)先需要考慮的事情之一。
頁面跳轉(zhuǎn)路由統(tǒng)一處理頁面跳轉(zhuǎn)?
對外賣App而言,使用過程中最多的就是頁面間的跳轉(zhuǎn),而頁面間跳轉(zhuǎn)經(jīng)常會造成ActivityNotFoundException,例如我們配了一個scheme,但對方的scheme路徑已經(jīng)發(fā)生了變化;又例如,我們調(diào)用手機上相冊的功能,而相冊應(yīng)用已被用戶自己禁用或移除了。解決這一類Crash,其實也很簡單,只需要在startActivity增加ActivityNotFoundException異常捕獲即可。但一個App里,啟動Activity的地方,幾乎是隨處可見,無法預(yù)測哪一處會造成ActivityNotFoundException。?
我們的做法是將頁面的跳轉(zhuǎn),都通過我們封裝的scheme路由去分發(fā)。這樣的好處是,通過scheme路由,在工程架構(gòu)上所有業(yè)務(wù)都是解耦,模塊間不需要相互依賴就可以實現(xiàn)頁面的跳轉(zhuǎn)和基本類型參數(shù)的傳遞;同時,由于所有的頁面跳轉(zhuǎn)都會走scheme路由,我們只需要在scheme路由里一處加上ActivityNotFoundException異常捕獲即可解決這種類型的Crash。路由設(shè)計示意圖如下:
網(wǎng)絡(luò)層統(tǒng)一處理API臟數(shù)據(jù)?
客戶端的很大一部分的Crash是因為API返回的臟數(shù)據(jù)。比如當(dāng)API返回空值、空數(shù)組或返回不是約定類型的數(shù)據(jù),App收到這些數(shù)據(jù),就極有可能發(fā)生空指針、數(shù)組越界和類型轉(zhuǎn)換錯誤等Crash。而且這樣的臟數(shù)據(jù),特別容易引起線上大面積的崩潰。?
最早我們的工程的網(wǎng)絡(luò)層用法是:頁面監(jiān)聽網(wǎng)絡(luò)成功和失敗的回調(diào),網(wǎng)絡(luò)成功后,將JSON數(shù)據(jù)傳遞給頁面,頁面解析Model,初始化View,如圖所示。這樣的問題就是,網(wǎng)絡(luò)雖然請求成功了,但是JSON解析Model這個過程可能存在問題,例如沒有返回數(shù)據(jù)或者返回了類型不對的數(shù)據(jù),而這個臟數(shù)據(jù)導(dǎo)致問題會出現(xiàn)在UI層,直接反應(yīng)給用戶。
根據(jù)上圖,我們可以看到由于網(wǎng)絡(luò)層只承擔(dān)了請求網(wǎng)絡(luò)的職責(zé),沒有承擔(dān)數(shù)據(jù)解析的職責(zé),數(shù)據(jù)解析的職責(zé)交給了頁面去處理。這樣使得我們一旦發(fā)現(xiàn)臟數(shù)據(jù)導(dǎo)致的Crash,就只能在網(wǎng)絡(luò)請求的回調(diào)里面增加各種判斷去兼容臟數(shù)據(jù)。我們有幾百個頁面,補漏完全補不過來。通過幾個版本的重構(gòu),我們重新劃分了網(wǎng)絡(luò)層的職責(zé),如圖所示:
從圖上可以看出,重構(gòu)后的網(wǎng)絡(luò)層負(fù)責(zé)請求網(wǎng)絡(luò)和數(shù)據(jù)解析,如果存在臟數(shù)據(jù)的話,在網(wǎng)絡(luò)層就會發(fā)現(xiàn)問題,不會影響到UI層,返回給UI層的都是校驗成功的數(shù)據(jù)。這樣改造后,我們發(fā)現(xiàn)這類的Crash率有了極大的改善。
大圖監(jiān)控上面講到大對象是導(dǎo)致OOM的主要原因之一,而Bitmap是App里最常見的大對象類型,因此對占用內(nèi)存過大的Bitmap對象的監(jiān)控就很有必要了。?
我們用AOP方式Hook了三種常見圖片庫的加載圖片回調(diào)方法,同時監(jiān)控圖片庫加載圖片時的兩個維度:?
1. 加載圖片使用的URL。外賣App中除靜態(tài)資源外,所有圖片都要求發(fā)布到專用的圖片CDN服務(wù)器上,加載圖片時使用正則表達(dá)式匹配URL,除了限定CDN域名之外還要求所有圖片加載時都要添加對應(yīng)的動態(tài)縮放參數(shù)。?
2. 最終加載出的圖片結(jié)果(也就是Bitmap對象)。我們知道Bitmap對象所占內(nèi)存和其分辨率大小成正比,而一般情況下在ImageView上設(shè)置超過自身尺寸的圖片是沒有意義的,所以我們要求顯示在ImageView中的Bitmap分辨率不允許超過View自身的尺寸(為了降低誤報率也可以設(shè)定一個報警閾值)。
開發(fā)過程中,在App里檢測到不合規(guī)的圖片時會立即高亮出錯的ImageView所在的位置并彈出對話框提示ImageView所在的Activity、XPath和加載圖片使用的URL等信息,如下圖,輔助開發(fā)同學(xué)定位并解決問題。在Release環(huán)境下可以將報警信息上報到服務(wù)器,實時觀察數(shù)據(jù),有問題及時處理。?
我們發(fā)現(xiàn)線上的很多Crash其實可以在開發(fā)過程中通過Lint檢查來避免。Lint是Google提供的Android靜態(tài)代碼檢查工具,可以掃描并發(fā)現(xiàn)代碼中潛在的問題,提醒開發(fā)人員及早修正,提高代碼質(zhì)量。
但是Android原生提供的Lint規(guī)則(如是否使用了高版本API)遠(yuǎn)遠(yuǎn)不夠,缺少一些我們認(rèn)為有必要的檢測,也不能檢查代碼規(guī)范。因此我們開始開發(fā)自定義Lint,目前我們通過自定義Lint規(guī)則已經(jīng)實現(xiàn)了Crash預(yù)防、Bug預(yù)防、提升性能/安全和代碼規(guī)范檢查這些功能。如檢查實現(xiàn)了Serializable接口的類,其成員變量(包括從父類繼承的)所聲明的類型都要實現(xiàn)Serializable接口,可以有效的避免NotSerializableException;強制使用封裝好的工具類如ColorUtil、WindowUtil等可以有效的避免因為參數(shù)不正確產(chǎn)生的IllegalArgumentException和因為Activity已經(jīng)finish導(dǎo)致的BadTokenException。
Lint檢查可以在多個階段執(zhí)行,包括在本地手動檢查、編碼實時檢查、編譯時檢查、commit時檢查,以及在CI系統(tǒng)中提Pull Request時檢查、打包時檢查等,如下圖所示。更詳細(xì)的內(nèi)容可參考《美團(tuán)外賣Android Lint代碼檢查實踐》。
資源重復(fù)檢查在之前的文章《美團(tuán)外賣Android平臺化架構(gòu)演進(jìn)實踐》中講述了我們的平臺化演進(jìn)過程,在這個過程中大家很大的一部分工作是下沉,但是下沉不完全就會導(dǎo)致一些類和資源的重復(fù),類因為有包名的限制不會出現(xiàn)問題。但是一些資源文件如layout、drawable等如果同名則下層會被上層覆蓋,這時layout里view的id發(fā)生了變化就可能導(dǎo)致空指針的問題。為了避免這種問題,我們寫了一個Gradle插件通過hook MergeResource這個Task,拿到所有l(wèi)ibrary和主庫的資源文件,如果檢查到重復(fù)則會中斷編譯過程,輸出重復(fù)的資源名及對應(yīng)的library name,同時避免有些資源因為樣式等原因確實需要覆蓋,因此我們設(shè)置了白名單。同時在這個過程中我們也拿到了所有的的圖片資源,可以順手做圖片大小的本地監(jiān)控,如下圖所示:?
在經(jīng)過前面提到的各種檢查和測試之后,應(yīng)用便開始發(fā)布了。我們建立了如下圖的監(jiān)控流程,來保證異常發(fā)生時能夠及時得到反饋并處理。首先是灰度監(jiān)控,灰度階段是增量Crash最容易暴露的階段,如果這個階段沒有很好的把握住,會使得增量變存量,從而導(dǎo)致Crash率上升。如果條件允許的話,可以在灰度期間制定一些灰度策略去提高這個階段Crash的暴露。例如分渠道灰度、分城市灰度、分業(yè)務(wù)場景灰度、新裝用戶的灰度等等,盡量覆蓋所有的分支。灰度結(jié)束之后便開始全量,在全量的過程中我們還需要一些日常Crash監(jiān)控和Crash率的異常報警來防止突發(fā)情況的發(fā)生,例如因為后臺上線或者運營配置錯誤導(dǎo)致的線上Crash。除此之外還需要一些其他的監(jiān)控,例如,之前提到的大圖監(jiān)控,來避免因為大圖導(dǎo)致的OOM。具體的輸出形式主要有郵件通知、IM通知、報表。
止損盡管我們在前面做了那么多,但是Crash還是無法避免的,例如,在灰度階段因為量級不夠,有些Crash沒有被暴露出來;又或者某些功能客戶端比后臺更早上線,而這些功能在灰度階段沒有被覆蓋到;這些情況下,如果出現(xiàn)問題就需要考慮如何止損了。
問題發(fā)生時首先需要評估重要性,如果問題不是很嚴(yán)重而且修復(fù)成本較高可以考慮在下個版本再修復(fù),相反如果問題比較嚴(yán)重,對用戶體驗或下單有影響時就必須要修復(fù)。修復(fù)時首先考慮業(yè)務(wù)降級,主要看該部分異常的業(yè)務(wù)是否有兜底或者A/B策略,這樣是最穩(wěn)妥也是最有效的方式。如果業(yè)務(wù)不能降級就需要考慮熱修復(fù)了,目前美團(tuán)外賣Android App接入的熱修復(fù)框架是自研的Robust,可以修復(fù)90%以上的場景,熱修成功率也達(dá)到了99%以上。如果問題發(fā)生在熱修復(fù)無法覆蓋的場景,就只能強制用戶升級。強制升級因為覆蓋周期長,同時影響用戶的體驗,只在萬不得已的情況下才會使用。
展望 Crash的自我修復(fù)我們在做新技術(shù)選型時除了要考慮是否能滿足業(yè)務(wù)需求、是否比現(xiàn)有技術(shù)更優(yōu)秀和團(tuán)隊學(xué)習(xí)成本等因素之外,兼容性和穩(wěn)定性也非常重要。但面對國內(nèi)非富多彩的Android系統(tǒng)環(huán)境,在體量百萬級以上的的App中幾乎不可能實現(xiàn)毫無瑕疵的技術(shù)方案和組件,所以一般情況下如果某個技術(shù)實現(xiàn)方案可以達(dá)到0.01‰以下的崩潰率,而其他方案也沒有更好的表現(xiàn),我們就認(rèn)為它是可以接受的。但是哪怕僅僅十萬分之一的崩潰率,也代表還有用戶受到影響,而我們認(rèn)為Crash對用戶來說是最糟糕的體驗,尤其是涉及到交易的場景,所以我們必須本著每一單都很重要的原則,盡最大努力保證用戶順利執(zhí)行流程。
實際情況中有一些技術(shù)方案在兼容性和穩(wěn)定性上做了一定妥協(xié)的場景,往往是因為考慮到性能或擴(kuò)展性等方面的優(yōu)勢。這種情況下我們其實可以再多做一些,進(jìn)一步提高App的可用性。就像很多操作系統(tǒng)都有“兼容模式”或者“安全模式”,很多自動化機械機器都配套有手動操作模式一樣,App里也可以實現(xiàn)備用的降級方案,然后設(shè)置特定條件的觸發(fā)策略,從而達(dá)到自動修復(fù)Crash的目的。
舉例來講,Android 3.0中引入了硬件加速機制,雖然可以提高繪制幀率并且降低CPU占用率,但是在某些機型上還是會有繪制錯亂甚至Crash的情況,這時我們就可以在App中記錄硬件加速相關(guān)的Crash問題或者使用檢測代碼主動檢測硬件加速功能是否正常工作,然后主動選擇是否開啟硬件加速,這樣既可以讓絕大部分用戶享受硬件加速帶來的優(yōu)勢,也可以保障硬件加速功能不完善的機型不受影響。?
還有一些類似的可以做自動降級的場景,比如:?
部分使用JNI實現(xiàn)的模塊,在SO加載失敗或者運行時發(fā)生異常則可以降級為Java版實現(xiàn)。?
RenderScript實現(xiàn)的圖片模糊效果,也可以在失敗后降級為普通的Java版高斯模糊算法。?
在使用Retrofit網(wǎng)絡(luò)庫時發(fā)現(xiàn)OkHttp3或者HttpURLConnection網(wǎng)絡(luò)通道失敗率高,可以主動切換到另一種通道。
這類問題都需要根據(jù)具體情況具體分析,如果可以找到準(zhǔn)確的判定條件和穩(wěn)定的修復(fù)方案,就可以讓App穩(wěn)定性再上一個臺階。
特定Crash類型日志自動回?fù)?/b>外賣業(yè)務(wù)發(fā)展迅速,即使我們在開發(fā)時使用各種工具、措施來避免Crash的發(fā)生,但Crash還是不可避免。線上某些怪異的Crash發(fā)生后,我們除了分析Crash堆棧信息之外,還可以使用離線日志回?fù)啤⑾掳l(fā)動態(tài)日志等工具來還原Crash發(fā)生時的場景,幫助開發(fā)同學(xué)定位問題,但是這兩種方式都有它們各自的問題。離線日志顧名思義,它的內(nèi)容都是預(yù)先記錄好的,有時候可能會漏掉一些關(guān)鍵信息,因為在代碼中加日志一般只是在業(yè)務(wù)關(guān)鍵點,在大量的普通方法中不可能都加上日志。動態(tài)日志(Holmes)存在的問題是每次下發(fā)只能針對已知UUID的一個用戶的一臺設(shè)備,對于大量線上Crash的情況這種操作并不合適,因為我們并不能知道哪個發(fā)生Crash的用戶還會再次復(fù)現(xiàn)這次操作,下發(fā)配置充滿了不確定性。
我們可以改造Holmes使其支持批量甚至全量下發(fā)動態(tài)日志,記錄的日志等到發(fā)生特定類型的Crash時才上報,這樣一來可以減少日志服務(wù)器壓力,同時也可以極大提高定位問題的效率,因為我們可以確定上報日志的設(shè)備最后都真正發(fā)生了該類型Crash,再來分析日志就可以做到事半功倍。
總結(jié)業(yè)務(wù)的快速發(fā)展,往往不可能給團(tuán)隊充足的時間去治理Crash,而Crash又是App最重要的指標(biāo)之一。團(tuán)隊需要由一個個Crash個例,去探究每一個Crash發(fā)生的最本質(zhì)原因,找到最合理解決這類Crash的方案,建立解決這一類Crash的長效機制,而不能飲鴆止渴。只有這樣,隨著版本的不斷迭代,我們才能在Crash治理之路上離目標(biāo)越來越近。
參考資料Crash率從2.2%降至0.2%,這個團(tuán)隊是怎么做到的?
Android運行時ART加載OAT文件的過程分析
Android動態(tài)日志系統(tǒng)Holmes
Android Hook技術(shù)防范漫談
美團(tuán)外賣Android Lint代碼檢查實踐
面試必備之UI刷新大解密
Flutter基礎(chǔ)-環(huán)境搭建及demo運行
我的Android重構(gòu)之旅:框架篇
MVC,MVP 和 MVVM 模式如何選擇?
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/71378.html
摘要:不努力不奮斗,可能就會在基層一輩子止步不前。不過,只一句,如果你還在做這一行,還是一名程序猿媛,想走上坡路的你,也許我這到手的十幾家一線互聯(lián)網(wǎng)公司性能優(yōu)化項目實戰(zhàn)可能會對你有所幫助。 ...
摘要:由于長期苦惱于第三方庫選擇的廣大開發(fā)者而言,這也是谷歌為我們提供的一盞明燈。手機淘寶構(gòu)架演化實踐淘寶相信都不陌生了從年開始,從萬增長到超過億,面臨的問題包括研發(fā)支撐所需要解決的事情各不相同。 ...
閱讀 2322·2021-11-17 09:33
閱讀 848·2021-10-13 09:40
閱讀 579·2019-08-30 15:54
閱讀 786·2019-08-29 15:38
閱讀 2423·2019-08-28 18:15
閱讀 2481·2019-08-26 13:38
閱讀 1847·2019-08-26 13:36
閱讀 2135·2019-08-26 11:36