成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專(zhuān)欄INFORMATION COLUMN

美團(tuán)外賣(mài)Android Crash治理之路

elva / 3096人閱讀

摘要:通過(guò)團(tuán)隊(duì)的全力全策,美團(tuán)外賣(mài)的平均率從千分之三降到了萬(wàn)分之二,最優(yōu)值萬(wàn)一左右率統(tǒng)計(jì)方式次數(shù)。美團(tuán)外賣(mài)自年創(chuàng)建以來(lái),業(yè)務(wù)就以指數(shù)級(jí)的速度發(fā)展。目前美團(tuán)外賣(mài)日完成訂單量已突破萬(wàn),成為美團(tuán)點(diǎn)評(píng)最重要的業(yè)務(wù)之一。

面試中常常問(wèn)到的是Android的性能優(yōu)化以及Crash處理。 今天我們來(lái)學(xué)習(xí)一下啊美團(tuán)App的Crash處理。更多參考《Android性能優(yōu)化:手把手帶你全面實(shí)現(xiàn)內(nèi)存優(yōu)化》

原為地址: https://blog.csdn.net/Meituan...

Crash率是衡量一個(gè)App好壞的重要指標(biāo)之一,如果你忽略了它的存在,它就會(huì)愈演愈烈,最后造成大量用戶(hù)的流失,進(jìn)而給公司帶來(lái)無(wú)法估量的損失。本文講述美團(tuán)外賣(mài)Android客戶(hù)端團(tuán)隊(duì)在將App的Crash率從千分之三做到萬(wàn)分之二過(guò)程中所做的大量實(shí)踐工作,拋磚引玉,希望能夠?yàn)槠渌麍F(tuán)隊(duì)提供一些經(jīng)驗(yàn)和啟發(fā)。

面臨的挑戰(zhàn)和成果

面對(duì)用戶(hù)使用頻率高,外賣(mài)業(yè)務(wù)增長(zhǎng)快,Android碎片化嚴(yán)重這些問(wèn)題,美團(tuán)外賣(mài)Android App如何持續(xù)的降低Crash率,是一項(xiàng)極具挑戰(zhàn)的事情。通過(guò)團(tuán)隊(duì)的全力全策,美團(tuán)外賣(mài)Android App的平均Crash率從千分之三降到了萬(wàn)分之二,最優(yōu)值萬(wàn)一左右(Crash率統(tǒng)計(jì)方式:Crash次數(shù)/DAU)。

美團(tuán)外賣(mài)自2013年創(chuàng)建以來(lái),業(yè)務(wù)就以指數(shù)級(jí)的速度發(fā)展。美團(tuán)外賣(mài)承載的業(yè)務(wù),從單一的餐飲業(yè)務(wù),發(fā)展到餐飲、超市、生鮮、果蔬、藥品、鮮花、蛋糕、跑腿等十多個(gè)大品類(lèi)業(yè)務(wù)。目前美團(tuán)外賣(mài)日完成訂單量已突破2000萬(wàn),成為美團(tuán)點(diǎn)評(píng)最重要的業(yè)務(wù)之一。美團(tuán)外賣(mài)客戶(hù)端所承載的業(yè)務(wù)模塊越來(lái)越多,產(chǎn)品復(fù)雜度越來(lái)越高,團(tuán)隊(duì)開(kāi)發(fā)人員日益增加,這些都給App降低Crash率帶來(lái)了巨大的挑戰(zhàn)。

Crash的治理實(shí)踐

對(duì)于Crash的治理,我們盡量遵守以下三點(diǎn)原則:?

由點(diǎn)到面。一個(gè)Crash發(fā)生了,我們不能只針對(duì)這個(gè)Crash的去解決,而要去考慮這一類(lèi)Crash怎么去解決和預(yù)防。只有這樣才能使得這一類(lèi)Crash真正被解決。?

異常不能隨便吃掉。隨意的使用try-catch,只會(huì)增加業(yè)務(wù)的分支和隱蔽真正的問(wèn)題,要了解Crash的本質(zhì)原因,根據(jù)本質(zhì)原因去解決。catch的分支,更要根據(jù)業(yè)務(wù)場(chǎng)景去兜底,保證后續(xù)的流程正常。?

預(yù)防勝于治理。當(dāng)Crash發(fā)生的時(shí)候,損失已經(jīng)造成了,我們?cè)僭趺粗卫硪仓皇菧p少損失。盡可能的提前預(yù)防Crash的發(fā)生,可以將Crash消滅在萌芽階段。

常規(guī)的Crash治理

常規(guī)Crash發(fā)生的原因主要是由于開(kāi)發(fā)人員編寫(xiě)代碼不小心導(dǎo)致的。解決這類(lèi)Crash需要由點(diǎn)到面,根據(jù)Crash引發(fā)的原因和業(yè)務(wù)本身,統(tǒng)一集中解決。常見(jiàn)的Crash類(lèi)型包括:空節(jié)點(diǎn)、角標(biāo)越界、類(lèi)型轉(zhuǎn)換異常、實(shí)體對(duì)象沒(méi)有序列化、數(shù)字轉(zhuǎn)換異常、Activity或Service找不到等。這類(lèi)Crash是App中最為常見(jiàn)的Crash,也是最容易反復(fù)出現(xiàn)的。在獲取Crash堆棧信息后,解決這類(lèi)Crash一般比較簡(jiǎn)單,更多考慮的應(yīng)該是如何避免。下面介紹兩個(gè)我們治理的量比較大的Crash。

NullPointerException

NullPointerException是我們遇到最頻繁的,造成這種Crash一般有兩種情況:?

對(duì)象本身沒(méi)有進(jìn)行初始化就進(jìn)行操作。?

對(duì)象已經(jīng)初始化過(guò),但是被回收或者手動(dòng)置為null,然后對(duì)其進(jìn)行操作。

針對(duì)第一種情況導(dǎo)致的原因有很多,可能是開(kāi)發(fā)人員的失誤、API返回?cái)?shù)據(jù)解析異常、進(jìn)程被殺死后靜態(tài)變量沒(méi)初始化導(dǎo)致,我們可以做的有:?

對(duì)可能為空的對(duì)象做判空處理。?

養(yǎng)成使用@NonNull和@Nullable注解的習(xí)慣。?

盡量不使用靜態(tài)變量,萬(wàn)不得已使用SharedPreferences來(lái)存儲(chǔ)。?

考慮使用Kotlin語(yǔ)言。

針對(duì)第二種情況大部分是由于Activity/Fragment銷(xiāo)毀或被移除后,在Message、Runnable、網(wǎng)絡(luò)等回調(diào)中執(zhí)行了一些代碼導(dǎo)致的,我們可以做的有:?

Message、Runnable回調(diào)時(shí),判斷Activity/Fragment是否銷(xiāo)毀或被移除;加try-catch保護(hù);Activity/Fragment銷(xiāo)毀時(shí)移除所有已發(fā)送的Runnable。?

封裝LifecycleMessage/Runnable基礎(chǔ)組件,并自定義Lint檢查,提示使用封裝好的基礎(chǔ)組件。?

在BaseActivity、BaseFragment的onDestory()里把當(dāng)前Activity所發(fā)的所有請(qǐng)求取消掉。

IndexOutOfBoundsException

這類(lèi)Crash常見(jiàn)于對(duì)ListView的操作和多線程下對(duì)容器的操作。

針對(duì)ListView中造成的IndexOutOfBoundsException,經(jīng)常是因?yàn)橥獠恳渤钟辛薃dapter里數(shù)據(jù)的引用(如在Adapter的構(gòu)造函數(shù)里直接賦值),這時(shí)如果外部引用對(duì)數(shù)據(jù)更改了,但沒(méi)有及時(shí)調(diào)用notifyDataSetChanged(),則有可能造成Crash,對(duì)此我們封裝了一個(gè)BaseAdapter,數(shù)據(jù)統(tǒng)一由Adapter自己維護(hù)通知, 同時(shí)也極大的避免了The content of the adapter has changed but ListView did not receive a notification,這兩類(lèi)Crash目前得到了統(tǒng)一的解決。

另外,很多容器是線程不安全的,所以如果在多線程下對(duì)其操作就容易引發(fā)IndexOutOfBoundsException。常用的如JDK里的ArrayList和Android里的SparseArray、ArrayMap,同時(shí)也要注意有一些類(lèi)的內(nèi)部實(shí)現(xiàn)也是用的線程不安全的容器,如Bundle里用的就是ArrayMap。

系統(tǒng)級(jí)Crash治理

眾所周知,Android的機(jī)型眾多,碎片化嚴(yán)重,各個(gè)硬件廠商可能會(huì)定制自己的ROM,更改系統(tǒng)方法,導(dǎo)致特定機(jī)型的崩潰。發(fā)現(xiàn)這類(lèi)Crash,主要靠云測(cè)平臺(tái)配合自動(dòng)化測(cè)試,以及線上監(jiān)控,這種情況下的Crash堆棧信息很難直接定位問(wèn)題。下面是常見(jiàn)的解決思路:

嘗試找到造成Crash的可疑代碼,看是否有特異的API或者調(diào)用方式不當(dāng)導(dǎo)致的,嘗試修改代碼邏輯來(lái)進(jìn)行規(guī)避。

通過(guò)Hook來(lái)解決,Hook分為Java Hook和Native Hook。Java Hook主要靠反射或者動(dòng)態(tài)代理來(lái)更改相應(yīng)API的行為,需要嘗試找到可以Hook的點(diǎn),一般Hook的點(diǎn)多為靜態(tài)變量,同時(shí)需要注意Android不同版本的API,類(lèi)名、方法名和成員變量名都可能不一樣,所以要做好兼容工作;Native Hook原理上是用更改后方法把舊方法在內(nèi)存地址上進(jìn)行替換,需要考慮到Dalvik和ART的差異;相對(duì)來(lái)說(shuō)Native Hook的兼容性更差一點(diǎn),所以用Native Hook的時(shí)候需要配合降級(jí)策略。

如果通過(guò)前兩種方式都無(wú)法解決的話,我們只能?chē)L試反編譯ROM,尋找解決的辦法。

我們舉一個(gè)定制系統(tǒng)ROM導(dǎo)致Crash的例子,根據(jù)Crash平臺(tái)統(tǒng)計(jì)數(shù)據(jù)發(fā)現(xiàn)該Crash只發(fā)生在vivo V3Max這類(lèi)機(jī)型上,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)上對(duì)應(yīng)系統(tǒng)版本的AbsListView里并沒(méi)有UpdateBottomFlagTask類(lèi),因此可以斷定是vivo該版本定制的ROM修改了系統(tǒng)的實(shí)現(xiàn)。我們?cè)诙ㄎ贿@個(gè)Crash的可疑點(diǎn)無(wú)果后決定通過(guò)Hook的方式解決,通過(guò)源碼發(fā)現(xiàn)AsyncTask$SerialExecutor是靜態(tài)變量,是一個(gè)很好的Hook的點(diǎn),通過(guò)反射添加try-catch解決。因?yàn)樾薷牡氖莊inal對(duì)象所以需要先反射修改accessFlags,需要注意ART和Dalvik下對(duì)應(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)外賣(mài)App用上述方法解決了對(duì)應(yīng)的Crash,但是美團(tuán)App里的外賣(mài)頻道因?yàn)槠脚_(tái)的限制無(wú)法通過(guò)這種方式,于是我們嘗試反編譯ROM。?
Android ROM編譯時(shí)會(huì)將framework、app、bin等目錄打入system.img中,system.img是Android系統(tǒng)中用來(lái)存放系統(tǒng)文件的鏡像 (image),文件格式一般為yaffs2或ext。但Android 5.0開(kāi)始支持dm-verity后,system.img不再提供,而是提供了三個(gè)文件system.new.dat,system.patch.dat,system.transfer.list,因此我們首先需要通過(guò)上述的三個(gè)文件得到system.img。但我們將vivo ROM解壓后發(fā)現(xiàn)廠商將system.new.dat進(jìn)行了分片,如下圖所示:

經(jīng)過(guò)對(duì)system.transfer.list中的信息和system.new.dat 1 2 3 … 文件大小對(duì)比研究,發(fā)現(xiàn)一些共同點(diǎn),system.transfer.list中的每一個(gè)block數(shù)*4KB 與對(duì)應(yīng)的分片文件的大小大致相同,故大膽猜測(cè),vivo ROM對(duì)system.patch.dat分片也只是單純的按block先后順序進(jìn)行了分片處理。所以我們只需要在轉(zhuǎn)化img前將這些分片文件合成一個(gè)system.patch.dat文件就可以了。最后根據(jù)system.img的文件系統(tǒng)格式進(jìn)行解包,拿到framework目錄,其中有framework.jar和boot.oat等文件,因?yàn)锳ndroid4.4之后引入了ART虛擬機(jī),會(huì)預(yù)先把system/framework中的一些jar包轉(zhuǎn)換為oat格式,所以我們還需要將對(duì)應(yīng)的oat文件通過(guò)ota2dex將其解包獲得dex文件,之后通過(guò)dex2jar和jd-gui查看源碼。

OOM

OOM是OutOfMemoryError的簡(jiǎn)稱(chēng),在常見(jiàn)的Crash疑難排行榜上,OOM絕對(duì)可以名列前茅并且經(jīng)久不衰。因?yàn)樗l(fā)生時(shí)的Crash堆棧信息往往不是導(dǎo)致問(wèn)題的根本原因,而只是壓死駱駝的最后一根稻草。?
導(dǎo)致OOM的原因大部分如下:?

內(nèi)存泄漏,大量無(wú)用對(duì)象沒(méi)有被及時(shí)回收導(dǎo)致后續(xù)申請(qǐng)內(nèi)存失敗。?

大內(nèi)存對(duì)象過(guò)多,最常見(jiàn)的大對(duì)象就是Bitmap,幾個(gè)大圖同時(shí)加載很容易觸發(fā)OOM。

內(nèi)存泄漏?
內(nèi)存泄漏指系統(tǒng)未能及時(shí)釋放已經(jīng)不再使用的內(nèi)存對(duì)象,一般是由錯(cuò)誤的程序代碼邏輯引起的。在Android平臺(tái)上,最常見(jiàn)也是最嚴(yán)重的內(nèi)存泄漏就是Activity對(duì)象泄漏。Activity承載了App的整個(gè)界面功能,Activity的泄漏同時(shí)也意味著它持有的大量資源對(duì)象都無(wú)法被回收,極其容易造成OOM。?
常見(jiàn)的可能會(huì)造成Activity泄漏的原因有:?

匿名內(nèi)部類(lèi)實(shí)現(xiàn)Handler處理消息,可能導(dǎo)致隱式持有的Activity對(duì)象無(wú)法回收。?

Activity和Context對(duì)象被混淆和濫用,在許多只需要Application Context而不需要使用Activity對(duì)象的地方使用了Activity對(duì)象,比如注冊(cè)各類(lèi)Receiver、計(jì)算屏幕密度等等。?

View對(duì)象處理不當(dāng),使用Activity的LayoutInflater創(chuàng)建的View自身持有的Context對(duì)象其實(shí)就是Activity,這點(diǎn)經(jīng)常被忽略,在自己實(shí)現(xiàn)View重用等場(chǎng)景下也會(huì)導(dǎo)致Activity泄漏。

對(duì)于Activity泄漏,目前已經(jīng)有了一個(gè)非常好用的檢測(cè)工具:LeakCanary,它可以自動(dòng)檢測(cè)到所有Activity的泄漏情況,并且在發(fā)生泄漏時(shí)給出十分友好的界面提示,同時(shí)為了防止開(kāi)發(fā)人員的疏漏,我們也會(huì)將其上報(bào)到服務(wù)器,統(tǒng)一檢查解決。另外我們可以在debug下使用StrictMode來(lái)檢查Activity的泄露、Closeable對(duì)象沒(méi)有被關(guān)閉等問(wèn)題。

大對(duì)象?
在Android平臺(tái)上,我們分析任一應(yīng)用的內(nèi)存信息,幾乎都可以得出同樣的結(jié)論:占用內(nèi)存最多的對(duì)象大都是Bitmap對(duì)象。隨著手機(jī)屏幕尺寸越來(lái)越大,屏幕分辨率也越來(lái)越高,1080p和更高的2k屏已經(jīng)占了大半份額,為了達(dá)到更好的視覺(jué)效果,我們往往需要使用大量高清圖片,同時(shí)也為OOM埋下了禍根。?
對(duì)于圖片內(nèi)存優(yōu)化,我們有幾個(gè)常用的思路:?

盡量使用成熟的圖片庫(kù),比如Glide,圖片庫(kù)會(huì)提供很多通用方面的保障,減少不必要的人為失誤。?

根據(jù)實(shí)際需要,也就是View尺寸來(lái)加載圖片,可以在分辨率較低的機(jī)型上盡可能少地占用內(nèi)存。除了常用的BitmapFactory.Options#inSampleSize和Glide提供的BitmapRequestBuilder#override之外,我們的圖片CDN服務(wù)器也支持圖片的實(shí)時(shí)縮放,可以在服務(wù)端進(jìn)行圖片縮放處理,從而減輕客戶(hù)端的內(nèi)存壓力。?

分析App內(nèi)存的詳細(xì)情況是解決問(wèn)題的第一步,我們需要對(duì)App運(yùn)行時(shí)到底占用了多少內(nèi)存、哪些類(lèi)型的對(duì)象有多少個(gè)有大致了解,并根據(jù)實(shí)際情況做出預(yù)測(cè),這樣才能在分析時(shí)做到有的放矢。Android Studio也提供了非常好用的Memory Profiler,堆轉(zhuǎn)儲(chǔ)和分配跟蹤器功能可以幫我們迅速定位問(wèn)題。

AOP增強(qiáng)輔助

AOP是面向切面編程的簡(jiǎn)稱(chēng),在Android的Gradle插件1.5.0中新增了Transform API之后,編譯時(shí)修改字節(jié)碼來(lái)實(shí)現(xiàn)AOP也因?yàn)橛辛斯俜街С侄兊梅浅7奖恪?
在一些特定情況下,可以通過(guò)AOP的方式自動(dòng)處理未捕獲的異常:?

拋異常的方法非常明確,調(diào)用方式比較固定。?

異常處理方式比較統(tǒng)一。?

和業(yè)務(wù)邏輯無(wú)關(guān),即自動(dòng)處理異常后不會(huì)影響正常的業(yè)務(wù)邏輯。典型的例子有讀取Intent Extras參數(shù)、讀取SharedPreferences、解析顏色字符串值和顯示隱藏Window等等。

這類(lèi)問(wèn)題的解決原理大致相同,我們以Intent Extras為例詳細(xì)介紹一下。讀取Intent Extras的問(wèn)題在于我們非常常用的方法 Intent#getStringExtra 在代碼邏輯出錯(cuò)或者惡意攻擊的情況下可能會(huì)拋出ClassNotFoundException異常,而我們平時(shí)在寫(xiě)代碼時(shí)又不太可能給所有調(diào)用都加上try-catch語(yǔ)句,于是一個(gè)更安全的Intent工具類(lèi)應(yīng)運(yùn)而生,理論上只要所有人都使用這個(gè)工具類(lèi)來(lái)訪問(wèn)Intent Extras參數(shù)就可以防止此類(lèi)型的Crash。但是面對(duì)龐大的舊代碼倉(cāng)庫(kù)和諸多的業(yè)務(wù)部門(mén),修改現(xiàn)有代碼需要極大成本,還有更多的外部依賴(lài)SDK基本不可能使用我們自己的工具類(lèi),此時(shí)就需要AOP大展身手了。?
我們專(zhuān)門(mén)制作了一個(gè)Gradle插件,只需要配置一下參數(shù)就可以將某個(gè)特定方法的調(diào)用替換成另一個(gè)方法:

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代碼(包括第三方庫(kù))里所有的Intent.getXXXExtra調(diào)用替換成IntentUtil類(lèi)中的安全版實(shí)現(xiàn)。當(dāng)然,并不是所有的異常都只需要catch住就萬(wàn)事大吉,如果真的有邏輯錯(cuò)誤肯定需要在開(kāi)發(fā)和測(cè)試階段及時(shí)暴露出來(lái),所以在IntentUtil中會(huì)對(duì)App的運(yùn)行環(huán)境做判斷,Debug下會(huì)將異常直接拋出,開(kāi)發(fā)同學(xué)可以根據(jù)Crash堆棧分析問(wèn)題,Release環(huán)境下則在捕獲到異常時(shí)返回對(duì)應(yīng)的默認(rèn)值然后將異常上報(bào)到服務(wù)器。

依賴(lài)庫(kù)的問(wèn)題

Android App經(jīng)常會(huì)依賴(lài)很多AAR, 每個(gè)AAR可能有多個(gè)版本,打包時(shí)Gradle會(huì)根據(jù)規(guī)則確定使用的最終版本號(hào)(默認(rèn)選擇最高版本或者強(qiáng)制指定的版本),而其他版本的AAR將被丟棄。如果互相依賴(lài)的AAR中有不兼容的版本,存在的問(wèn)題在打包時(shí)是不能發(fā)現(xiàn)的,只有在相關(guān)代碼執(zhí)行時(shí)才會(huì)出現(xiàn),會(huì)造成NoClassDefFoundError、NoSuchFieldError、NoSuchMethodError等異常。如圖所示,order和store兩個(gè)業(yè)務(wù)庫(kù)都依賴(lài)了platform.aar,一個(gè)是1.0版本,一個(gè)是2.0版本,默認(rèn)最終打進(jìn)APK的只有platform 2.0版本,這時(shí)如果order庫(kù)里用到的platform庫(kù)里的某個(gè)類(lèi)或者方法在2.0版本中被刪除了,運(yùn)行時(shí)就可能發(fā)生異常,雖然SDK在升級(jí)時(shí)會(huì)盡量做到向下兼容,但很多時(shí)候尤其是第三方SDK是沒(méi)法得到保證的,在美團(tuán)外賣(mài)Android App v6.0版本時(shí)因?yàn)檫@個(gè)原因?qū)е聼嵝迯?fù)功能喪失,因此為了提前發(fā)現(xiàn)問(wèn)題,我們接入了依賴(lài)檢查插件Defensor。

Defensor在編譯時(shí)通過(guò)DexTask獲取到所有的輸入文件(也就是被編譯過(guò)的class文件),然后檢查每個(gè)文件里引用的類(lèi)、字段、方法等是否存在。

除此之外我們寫(xiě)了一個(gè)Gradle插件SVD(strict version dependencies)來(lái)對(duì)那些重要的SDK的版本進(jìn)行統(tǒng)一管理。插件會(huì)在編譯時(shí)檢查Gradle最終使用的SDK版本是否和配置中的一致,如果不一致插件會(huì)終止編譯并報(bào)錯(cuò),并同時(shí)會(huì)打印出發(fā)生沖突的SDK的所有依賴(lài)關(guān)系。

Crash的預(yù)防實(shí)踐

單純的靠約定或規(guī)范去減少Crash的發(fā)生是不現(xiàn)實(shí)的。約定和規(guī)范受限于組織架構(gòu)和具體執(zhí)行的個(gè)人,很容易被忽略,只有靠工程架構(gòu)和工具才能保證Crash的預(yù)防長(zhǎng)久的執(zhí)行下去。

工程架構(gòu)對(duì)Crash率的影響

在治理Crash的實(shí)踐中,我們往往忽略了工程架構(gòu)對(duì)Crash率的影響。Crash的發(fā)生大部分原因是源于程序員的不合理的代碼,而程序員工作中最直接的接觸的就是工程架構(gòu)。對(duì)于一個(gè)邊界模糊,層級(jí)混亂的架構(gòu),程序員是更加容易寫(xiě)出引起Crash的代碼。在這樣的架構(gòu)里面,即使程序員意識(shí)到導(dǎo)致某種寫(xiě)法存在問(wèn)題,想要去改善這樣不合理的代碼,也是非常困難的。相反,一個(gè)層級(jí)清晰,邊界明確的架構(gòu),是能夠大大減少Crash發(fā)生的概率,治理和預(yù)防Crash也是相對(duì)更容易。這里我們可以舉幾個(gè)我們實(shí)踐過(guò)的例子闡述。

業(yè)務(wù)模塊的劃分?
原來(lái)我們的Crash基本上都是由個(gè)別同學(xué)關(guān)注解決的,團(tuán)隊(duì)里的每個(gè)同學(xué)都會(huì)提交可能引起Crash的代碼,如果負(fù)責(zé)Crash的同學(xué)因?yàn)槟承┦虑椋瑫簳r(shí)沒(méi)有關(guān)注App的Crash率,那么造成Crash的同學(xué)也不會(huì)知道他的代碼引起了Crash。

對(duì)于這個(gè)問(wèn)題,我們的做法是App的業(yè)務(wù)模塊化。業(yè)務(wù)模塊化后,每個(gè)業(yè)務(wù)都有都有唯一包名和對(duì)應(yīng)的負(fù)責(zé)人。當(dāng)某個(gè)模塊發(fā)生了Crash,可以根據(jù)包名提交問(wèn)題給這個(gè)模塊的負(fù)責(zé)人,讓他第一時(shí)間進(jìn)行處理。業(yè)務(wù)模塊化本身也是工程架構(gòu)優(yōu)先需要考慮的事情之一。

頁(yè)面跳轉(zhuǎn)路由統(tǒng)一處理頁(yè)面跳轉(zhuǎn)?
對(duì)外賣(mài)App而言,使用過(guò)程中最多的就是頁(yè)面間的跳轉(zhuǎn),而頁(yè)面間跳轉(zhuǎn)經(jīng)常會(huì)造成ActivityNotFoundException,例如我們配了一個(gè)scheme,但對(duì)方的scheme路徑已經(jīng)發(fā)生了變化;又例如,我們調(diào)用手機(jī)上相冊(cè)的功能,而相冊(cè)應(yīng)用已被用戶(hù)自己禁用或移除了。解決這一類(lèi)Crash,其實(shí)也很簡(jiǎn)單,只需要在startActivity增加ActivityNotFoundException異常捕獲即可。但一個(gè)App里,啟動(dòng)Activity的地方,幾乎是隨處可見(jiàn),無(wú)法預(yù)測(cè)哪一處會(huì)造成ActivityNotFoundException。?
我們的做法是將頁(yè)面的跳轉(zhuǎn),都通過(guò)我們封裝的scheme路由去分發(fā)。這樣的好處是,通過(guò)scheme路由,在工程架構(gòu)上所有業(yè)務(wù)都是解耦,模塊間不需要相互依賴(lài)就可以實(shí)現(xiàn)頁(yè)面的跳轉(zhuǎn)和基本類(lèi)型參數(shù)的傳遞;同時(shí),由于所有的頁(yè)面跳轉(zhuǎn)都會(huì)走scheme路由,我們只需要在scheme路由里一處加上ActivityNotFoundException異常捕獲即可解決這種類(lèi)型的Crash。路由設(shè)計(jì)示意圖如下:

網(wǎng)絡(luò)層統(tǒng)一處理API臟數(shù)據(jù)?
客戶(hù)端的很大一部分的Crash是因?yàn)锳PI返回的臟數(shù)據(jù)。比如當(dāng)API返回空值、空數(shù)組或返回不是約定類(lèi)型的數(shù)據(jù),App收到這些數(shù)據(jù),就極有可能發(fā)生空指針、數(shù)組越界和類(lèi)型轉(zhuǎn)換錯(cuò)誤等Crash。而且這樣的臟數(shù)據(jù),特別容易引起線上大面積的崩潰。?
最早我們的工程的網(wǎng)絡(luò)層用法是:頁(yè)面監(jiān)聽(tīng)網(wǎng)絡(luò)成功和失敗的回調(diào),網(wǎng)絡(luò)成功后,將JSON數(shù)據(jù)傳遞給頁(yè)面,頁(yè)面解析Model,初始化View,如圖所示。這樣的問(wèn)題就是,網(wǎng)絡(luò)雖然請(qǐng)求成功了,但是JSON解析Model這個(gè)過(guò)程可能存在問(wèn)題,例如沒(méi)有返回?cái)?shù)據(jù)或者返回了類(lèi)型不對(duì)的數(shù)據(jù),而這個(gè)臟數(shù)據(jù)導(dǎo)致問(wèn)題會(huì)出現(xiàn)在UI層,直接反應(yīng)給用戶(hù)。

根據(jù)上圖,我們可以看到由于網(wǎng)絡(luò)層只承擔(dān)了請(qǐng)求網(wǎng)絡(luò)的職責(zé),沒(méi)有承擔(dān)數(shù)據(jù)解析的職責(zé),數(shù)據(jù)解析的職責(zé)交給了頁(yè)面去處理。這樣使得我們一旦發(fā)現(xiàn)臟數(shù)據(jù)導(dǎo)致的Crash,就只能在網(wǎng)絡(luò)請(qǐng)求的回調(diào)里面增加各種判斷去兼容臟數(shù)據(jù)。我們有幾百個(gè)頁(yè)面,補(bǔ)漏完全補(bǔ)不過(guò)來(lái)。通過(guò)幾個(gè)版本的重構(gòu),我們重新劃分了網(wǎng)絡(luò)層的職責(zé),如圖所示:

從圖上可以看出,重構(gòu)后的網(wǎng)絡(luò)層負(fù)責(zé)請(qǐng)求網(wǎng)絡(luò)和數(shù)據(jù)解析,如果存在臟數(shù)據(jù)的話,在網(wǎng)絡(luò)層就會(huì)發(fā)現(xiàn)問(wèn)題,不會(huì)影響到UI層,返回給UI層的都是校驗(yàn)成功的數(shù)據(jù)。這樣改造后,我們發(fā)現(xiàn)這類(lèi)的Crash率有了極大的改善。

大圖監(jiān)控

上面講到大對(duì)象是導(dǎo)致OOM的主要原因之一,而B(niǎo)itmap是App里最常見(jiàn)的大對(duì)象類(lèi)型,因此對(duì)占用內(nèi)存過(guò)大的Bitmap對(duì)象的監(jiān)控就很有必要了。?
我們用AOP方式Hook了三種常見(jiàn)圖片庫(kù)的加載圖片回調(diào)方法,同時(shí)監(jiān)控圖片庫(kù)加載圖片時(shí)的兩個(gè)維度:?
1. 加載圖片使用的URL。外賣(mài)App中除靜態(tài)資源外,所有圖片都要求發(fā)布到專(zhuān)用的圖片CDN服務(wù)器上,加載圖片時(shí)使用正則表達(dá)式匹配URL,除了限定CDN域名之外還要求所有圖片加載時(shí)都要添加對(duì)應(yīng)的動(dòng)態(tài)縮放參數(shù)。?
2. 最終加載出的圖片結(jié)果(也就是Bitmap對(duì)象)。我們知道Bitmap對(duì)象所占內(nèi)存和其分辨率大小成正比,而一般情況下在ImageView上設(shè)置超過(guò)自身尺寸的圖片是沒(méi)有意義的,所以我們要求顯示在ImageView中的Bitmap分辨率不允許超過(guò)View自身的尺寸(為了降低誤報(bào)率也可以設(shè)定一個(gè)報(bào)警閾值)。

開(kāi)發(fā)過(guò)程中,在App里檢測(cè)到不合規(guī)的圖片時(shí)會(huì)立即高亮出錯(cuò)的ImageView所在的位置并彈出對(duì)話框提示ImageView所在的Activity、XPath和加載圖片使用的URL等信息,如下圖,輔助開(kāi)發(fā)同學(xué)定位并解決問(wèn)題。在Release環(huán)境下可以將報(bào)警信息上報(bào)到服務(wù)器,實(shí)時(shí)觀察數(shù)據(jù),有問(wèn)題及時(shí)處理。?

Lint檢查

我們發(fā)現(xiàn)線上的很多Crash其實(shí)可以在開(kāi)發(fā)過(guò)程中通過(guò)Lint檢查來(lái)避免。Lint是Google提供的Android靜態(tài)代碼檢查工具,可以掃描并發(fā)現(xiàn)代碼中潛在的問(wèn)題,提醒開(kāi)發(fā)人員及早修正,提高代碼質(zhì)量。

但是Android原生提供的Lint規(guī)則(如是否使用了高版本API)遠(yuǎn)遠(yuǎn)不夠,缺少一些我們認(rèn)為有必要的檢測(cè),也不能檢查代碼規(guī)范。因此我們開(kāi)始開(kāi)發(fā)自定義Lint,目前我們通過(guò)自定義Lint規(guī)則已經(jīng)實(shí)現(xiàn)了Crash預(yù)防、Bug預(yù)防、提升性能/安全和代碼規(guī)范檢查這些功能。如檢查實(shí)現(xiàn)了Serializable接口的類(lèi),其成員變量(包括從父類(lèi)繼承的)所聲明的類(lèi)型都要實(shí)現(xiàn)Serializable接口,可以有效的避免NotSerializableException;強(qiáng)制使用封裝好的工具類(lèi)如ColorUtil、WindowUtil等可以有效的避免因?yàn)閰?shù)不正確產(chǎn)生的IllegalArgumentException和因?yàn)锳ctivity已經(jīng)finish導(dǎo)致的BadTokenException。

Lint檢查可以在多個(gè)階段執(zhí)行,包括在本地手動(dòng)檢查、編碼實(shí)時(shí)檢查、編譯時(shí)檢查、commit時(shí)檢查,以及在CI系統(tǒng)中提Pull Request時(shí)檢查、打包時(shí)檢查等,如下圖所示。更詳細(xì)的內(nèi)容可參考《美團(tuán)外賣(mài)Android Lint代碼檢查實(shí)踐》。

資源重復(fù)檢查

在之前的文章《美團(tuán)外賣(mài)Android平臺(tái)化架構(gòu)演進(jìn)實(shí)踐》中講述了我們的平臺(tái)化演進(jìn)過(guò)程,在這個(gè)過(guò)程中大家很大的一部分工作是下沉,但是下沉不完全就會(huì)導(dǎo)致一些類(lèi)和資源的重復(fù),類(lèi)因?yàn)橛邪南拗撇粫?huì)出現(xiàn)問(wèn)題。但是一些資源文件如layout、drawable等如果同名則下層會(huì)被上層覆蓋,這時(shí)layout里view的id發(fā)生了變化就可能導(dǎo)致空指針的問(wèn)題。為了避免這種問(wèn)題,我們寫(xiě)了一個(gè)Gradle插件通過(guò)hook MergeResource這個(gè)Task,拿到所有l(wèi)ibrary和主庫(kù)的資源文件,如果檢查到重復(fù)則會(huì)中斷編譯過(guò)程,輸出重復(fù)的資源名及對(duì)應(yīng)的library name,同時(shí)避免有些資源因?yàn)闃邮降仍虼_實(shí)需要覆蓋,因此我們?cè)O(shè)置了白名單。同時(shí)在這個(gè)過(guò)程中我們也拿到了所有的的圖片資源,可以順手做圖片大小的本地監(jiān)控,如下圖所示:?

Crash的監(jiān)控&止損的實(shí)踐 監(jiān)控

在經(jīng)過(guò)前面提到的各種檢查和測(cè)試之后,應(yīng)用便開(kāi)始發(fā)布了。我們建立了如下圖的監(jiān)控流程,來(lái)保證異常發(fā)生時(shí)能夠及時(shí)得到反饋并處理。首先是灰度監(jiān)控,灰度階段是增量Crash最容易暴露的階段,如果這個(gè)階段沒(méi)有很好的把握住,會(huì)使得增量變存量,從而導(dǎo)致Crash率上升。如果條件允許的話,可以在灰度期間制定一些灰度策略去提高這個(gè)階段Crash的暴露。例如分渠道灰度、分城市灰度、分業(yè)務(wù)場(chǎng)景灰度、新裝用戶(hù)的灰度等等,盡量覆蓋所有的分支?;叶冉Y(jié)束之后便開(kāi)始全量,在全量的過(guò)程中我們還需要一些日常Crash監(jiān)控和Crash率的異常報(bào)警來(lái)防止突發(fā)情況的發(fā)生,例如因?yàn)楹笈_(tái)上線或者運(yùn)營(yíng)配置錯(cuò)誤導(dǎo)致的線上Crash。除此之外還需要一些其他的監(jiān)控,例如,之前提到的大圖監(jiān)控,來(lái)避免因?yàn)榇髨D導(dǎo)致的OOM。具體的輸出形式主要有郵件通知、IM通知、報(bào)表。

止損

盡管我們?cè)谇懊孀隽四敲炊啵荂rash還是無(wú)法避免的,例如,在灰度階段因?yàn)榱考?jí)不夠,有些Crash沒(méi)有被暴露出來(lái);又或者某些功能客戶(hù)端比后臺(tái)更早上線,而這些功能在灰度階段沒(méi)有被覆蓋到;這些情況下,如果出現(xiàn)問(wèn)題就需要考慮如何止損了。

問(wèn)題發(fā)生時(shí)首先需要評(píng)估重要性,如果問(wèn)題不是很?chē)?yán)重而且修復(fù)成本較高可以考慮在下個(gè)版本再修復(fù),相反如果問(wèn)題比較嚴(yán)重,對(duì)用戶(hù)體驗(yàn)或下單有影響時(shí)就必須要修復(fù)。修復(fù)時(shí)首先考慮業(yè)務(wù)降級(jí),主要看該部分異常的業(yè)務(wù)是否有兜底或者A/B策略,這樣是最穩(wěn)妥也是最有效的方式。如果業(yè)務(wù)不能降級(jí)就需要考慮熱修復(fù)了,目前美團(tuán)外賣(mài)Android App接入的熱修復(fù)框架是自研的Robust,可以修復(fù)90%以上的場(chǎng)景,熱修成功率也達(dá)到了99%以上。如果問(wèn)題發(fā)生在熱修復(fù)無(wú)法覆蓋的場(chǎng)景,就只能強(qiáng)制用戶(hù)升級(jí)。強(qiáng)制升級(jí)因?yàn)楦采w周期長(zhǎng),同時(shí)影響用戶(hù)的體驗(yàn),只在萬(wàn)不得已的情況下才會(huì)使用。

展望 Crash的自我修復(fù)

我們?cè)谧鲂录夹g(shù)選型時(shí)除了要考慮是否能滿(mǎn)足業(yè)務(wù)需求、是否比現(xiàn)有技術(shù)更優(yōu)秀和團(tuán)隊(duì)學(xué)習(xí)成本等因素之外,兼容性和穩(wěn)定性也非常重要。但面對(duì)國(guó)內(nèi)非富多彩的Android系統(tǒng)環(huán)境,在體量百萬(wàn)級(jí)以上的的App中幾乎不可能實(shí)現(xiàn)毫無(wú)瑕疵的技術(shù)方案和組件,所以一般情況下如果某個(gè)技術(shù)實(shí)現(xiàn)方案可以達(dá)到0.01‰以下的崩潰率,而其他方案也沒(méi)有更好的表現(xiàn),我們就認(rèn)為它是可以接受的。但是哪怕僅僅十萬(wàn)分之一的崩潰率,也代表還有用戶(hù)受到影響,而我們認(rèn)為Crash對(duì)用戶(hù)來(lái)說(shuō)是最糟糕的體驗(yàn),尤其是涉及到交易的場(chǎng)景,所以我們必須本著每一單都很重要的原則,盡最大努力保證用戶(hù)順利執(zhí)行流程。

實(shí)際情況中有一些技術(shù)方案在兼容性和穩(wěn)定性上做了一定妥協(xié)的場(chǎng)景,往往是因?yàn)榭紤]到性能或擴(kuò)展性等方面的優(yōu)勢(shì)。這種情況下我們其實(shí)可以再多做一些,進(jìn)一步提高App的可用性。就像很多操作系統(tǒng)都有“兼容模式”或者“安全模式”,很多自動(dòng)化機(jī)械機(jī)器都配套有手動(dòng)操作模式一樣,App里也可以實(shí)現(xiàn)備用的降級(jí)方案,然后設(shè)置特定條件的觸發(fā)策略,從而達(dá)到自動(dòng)修復(fù)Crash的目的。

舉例來(lái)講,Android 3.0中引入了硬件加速機(jī)制,雖然可以提高繪制幀率并且降低CPU占用率,但是在某些機(jī)型上還是會(huì)有繪制錯(cuò)亂甚至Crash的情況,這時(shí)我們就可以在App中記錄硬件加速相關(guān)的Crash問(wèn)題或者使用檢測(cè)代碼主動(dòng)檢測(cè)硬件加速功能是否正常工作,然后主動(dòng)選擇是否開(kāi)啟硬件加速,這樣既可以讓絕大部分用戶(hù)享受硬件加速帶來(lái)的優(yōu)勢(shì),也可以保障硬件加速功能不完善的機(jī)型不受影響。?
還有一些類(lèi)似的可以做自動(dòng)降級(jí)的場(chǎng)景,比如:?

部分使用JNI實(shí)現(xiàn)的模塊,在SO加載失敗或者運(yùn)行時(shí)發(fā)生異常則可以降級(jí)為Java版實(shí)現(xiàn)。?

RenderScript實(shí)現(xiàn)的圖片模糊效果,也可以在失敗后降級(jí)為普通的Java版高斯模糊算法。?

在使用Retrofit網(wǎng)絡(luò)庫(kù)時(shí)發(fā)現(xiàn)OkHttp3或者HttpURLConnection網(wǎng)絡(luò)通道失敗率高,可以主動(dòng)切換到另一種通道。

這類(lèi)問(wèn)題都需要根據(jù)具體情況具體分析,如果可以找到準(zhǔn)確的判定條件和穩(wěn)定的修復(fù)方案,就可以讓App穩(wěn)定性再上一個(gè)臺(tái)階。

特定Crash類(lèi)型日志自動(dòng)回?fù)?/b>

外賣(mài)業(yè)務(wù)發(fā)展迅速,即使我們?cè)陂_(kāi)發(fā)時(shí)使用各種工具、措施來(lái)避免Crash的發(fā)生,但Crash還是不可避免。線上某些怪異的Crash發(fā)生后,我們除了分析Crash堆棧信息之外,還可以使用離線日志回?fù)啤⑾掳l(fā)動(dòng)態(tài)日志等工具來(lái)還原Crash發(fā)生時(shí)的場(chǎng)景,幫助開(kāi)發(fā)同學(xué)定位問(wèn)題,但是這兩種方式都有它們各自的問(wèn)題。離線日志顧名思義,它的內(nèi)容都是預(yù)先記錄好的,有時(shí)候可能會(huì)漏掉一些關(guān)鍵信息,因?yàn)樵诖a中加日志一般只是在業(yè)務(wù)關(guān)鍵點(diǎn),在大量的普通方法中不可能都加上日志。動(dòng)態(tài)日志(Holmes)存在的問(wèn)題是每次下發(fā)只能針對(duì)已知UUID的一個(gè)用戶(hù)的一臺(tái)設(shè)備,對(duì)于大量線上Crash的情況這種操作并不合適,因?yàn)槲覀儾⒉荒苤滥膫€(gè)發(fā)生Crash的用戶(hù)還會(huì)再次復(fù)現(xiàn)這次操作,下發(fā)配置充滿(mǎn)了不確定性。

我們可以改造Holmes使其支持批量甚至全量下發(fā)動(dòng)態(tài)日志,記錄的日志等到發(fā)生特定類(lèi)型的Crash時(shí)才上報(bào),這樣一來(lái)可以減少日志服務(wù)器壓力,同時(shí)也可以極大提高定位問(wèn)題的效率,因?yàn)槲覀兛梢源_定上報(bào)日志的設(shè)備最后都真正發(fā)生了該類(lèi)型Crash,再來(lái)分析日志就可以做到事半功倍。

總結(jié)

業(yè)務(wù)的快速發(fā)展,往往不可能給團(tuán)隊(duì)充足的時(shí)間去治理Crash,而Crash又是App最重要的指標(biāo)之一。團(tuán)隊(duì)需要由一個(gè)個(gè)Crash個(gè)例,去探究每一個(gè)Crash發(fā)生的最本質(zhì)原因,找到最合理解決這類(lèi)Crash的方案,建立解決這一類(lèi)Crash的長(zhǎng)效機(jī)制,而不能飲鴆止渴。只有這樣,隨著版本的不斷迭代,我們才能在Crash治理之路上離目標(biāo)越來(lái)越近。

參考資料

Crash率從2.2%降至0.2%,這個(gè)團(tuán)隊(duì)是怎么做到的?

Android運(yùn)行時(shí)ART加載OAT文件的過(guò)程分析

Android動(dòng)態(tài)日志系統(tǒng)Holmes

Android Hook技術(shù)防范漫談

美團(tuán)外賣(mài)Android Lint代碼檢查實(shí)踐

面試必備之UI刷新大解密

Flutter基礎(chǔ)-環(huán)境搭建及demo運(yùn)行

我的Android重構(gòu)之旅:框架篇

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

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/71378.html

相關(guān)文章

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<