深度解析Android穩(wěn)定性優(yōu)化(全文較長罚屋,建議收藏)

成為一名優(yōu)秀的Android開發(fā),需要一份完備的知識體系印蔗,在這里户辫,讓我們一起成長為自己所想的那樣~陌凳。

一、正確認(rèn)識

首先沐飘,我們必須對App的穩(wěn)定性有正確的認(rèn)識,它是App質(zhì)量構(gòu)建體系中最基本和最關(guān)鍵的一環(huán)牲迫。如果我們的App不穩(wěn)定耐朴,并且經(jīng)常不能正常地提供服務(wù),那么用戶大概率會卸載掉它盹憎。所以穩(wěn)定性很重要筛峭,并且Crash是P0優(yōu)先級,需要優(yōu)先解決陪每。 而且镰吵,穩(wěn)定性可優(yōu)化的面很廣疤祭,它不僅僅只包含Crash這一部分勺馆,也包括卡頓谓传、耗電等優(yōu)化范疇续挟。

1侥衬、穩(wěn)定性緯度

應(yīng)用的穩(wěn)定性可以分為三個緯度轴总,如下所示:

  • 1怀樟、Crash緯度:最重要的指標(biāo)就是應(yīng)用的Crash率往堡。
  • 2、性能緯度:包括啟動速度吨瞎、內(nèi)存穆咐、繪制等等優(yōu)化方向对湃,相對于Crash來說是次要的,在做應(yīng)用性能體系化建設(shè)之前归露,我們必須要確保應(yīng)用的功能穩(wěn)定可用剧包。
  • 3疆液、業(yè)務(wù)高可用緯度:它是非常關(guān)鍵的一步堕油,我們需要采用多種手段來保證我們App的主流程以及核心路徑的穩(wěn)定性掉缺,只有用戶經(jīng)常使用我們的App眶明,它才有可能發(fā)現(xiàn)別的方面的問題搜囱。

2蜀肘、穩(wěn)定性優(yōu)化注意事項

我們在做應(yīng)用的穩(wěn)定性優(yōu)化的時候,需要注意三個要點扮宠,如下所示:

1西乖、重在預(yù)防、監(jiān)控必不可少

對于穩(wěn)定性來說浴栽,如果App已經(jīng)到了線上才發(fā)現(xiàn)異常,那其實已經(jīng)造成了損失,所以被廓,對于穩(wěn)定性的優(yōu)化坏晦,其重點在于預(yù)防。從開發(fā)同學(xué)的編碼環(huán)節(jié)昆婿,到測試同學(xué)的測試環(huán)節(jié),以及到上線前的發(fā)布環(huán)節(jié)蜓斧、上線后的運維環(huán)節(jié)仓蛆,這些環(huán)節(jié)都需要來預(yù)防異常情況的發(fā)生挎春。如果異常真的發(fā)生了看疙,也需要將想方設(shè)法將損失降到最低能庆,爭取用最小的代價來暴露盡可能多的問題。

此外渠旁,監(jiān)控也是必不可少的一步粤铭,預(yù)防做的再好,到了線上投慈,總會有各種各樣的異常發(fā)生承耿。所以,無論如何伪煤,我們都需要有全面的監(jiān)控手段來更加靈敏地發(fā)現(xiàn)問題加袋。

2、思考更深一層抱既、重視隱含信息:如解決Crash問題時思考是否會引發(fā)同一類問題

當(dāng)我們看到了一個Crash的時候职烧,不能簡單地只處理這一個Crash,而是需要思考更深一層防泵,要考慮會不會在其它地方會有一樣的Crash類型發(fā)生蚀之。如果有這樣的情況,我們必須對其統(tǒng)一處理和預(yù)防捷泞。

此外足删,我們還要關(guān)注Crash相關(guān)的隱含信息,比如锁右,在面試過程當(dāng)中失受,面試官問你,你們應(yīng)用的Crash率是多少咏瑟,這個問題表明上問的是Crash率拂到,但是實際上它是問你一些隱含信息的,過高的Crash率就代表開發(fā)人員的水平不行码泞,leader的架構(gòu)能力不行兄旬,項目的各個階段中優(yōu)化的空間非常大,這樣一來余寥,面試官對你的印象和評價也不會好领铐。

3悯森、長效保持需要科學(xué)流程

應(yīng)用穩(wěn)定性的建設(shè)過程是一個細(xì)活,所以很容易出現(xiàn)這個版本優(yōu)化好了罐孝,但是在接下來的版本中如果我們不管它呐馆,它就會發(fā)生持續(xù)惡化的情況,因此莲兢,我們必須從項目研發(fā)的每一個流程入手汹来,建立科學(xué)完善的相關(guān)規(guī)范,才能保證長效的優(yōu)化效果改艇。

3收班、Crash相關(guān)指標(biāo)

要對應(yīng)用的穩(wěn)定性進行優(yōu)化,我們就必須先了解與Crash相關(guān)的一些指標(biāo)谒兄。

1摔桦、UV、PV

  • PV(Page View):訪問量
  • UV(Unique Visitor):獨立訪客承疲,0 - 24小時內(nèi)的同一終端只計算一次

2邻耕、UV、PV燕鸽、啟動兄世、增量、存量 Crash率

  • UV Crash率(Crash UV / DAU):針對用戶使用量的統(tǒng)計啊研,統(tǒng)計一段時間內(nèi)所有用戶發(fā)生崩潰的占比御滩,用于評估Crash率的影響范圍,結(jié)合PV党远。需要注意的是削解,需要確保一直使用同一種衡量方式。
  • PV Crash率:評估相關(guān)Crash影響的嚴(yán)重程度沟娱。
  • 啟動Crash率:啟動階段氛驮,用戶還沒有完全打開App而發(fā)生的Crash,它是影響最嚴(yán)重的Crash济似,對用戶傷害最大矫废,無法通過熱修復(fù)拯救,需結(jié)合客戶端容災(zāi)碱屁,以進行App的自主修復(fù)。(這塊后面會講)
  • 增量蛾找、存量Crash率:增量Crash是指的新增的Crash娩脾,而存量Crash則表示一些歷史遺留bug。增量Crash是新版本重點打毛,存量Crash是需要持續(xù)啃的硬骨頭柿赊,我們需要優(yōu)先解決增量俩功、持續(xù)跟進存量問題

4碰声、Crash率評價

那么诡蜓,我們App的Crash率降低多少才能算是一個正常水平或優(yōu)秀的水平呢?

  • Java與Native的總崩潰率必須在千分之二以下胰挑。
  • Crash率萬分位為優(yōu)秀:需要注意90%的Crash都是比較容易解決的蔓罚,但是要解決最后的10%需要付出巨大的努力。

5瞻颂、Crash關(guān)鍵問題

這里我們還需要關(guān)注Crash相關(guān)的關(guān)鍵問題豺谈,如果應(yīng)用發(fā)生了Crash,我們應(yīng)該盡可能還原Crash現(xiàn)場贡这。因此茬末,我們需要全面地采集應(yīng)用發(fā)生Crash時的相關(guān)信息,如下所示:

  • 堆棧盖矫、設(shè)備丽惭、OS版本、進程辈双、線程名责掏、Logcat
  • 前后臺、使用時長辐马、App版本拷橘、小版本、渠道
  • CPU架構(gòu)喜爷、內(nèi)存信息冗疮、線程數(shù)、資源包信息檩帐、用戶行為日志

接著术幔,采集完上述信息并上報到后臺后,我們會在APM后臺進行聚合展示湃密,具體的展示信息如下所示:

  • Crash現(xiàn)場信息
  • Crash Top機型诅挑、OS版本、分布版本泛源、區(qū)域
  • Crash起始版本拔妥、上報趨勢、是否新增达箍、持續(xù)没龙、量級

最后,我們可以根據(jù)以上信息決定Crash是否需要立馬解決以及在哪個版本進行解決,關(guān)于APM聚合展示這塊可以參考 Bugly平臺 的APM后臺聚合展示硬纤。

然后解滓,我們再來看看與Crash相關(guān)的整體架構(gòu)。

6筝家、APM Crash部分整體架構(gòu)

APM Crash部分的整體架構(gòu)從上至下分為采集層洼裤、處理層、展示層溪王、報警層腮鞍。下面,我們來詳細(xì)講解一下每一層所做的處理在扰。

1)缕减、采集層

首先,我們需要在采集層這一層去獲取足夠多的Crash相關(guān)信息芒珠,以確保能夠精確定位到問題桥狡。需要采集的信息主要為如下幾種:

  • 錯誤堆棧
  • 設(shè)備信息
  • 行為日志
  • 其它信息

2)、處理層

然后皱卓,在處理層裹芝,我們會對App采集到的數(shù)據(jù)進行處理。

  • 數(shù)據(jù)清洗:將一些不符合條件的數(shù)據(jù)過濾掉娜汁,比如說嫂易,因為一些特殊情況,一些App采集到的數(shù)據(jù)不完整掐禁,或者由于上傳數(shù)據(jù)失敗而導(dǎo)致的數(shù)據(jù)不完整怜械,這些數(shù)據(jù)在APM平臺上肯定是無法全面地展示的,所以傅事,首先我們需要把這些信息進行過濾缕允。
  • 數(shù)據(jù)聚合:在這一層,我們會把Crash相關(guān)的數(shù)據(jù)進行聚合蹭越。
  • 緯度分類:如Top機型下的Crash障本、用戶Crash率的前10%等等維度。
  • 趨勢對比

3)响鹃、展示層

經(jīng)過處理層之后驾霜,就會來到展示層,展示的信息為如下幾類:

  • 數(shù)據(jù)還原
  • 緯度信息
  • 起始版本
  • 其它信息

4)买置、報警層

最后粪糙,就會來到報警層,當(dāng)發(fā)生嚴(yán)重異常的時候忿项,會通知相關(guān)的同學(xué)進行緊急處理蓉冈。報警的規(guī)則我們可以自定義脆栋,例如整體的Crash率,其環(huán)比(與上一期進行對比)或同比(如本月10號與上月10號)抖動超過5%洒擦,或者是單個Crash突然間激增。報警的方式可以通過 郵件怕膛、IM熟嫩、電話、短信 等等方式褐捻。

7掸茅、責(zé)任歸屬

最后,我們來看下Crash相關(guān)的非技術(shù)問題柠逞,需要注意的是昧狮,我們要解決的是如何長期保持較低的Crash率這個問題。我們需要保證能夠迅速找到相關(guān)bug的相關(guān)責(zé)任人并讓開發(fā)同學(xué)能夠及時地處理線上的bug板壮。具體的解決方法為如下幾種:

  • 設(shè)立專項小組輪值:成立一個虛擬的專項小組逗鸣,來專門跟蹤每個版本線上的Crash率,組內(nèi)的成員可以輪流跟蹤線上的Crash绰精,這樣撒璧,就可以從源頭來保證所有Crash一定會有人跟進。
  • 自動匹配責(zé)任人將APM平臺與bug單系統(tǒng)打通笨使,這樣APM后臺一旦發(fā)現(xiàn)緊急bug就能第一時間下發(fā)到bug單系統(tǒng)給相關(guān)責(zé)任人發(fā)提醒卿樱。
  • 處理流程全紀(jì)錄:我們需要記錄Crash處理流程的每一步,確保緊急Crash的處理不會被延誤硫椰。

二繁调、Crash優(yōu)化

1、單個Crash處理方案

對于單個Crash的處理方案我們可以按如下三個步驟來進行解決處理靶草。

1)蹄胰、根據(jù)堆棧及現(xiàn)場信息找答案

  • 解決90%問題
  • 解決完后需考慮產(chǎn)生Crash深層次的原因

2)、找共性:機型爱致、OS烤送、實驗開關(guān)、資源包糠悯,考慮影響范圍

3)帮坚、線下復(fù)現(xiàn)、遠(yuǎn)程調(diào)試

2互艾、Crash率治理方案

要對應(yīng)用的Crash率進行治理试和,一般需要對以下三種類型的Crash進行對應(yīng)的處理,如下所示:

  • 1)纫普、解決線上常規(guī)Crash
  • 2)阅悍、系統(tǒng)級Crash嘗試Hook繞過
  • 3)好渠、疑難Crash重點突破或更換方案

3、Java Crash

出現(xiàn)未捕獲異常节视,導(dǎo)致出現(xiàn)異常退出

Thread.setDefaultUncaughtExceptionHandler()拳锚;
復(fù)制代碼

我們通過設(shè)置自定義的UncaughtExceptionHandler,就可以在崩潰發(fā)生的時候獲取到現(xiàn)場信息寻行。注意霍掺,這個鉤子是針對單個進程而言的,在多進程的APP中拌蜘,監(jiān)控哪個進程杆烁,就需要在哪個進程中設(shè)置一遍ExceptionHandler

獲取主線程的堆棧信息:

Looper.getMainLooper().getThread().getStackTrace();
復(fù)制代碼

獲取當(dāng)前線程的堆棧信息:

Thread.currentThread().getStackTrace();
復(fù)制代碼

獲取全部線程的堆棧信息:

Thread.getAllStackTraces();
復(fù)制代碼

第三方Crash監(jiān)控工具如 Fabric简卧、騰訊Bugly兔魂,都是以字符串拼接的方式將數(shù)組StackTraceElement[]轉(zhuǎn)換成字符串形式,進行保存举娩、上報或者展示析校。

那么,我們?nèi)绾畏椿煜蟼鞯亩褩P畔ⅲ?/h3>

對此铜涉,我們一般有兩種可選的處理方案勺良,如下所示:

  • 1、每次打包生成混淆APK的時候骄噪,需要把Mapping文件保存并上傳到監(jiān)控后臺尚困。
  • 2、Android原生的反混淆的工具包是retrace.jar链蕊,在監(jiān)控后臺用來實時解析每個上報的崩潰時事甜。retrace.jar 會將Mapping文件進行文本解析和對象實例化,這個過程比較耗時滔韵。因此可以將Mapping對象實例進行內(nèi)存緩存逻谦,但為了防止內(nèi)存泄露和內(nèi)存過多占用,需要增加定期自動回收的邏輯陪蜻。

如何獲取logcat方法邦马?

logcat日志流程是這樣的,應(yīng)用層 --> liblog.so --> logd宴卖,底層使用 ring buffer 來存儲數(shù)據(jù)滋将。獲取的方式有以下三種:

1、通過logcat命令獲取症昏。

  • 優(yōu)點:非常簡單随闽,兼容性好。
  • 缺點:整個鏈路比較長肝谭,可控性差掘宪,失敗率高蛾扇,特別是堆破壞或者堆內(nèi)存不足時,基本會失敗魏滚。

2镀首、hook liblog.so實現(xiàn)

通過hook liblog.so 中的 __android_log_buf_write 方法,將內(nèi)容重定向到自己的buffer中鼠次。

  • 優(yōu)點:簡單蘑斧,兼容性相對還好。
  • 缺點:要一直打開须眷。

3、自定義獲取代碼沟突。通過移植底層獲取logcat的實現(xiàn)花颗,通過socket直接跟logd交互。

  • 優(yōu)點:比較靈活惠拭,預(yù)先分配好資源扩劝,成功率也比較高。
  • 缺點:實現(xiàn)非常復(fù)雜

如何獲取Java 堆棧职辅?

當(dāng)發(fā)生native崩潰時棒呛,我們通過unwind只能拿到Native堆棧。但是我們希望可以拿到當(dāng)時各個線程的Java堆棧域携。對于這個問題簇秒,目前有兩種處理方式,分別如下所示:

1秀鞭、Thread.getAllStackTraces()趋观。

優(yōu)點

簡單,兼容性好锋边。

缺點
  • 成功率不高皱坛,依靠系統(tǒng)接口在極端情況也會失敗。
  • 7.0之后這個接口是沒有主線程堆棧豆巨。
  • 使用Java層的接口需要暫停線程剩辟。

2、hook libart.so往扔。

通過hook ThreadList和Thread 的函數(shù)贩猎,獲得跟ANR一樣的堆棧。為了穩(wěn)定性萍膛,需要在fork的子進程中執(zhí)行融欧。

  • 優(yōu)點:信息很全,基本跟ANR的日志一樣卦羡,有native線程狀態(tài)噪馏,鎖信息等等麦到。
  • 缺點:黑科技的兼容性問題,失敗時我們可以使用Thread.getAllStackTraces()兜底欠肾。

4瓶颠、Java Crash處理流程

講解了Java Crash相關(guān)的知識后,我們就可以去了解下Java Crash的處理流程刺桃,這里借用Gityuan流程圖進行講解粹淋,如下圖所示:

1、首先發(fā)生crash所在進程瑟慈,在創(chuàng)建之初便準(zhǔn)備好了defaultUncaughtHandler桃移,用來處理Uncaught Exception,并輸出當(dāng)前crash的基本信息葛碧;

2借杰、調(diào)用當(dāng)前進程中的AMP.handleApplicationCrash;經(jīng)過binder ipc機制进泼,傳遞到system_server進程蔗衡;

3、接下來乳绕,進入system_server進程绞惦,調(diào)用binder服務(wù)端執(zhí)行AMS.handleApplicationCrash;

4洋措、從mProcessNames查找到目標(biāo)進程的ProcessRecord對象济蝉;并將進程crash信息輸出到目錄/data/system/dropbox;

5菠发、執(zhí)行makeAppCrashingLocked:

  • 創(chuàng)建當(dāng)前用戶下的crash應(yīng)用的error receiver堆生,并忽略當(dāng)前應(yīng)用的廣播;
  • 停止當(dāng)前進程中所有activity中的WMS的凍結(jié)屏幕消息雷酪,并執(zhí)行相關(guān)一些屏幕相關(guān)操作淑仆;

6、再執(zhí)行handleAppCrashLocked方法:

  • 當(dāng)1分鐘內(nèi)同一進程未發(fā)生連續(xù)crash兩次時哥力,則執(zhí)行結(jié)束棧頂正在運行activity的流程死讹;
  • 當(dāng)1分鐘內(nèi)同一進程連續(xù)crash兩次時斟湃,且非persistent進程,則直接結(jié)束該應(yīng)用所有activity,并殺死該進程以及同一個進程組下的所有進程活尊。然后再恢復(fù)棧頂?shù)谝粋€非finishing狀態(tài)的activity;
  • 當(dāng)1分鐘內(nèi)同一進程連續(xù)crash兩次時牵辣,且persistent進程陨闹,則只執(zhí)行恢復(fù)棧頂?shù)谝粋€非finishing狀態(tài)的activity扑浸。

7、通過mUiHandler發(fā)送消息SHOW_ERROR_MSG梁丘,彈出crash對話框侵浸;

8旺韭、到此,system_server進程執(zhí)行完成掏觉∏耍回到crash進程開始執(zhí)行殺掉當(dāng)前進程的操作;

9澳腹、當(dāng)crash進程被殺织盼,通過binder死亡通知,告知system_server進程來執(zhí)行appDiedLocked()酱塔;

10沥邻、最后,執(zhí)行清理應(yīng)用相關(guān)的四大組件信息羊娃。

補充加油站:binder 死亡通知原理

這里我們還需要了解下binder 死亡通知的原理唐全,其流程圖如下所示:

由于Crash進程中擁有一個Binder服務(wù)端ApplicationThread,而應(yīng)用進程在創(chuàng)建過程調(diào)用attachApplicationLocked()迁沫,從而attach到system_server進程,在system_server進程內(nèi)有一個ApplicationThreadProxy捌蚊,這是相對應(yīng)的Binder客戶端集畅。當(dāng)Binder服務(wù)端ApplicationThread所在進程(即Crash進程)掛掉后,則Binder客戶端能收到相應(yīng)的死亡通知缅糟,從而進入binderDied流程挺智。

5、Native Crash

特點:

  • 訪問非法地址
  • 地址對齊出錯
  • 發(fā)生程序主動abort

上述都會產(chǎn)生相應(yīng)的signal信號窗宦,導(dǎo)致程序異常退出赦颇。

1、合格的異常捕獲組件

一個合格的異常捕獲組件需要包含以下功能:

  • 支持在crash時進行更多擴展操作
  • 打印logcat和日志
  • 上報crash次數(shù)
  • 對不同crash做不同恢復(fù)措施
  • 可以針對業(yè)務(wù)不斷改進的適應(yīng)

2赴涵、現(xiàn)有方案

1媒怯、Google Breakpad

  • 優(yōu)點:權(quán)威、跨平臺
  • 缺點:代碼體量較大

2髓窜、Logcat

  • 優(yōu)點:利用安卓系統(tǒng)實現(xiàn)
  • 缺點:需要在crash時啟動新進程過濾logcat日志扇苞,不可靠

3、coffeecatch

  • 優(yōu)點:實現(xiàn)簡潔寄纵、改動容易
  • 缺點:有兼容性問題

3鳖敷、Native崩潰捕獲流程

Native崩潰捕獲的過程涉及到三端,這里我們分別來了解下其對應(yīng)的處理程拭。

1定踱、編譯端

編譯C/C++需將帶符號信息的文件保留下來。

2恃鞋、客戶端

捕獲到崩潰時崖媚,將收集到盡可能多的有用信息寫入日志文件亦歉,然后選擇合適的時機上傳到服務(wù)器。

3至扰、服務(wù)端

讀取客戶端上報的日志文件鳍徽,尋找合適的符號文件,生成可讀的C/C++調(diào)用棧敢课。

4阶祭、Native崩潰捕獲的難點

核心:如何確保客戶端在各種極端情況下依然可以生成崩潰日志直秆。

1濒募、文件句柄泄漏,導(dǎo)致創(chuàng)建日志文件失敾帷瑰剃?

提前申請文件句柄fd預(yù)留。

2筝野、棧溢出導(dǎo)致日志生成失斏我Α?

  • 使用額外的椥梗空間signalstack挥唠,避免棧溢出導(dǎo)致進程沒有空間創(chuàng)建調(diào)用棧執(zhí)行處理函數(shù)。(signalstack:系統(tǒng)會在危險情況下把棧指針指向這個地方焕议,使得可以在一個新的棧上運行信號處理函數(shù))
  • 特殊請求需直接替換當(dāng)前棧宝磨,所以應(yīng)在堆中預(yù)留部分空間。

3盅安、堆內(nèi)存耗盡導(dǎo)致日志生產(chǎn)失敾斤薄?

參考Breakpad重新封裝Linux Syscall Support的做法以避免直接調(diào)用libc去分配堆內(nèi)存别瞭。

4窿祥、堆破壞或二次崩潰導(dǎo)致日志生成失敗蝙寨?

Breakpad使用了fork子進程甚至孫進程的方式去收集崩潰現(xiàn)場壁肋,即便出現(xiàn)二次崩潰,也只是這部分信息丟失籽慢。

這里說下Breakpad缺點:

  • 生成的minidump文件是二進制的浸遗,包含過多不重要的信息,導(dǎo)致文件數(shù)過大箱亿。但minidump可以使用gdb調(diào)試跛锌、看到傳入?yún)?shù)。

需要了解的是,未來Chromium會使用Crashpad替代Breakpad髓帽。

5菠赚、想要遵循Android的文本格式并添加更多重要的信息?

改造Breakpad郑藏,增加Logcat信息衡查,Java調(diào)用棧信息、其它有用信息必盖。

5拌牲、Native崩潰捕獲注冊

一個Native Crash log信息如下:

堆棧信息中 pc 后面跟的內(nèi)存地址,就是當(dāng)前函數(shù)的棧地址歌粥,我們可以通過下面的命令行得出出錯的代碼行數(shù)

arm-linux-androideabi-addr2line -e 內(nèi)存地址
復(fù)制代碼

下面列出全部的信號量以及所代表的含義:

#define SIGHUP 1  // 終端連接結(jié)束時發(fā)出(不管正乘觯或非正常)
#define SIGINT 2  // 程序終止(例如Ctrl-C)
#define SIGQUIT 3 // 程序退出(Ctrl-\)
#define SIGILL 4 // 執(zhí)行了非法指令,或者試圖執(zhí)行數(shù)據(jù)段失驶,堆棧溢出
#define SIGTRAP 5 // 斷點時產(chǎn)生土居,由debugger使用
#define SIGABRT 6 // 調(diào)用abort函數(shù)生成的信號,表示程序異常
#define SIGIOT 6 // 同上嬉探,更全擦耀,IO異常也會發(fā)出
#define SIGBUS 7 // 非法地址,包括內(nèi)存地址對齊出錯涩堤,比如訪問一個4字節(jié)的整數(shù), 但其地址不是4的倍數(shù)
#define SIGFPE 8 // 計算錯誤眷蜓,比如除0、溢出
#define SIGKILL 9 // 強制結(jié)束程序定躏,具有最高優(yōu)先級账磺,本信號不能被阻塞芹敌、處理和忽略
#define SIGUSR1 10 // 未使用痊远,保留
#define SIGSEGV 11 // 非法內(nèi)存操作,與 SIGBUS不同氏捞,他是對合法地址的非法訪問碧聪,    比如訪問沒有讀權(quán)限的內(nèi)存,向沒有寫權(quán)限的地址寫數(shù)據(jù)
#define SIGUSR2 12 // 未使用液茎,保留
#define SIGPIPE 13 // 管道破裂逞姿,通常在進程間通信產(chǎn)生
#define SIGALRM 14 // 定時信號,
#define SIGTERM 15 // 結(jié)束程序,類似溫和的 SIGKILL捆等,可被阻塞和處理滞造。通常程序如    果終止不了,才會嘗試SIGKILL
#define SIGSTKFLT 16  // 協(xié)處理器堆棧錯誤
#define SIGCHLD 17 // 子進程結(jié)束時, 父進程會收到這個信號栋烤。
#define SIGCONT 18 // 讓一個停止的進程繼續(xù)執(zhí)行
#define SIGSTOP 19 // 停止進程,本信號不能被阻塞,處理或忽略
#define SIGTSTP 20 // 停止進程,但該信號可以被處理和忽略
#define SIGTTIN 21 // 當(dāng)后臺作業(yè)要從用戶終端讀數(shù)據(jù)時, 該作業(yè)中的所有進程會收到SIGTTIN信號
#define SIGTTOU 22 // 類似于SIGTTIN, 但在寫終端時收到
#define SIGURG 23 // 有緊急數(shù)據(jù)或out-of-band數(shù)據(jù)到達socket時產(chǎn)生
#define SIGXCPU 24 // 超過CPU時間資源限制時發(fā)出
#define SIGXFSZ 25 // 當(dāng)進程企圖擴大文件以至于超過文件大小資源限制
#define SIGVTALRM 26 // 虛擬時鐘信號. 類似于SIGALRM, 但是計算的是該進程占用的CPU時間.
#define SIGPROF 27 // 類似于SIGALRM/SIGVTALRM, 但包括該進程用的CPU時間以及系統(tǒng)調(diào)用的時間
#define SIGWINCH 28 // 窗口大小改變時發(fā)出
#define SIGIO 29 // 文件描述符準(zhǔn)備就緒, 可以開始進行輸入/輸出操作
#define SIGPOLL SIGIO // 同上谒养,別稱
#define SIGPWR 30 // 電源異常
#define SIGSYS 31 // 非法的系統(tǒng)調(diào)用
復(fù)制代碼

一般關(guān)注SIGILL(執(zhí)行了非法指令,或者試圖執(zhí)行數(shù)據(jù)段明郭,堆棧溢出), SIGABRT(調(diào)用abort函數(shù)生成的信號买窟,表示程序異常), SIGBUS(非法地址丰泊,包括內(nèi)存地址對齊出錯,比如訪問一個4字節(jié)的整數(shù), 但其地址不是4的倍數(shù)), SIGFPE, SIGSEGV, SIGSTKFLT, SIGSYS即可始绍。

要訂閱異常發(fā)生的信號瞳购,最簡單的做法就是直接用一個循環(huán)遍歷所有要訂閱的信號,對每個信號調(diào)用sigaction()亏推。

注意

  • JNI_OnLoad是最適合安裝信號初始函數(shù)的地方学赛。
  • 建議在上報時調(diào)用Java層的方法統(tǒng)一上報。Native崩潰捕獲注冊径簿。

6罢屈、崩潰分析流程

首先,應(yīng)收集崩潰現(xiàn)場的一些相關(guān)信息篇亭,如下:

1缠捌、崩潰信息

  • 進程名、線程名
  • 崩潰堆棧和類型
  • 有時候也需要知道主線程的調(diào)用棧

2译蒂、系統(tǒng)信息

  • 系統(tǒng)運行日志

    /system/etc/event-log-tags

  • 機型曼月、系統(tǒng)、廠商柔昼、CPU哑芹、ABI、Linux版本等

注意捕透,我們可以去尋找共性問題聪姿,如下:

  • 設(shè)備狀態(tài)
  • 是否root
  • 是否是模擬器

3、內(nèi)存信息

系統(tǒng)剩余內(nèi)存
/proc/meminfo
復(fù)制代碼

當(dāng)系統(tǒng)可用內(nèi)存小于MemTotal的10%時乙嘀,OOM末购、大量GC、系統(tǒng)頻繁自殺拉起等問題非常容易出現(xiàn)虎谢。

應(yīng)用使用內(nèi)存

包括Java內(nèi)存盟榴、RSS、PSS

PSS和RSS通過/proc/self/smap計算婴噩,可以得到apk擎场、dex、so等更詳細(xì)的分類統(tǒng)計几莽。

虛擬內(nèi)存

獲取大醒赴臁:

/proc/self/status
復(fù)制代碼

獲取其具體的分布情況:

/proc/self/maps
復(fù)制代碼

需要注意的是,對于32位進程章蚣,32位CPU站欺,虛擬內(nèi)存達到3GB就可能會引起內(nèi)存失敗的問題。如果是64位的CPU,虛擬內(nèi)存一般在3~4GB镊绪。如果支持64位進程匀伏,虛擬內(nèi)存就不會成為問題。

4蝴韭、資源信息

如果應(yīng)用堆內(nèi)存和設(shè)備內(nèi)存比較充足够颠,但還出現(xiàn)內(nèi)存分配失敗,則可能跟資源泄漏有關(guān)榄鉴。

文件句柄fd

獲取fd的限制數(shù)量:

/proc/self/limits
復(fù)制代碼

一般單個進程允許打開的最大句柄個數(shù)為1024履磨,如果超過800需將所有fd和文件名輸出日志進行排查

線程數(shù)

獲取線程數(shù)大星斐尽:

/proc/self/status
復(fù)制代碼

一個線程一般占2MB的虛擬內(nèi)存剃诅,線程數(shù)超過400個比較危險,需要將所有tid和線程名輸出到日志進行排查驶忌。

JNI

容易出現(xiàn)引用失效矛辕、引用爆表等崩潰。

通過DumpReferenceTables統(tǒng)計JNI的引用表付魔,進一步分析是否出現(xiàn)JNI泄漏等問題聊品。

補充加油站:dumpReferenceTables的出處

在dalvik.system.VMDebug類中,是一個native方法几苍,亦是static方法翻屈;在JNI中可以這么調(diào)用

jclass vm_class = env->FindClass("dalvik/system/VMDebug");
jmethodID dump_mid = env->GetStaticMethodID( vm_class, "dumpReferenceTables", "()V" );
env->CallStaticVoidMethod( vm_class, dump_mid );
復(fù)制代碼

5、應(yīng)用信息

  • 崩潰場景
  • 關(guān)鍵操作路徑
  • 其它跟自身應(yīng)用相關(guān)的自定義信息:運行時間妻坝、是否加載補丁伸眶、是否全新安裝或升級。

6刽宪、崩潰分析流程

接下來進行崩潰分析:

1厘贼、確定重點
  • 確認(rèn)嚴(yán)重程度
  • 優(yōu)先解決Top崩潰或?qū)I(yè)務(wù)有重大影響的崩潰:如啟動、支付過程的崩潰
  • Java崩潰:如果是OOM纠屋,需進一步查看日志中的內(nèi)存信息和資源信息
  • Native崩潰:查看signal涂臣、code盾计、fault addr以及崩潰時的Java堆棧

常見的崩潰類型有:

  • SIGSEGV:空指針售担、非法指針等
  • SIGABRT:ANR、調(diào)用abort推出等

如果是ANR署辉,先看主線程堆棧族铆、是否因為鎖等待導(dǎo)致,然后看ANR日志中的iowait哭尝、CPU哥攘、GC、systemserver等信息,確定是I/O問題或CPU競爭問題還是大量GC導(dǎo)致的ANR逝淹。

注意耕姊,當(dāng)從一條崩潰日志中無法看出問題原因時,需要查看相同崩潰點下的更多崩潰日志栅葡,或者也可以查看內(nèi)存信息茉兰、資源信息等進行異常排查。

2欣簇、查找共性

機型规脸、系統(tǒng)、ROM熊咽、廠商莫鸭、ABI這些信息都可以作為共性參考,對于下一步復(fù)現(xiàn)問題有明確指引横殴。

3被因、嘗試復(fù)現(xiàn)

復(fù)現(xiàn)之后再增加日志或使用Debugger、GDB進行調(diào)試衫仑。如不能復(fù)現(xiàn)氏身,可以采用一些高級手段,如xlog日志惑畴、遠(yuǎn)程診斷蛋欣、動態(tài)分析等等。

補充加油站:系統(tǒng)崩潰解決方式

  • 1如贷、通過共性信息查找可能的原因
  • 2陷虎、嘗試使用其它使用方式規(guī)避
  • 3、Hook解決

7杠袱、實戰(zhàn):使用Breakpad捕獲native崩潰

首先楣富,這里給出《Android開發(fā)高手課》張紹文老師寫的crash捕獲示例工程庄萎,工程里面已經(jīng)集成了Breakpad 來獲取發(fā)生 native crash 時候的系統(tǒng)信息和線程堆棧信息兼犯。下面來詳細(xì)介紹下使用Breakpad來分析native崩潰的流程:

1具篇、示例工程是采用cmake的構(gòu)建方式,所以需要先到Android Studio中SDK Manager中的SDK Tools下下載NDK和cmake秒紧。

2、安裝實例工程后叙淌,點擊CRASH按鈕產(chǎn)生一個native崩潰。生成的 crash信息,如果授予Sdcard權(quán)限會優(yōu)先存放在/sdcard/crashDump下,便于我們做進一步的分析。反之會放到目錄 /data/data/com.dodola.breakpad/files/crashDump中送巡。

3辨嗽、使用adb pull命令將抓取到的crash日志文件放到電腦本地目錄中:

adb pull /sdcard/crashDump/***.dmp > ~/Documents/crash_log.dmp
復(fù)制代碼

4糟需、下載并編譯Breakpad源碼谷朝,在src/processor目錄下找到minidump_stackwalk,使用這個工具將dmp文件轉(zhuǎn)換為txt文件:

// 在項目目錄下clone Breakpad倉庫
git clone https://github.com/google/breakpad.git

// 切換到Breakpad根目錄進行配置杈帐、編譯
cd breakpad
./configure && make

// 使用src/processor目錄下的minidump_stackwalk工具將dmp文件轉(zhuǎn)換為txt文件
./src/processor/minidump_stackwalk ~/Documents/crashDump/crash_log.dmp >crash_log.txt 
復(fù)制代碼

5、打開crash_log.txt专钉,可以得到如下內(nèi)容:

Operating system: Android
                  0.0.0 Linux 4.4.78-perf-g539ee70 #1 SMP PREEMPT Mon Jan 14 17:08:14 CST 2019 aarch64
CPU: arm64
     8 CPUs

GPU: UNKNOWN

Crash reason:  SIGSEGV /SEGV_MAPERR
Crash address: 0x0
Process uptime: not available

Thread 0 (crashed)
 0  libcrash-lib.so + 0x650
復(fù)制代碼

其中我們需要的關(guān)鍵信息為CPU是arm64的站叼,并且crash的地址為0x650第练。接下來我們需要將這個地址轉(zhuǎn)換為代碼中對應(yīng)的行呕寝。

6、使用ndk 中提供的addr2line來根據(jù)地址進行一個符號反解的過程婴梧。

如果是arm64的so使用 $NDKHOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line壁涎。

如果是arm的so使用 $NDKHOME/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line。

由crash_log.txt的信息可知志秃,我們機器的cpu架構(gòu)是arm64的怔球,因此需要使用aarch64-linux-android-addr2line這個命令行工具。該命令的一般使用格式如下: // 注意:在mac下 ./ 代表執(zhí)行文件 ./aarch64-linux-android-addr2line -e 對應(yīng)的.so 需要解析的地址

上述中對應(yīng)的.so文件在項目編譯之后浮还,會出現(xiàn)在Chapter01-master/sample/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/libcrash-lib.so這個位置竟坛,由于我的手機CPU架構(gòu)是arm64的,所以這里選擇的是arm64-v8a中的libcrash-lib.so钧舌。接下來我們使用aarch64-linux-android-addr2line這個命令:

./aarch64-linux-android-addr2line -f -C -e ~/Documents/open-project/Chapter01-master/sample/build/intermediates/merged_native_libs/debug/out/lib/arm64-v8a/libcrash-lib.so 0x650

參數(shù)含義:
-e --exe=<executable>:指定需要轉(zhuǎn)換地址的可執(zhí)行文件名担汤。
-f --functions:在顯示文件名、行號輸出信息的同時顯示函數(shù)名信息洼冻。
-C --demangle[=style]:將低級別的符號名解碼為用戶級別的名字崭歧。
復(fù)制代碼

結(jié)果輸出為:

Crash()
/Users/quchao/Documents/open-project/Chapter01-master/sample/src/main/cpp/crash.cpp:10
復(fù)制代碼

由此,我們得出crash的代碼行為crash.cpp文件中的第10行撞牢,接下來根據(jù)項目具體情況進行相應(yīng)的修改即可率碾。

Tips:這是從事NDK開發(fā)(音視頻叔营、圖像處理、OpenCv所宰、熱修復(fù)框架開發(fā))同學(xué)調(diào)試native層錯誤時經(jīng)常要使用的技巧绒尊,強烈建議熟練掌握。

6仔粥、疑難Crash解決方案

最后婴谱,筆者這里再講解下一些疑難Crash的解決方案。

問題1:如何解決Android 7.0 Toast BadTokenException躯泰?

參考Android 8.0 try catch的做法谭羔,代理Toast里的mTN(handler)就可以實現(xiàn)捕獲異常。

問題2:如何解決 SharedPreference apply 引起的 ANR 問題

apply為什么會引起ANR麦向?

SP 調(diào)用 apply 方法口糕,會創(chuàng)建一個等待鎖放到 QueuedWork 中,并將真正的數(shù)據(jù)持久化封裝成一個任務(wù)放到異步隊列中執(zhí)行磕蛇,任務(wù)執(zhí)行結(jié)束會釋放鎖景描。Activity onStop 以及 Service 處理 onStop,onStartCommand 時秀撇,執(zhí)行 QueuedWork.waitToFinish() 等待所有的等待鎖釋放超棺。

如何解決?

所有此類 ANR 都是經(jīng)由 QueuedWork.waitToFinish() 觸發(fā)的呵燕,只要在調(diào)用此函數(shù)之前棠绘,將其中保存的隊列手動清空即可。

具體是Hook ActivityThrad的Handler變量再扭,拿到此變量后給其設(shè)置一個Callback氧苍,Handler 的 dispatchMessage 中會先處理 callback。最后在 Callback 中調(diào)用隊列的清理工作泛范,注意隊列清理需要反射調(diào)用 QueuedWork让虐。

注意

apply 機制本身的失敗率就比較高(1.8%左右),清理等待鎖隊列對持久化造成的影響不大罢荡。

問題3:如何解決TimeoutExceptin異常赡突?

它是由系統(tǒng)的FinalizerWatchdogDaemon拋出來的。

這里首先介紹下看門狗 WatchDog区赵,它 的作用是監(jiān)控重要服務(wù)的運行狀態(tài)惭缰,當(dāng)重要服務(wù)停止時,發(fā)生 Timeout 異常崩潰笼才,WatchDog 負(fù)責(zé)將應(yīng)用重啟漱受。而當(dāng)關(guān)閉 WatchDog(執(zhí)行stop()方法)后,當(dāng)重要服務(wù)停止時骡送,也不會發(fā)生 Timeout 異常昂羡,是一種通過非正常手段防止異常發(fā)生的方法絮记。

規(guī)避方案

stop方法,在Android 6.0之前會有線程同步問題紧憾。 因為6.0之前調(diào)用threadToStop的interrupt方法是沒有加鎖的到千,所以可能會有線程同步的問題昌渤。

注意:Stop的時候有一定概率導(dǎo)致即使沒有超時也會報timeoutexception赴穗。

缺點

只是為了避免上報異常采取的一種hack方案,并沒有真正解決引起finialize超時的問題膀息。

問題4:如何解決輸入法的內(nèi)存泄漏般眉?

通過反射將輸入法的兩個View置空。

7潜支、進程钡樵撸活

我們可以利用SyncAdapter提高進程優(yōu)先級,它是Android系統(tǒng)提供一個賬號同步機制冗酿,它屬于核心進程級別埠对,而使用了SyncAdapter的進程優(yōu)先級本身也會提高,使用方式請Google裁替,關(guān)聯(lián)SyncAdapter后项玛,進程的優(yōu)先級變?yōu)?,僅低于前臺正在運行的進程弱判,因此可以降低應(yīng)用被系統(tǒng)殺掉的概率襟沮。

8、總結(jié)

對于App的Crash優(yōu)化昌腰,總的來說开伏,我們需要考慮以下四個要點:

  • 1、重在預(yù)防:重視應(yīng)用的整個流程遭商、包括開發(fā)人員的培訓(xùn)固灵、編譯檢查、靜態(tài)掃描劫流、規(guī)范的測試怎虫、灰度、發(fā)布流程等
  • 2困介、不應(yīng)該隨意使用try catch去隱藏問題:而應(yīng)該從源頭入手大审,了解崩潰的本質(zhì)原因,保證后面的運行流程座哩。
  • 3徒扶、解決崩潰的過程應(yīng)該由點到面,考慮一類崩潰怎么解決根穷。
  • 4姜骡、崩潰與內(nèi)存导坟、卡頓、I/O內(nèi)存緊密相關(guān)

三圈澈、ANR優(yōu)化

1惫周、ANR監(jiān)控實現(xiàn)方式

1、使用FileObserver監(jiān)聽 /data/anr/traces.txt的變化

缺點

高版本ROM需要root權(quán)限康栈。

解決方案

海外Google Play服務(wù)递递、國內(nèi)Hardcoder。

2啥么、監(jiān)控消息隊列的運行時間

卡頓監(jiān)控原理:

利用主線程的消息隊列處理機制登舞,應(yīng)用發(fā)生卡頓,一定是在dispatchMessage中執(zhí)行了耗時操作悬荣。我們通過給主線程的Looper設(shè)置一個Printer菠秒,打點統(tǒng)計dispatchMessage方法執(zhí)行的時間,如果超出閥值氯迂,表示發(fā)生卡頓践叠,則dump出各種信息,提供開發(fā)者分析性能瓶頸嚼蚀。

為卡頓監(jiān)控代碼增加ANR的線程監(jiān)控禁灼,在發(fā)送消息時,在ANR線程中保存一個狀態(tài)驰坊,主線程消息執(zhí)行完后再Reset標(biāo)志位匾二。如果在ANR線程中收到發(fā)送消息后,超過一定時間沒有復(fù)位拳芙,就可以任務(wù)發(fā)生了ANR察藐。

缺點

  • 無法準(zhǔn)確判斷是否真正出現(xiàn)ANR,只能說明APP發(fā)生了UI阻塞舟扎,需要進行二次校驗分飞。校驗的方式就是等待手機系統(tǒng)出現(xiàn)發(fā)生了Error的進程,并且Error類型是NOT_RESPONDING(值為2)睹限。

在每次出現(xiàn)ANR彈框前譬猫,Native層都會發(fā)出signal為SIGNAL_QUIT(值為3)的信號事件,也可以監(jiān)聽此信號羡疗。

  • 無法得到完整ANR日志
  • 隸屬于卡頓優(yōu)化的方式

3染服、需要考慮應(yīng)用退出場景

  • 主動自殺
  • Process.killProcess()、exit()等。
  • 崩潰
  • 系統(tǒng)重啟
  • 系統(tǒng)異常、斷電祷蝌、用戶重啟等:通過比較應(yīng)用開機運行時間是否比之前記錄的值更小婉支。
  • 被系統(tǒng)殺死
  • 被LMK殺死秉颗、從系統(tǒng)的任務(wù)管理器中劃掉等痢毒。

注意

由于traces.txt上傳比較耗時,所以一般線下采用蚕甥,線上建議綜合ProcessErrorStateInfo和出現(xiàn)ANR時的堆棧信息來實現(xiàn)ANR的實時上傳哪替。

2、ANR優(yōu)化

ANR發(fā)生原因:沒有在規(guī)定的時間內(nèi)完成要完成的事情菇怀。

ANR分類

發(fā)生場景

  • Activity onCreate方法或Input事件超過5s沒有完成凭舶;
  • BroadcastReceiver前臺10s,后臺60s敏释;
  • ContentProvider 在publish過超時10s库快;
  • Service前臺20s摸袁,后臺200s钥顽。

發(fā)生原因

  • 主線程有耗時操作
  • 復(fù)雜布局
  • IO操作
  • 被子線程同步鎖block
  • 被Binder對端block
  • Binder被占滿導(dǎo)致主線程無法和SystemServer通信
  • 得不到系統(tǒng)資源(CPU/RAM/IO)

從進程角度看發(fā)生原因有:

  • 當(dāng)前進程:主線程本身耗時或者主線程的消息隊列存在耗時操作、主線程被本進程的其它子線程所blocked
  • 遠(yuǎn)端進程:binder call靠汁、socket通信

Andorid系統(tǒng)監(jiān)測ANR的核心原理是消息調(diào)度和超時處理蜂大。

ANR排查流程

1、Log獲取

1蝶怔、抓取bugreport

adb shell bugreport > bugreport.txt
復(fù)制代碼

2奶浦、直接導(dǎo)出/data/anr/traces.txt文件

adb pull /data/anr/traces.txt trace.txt
復(fù)制代碼

2、搜索“ANR in”處log關(guān)鍵點解讀

  • 發(fā)生時間(可能會延時10-20s)

  • pid:當(dāng)pid=0踢星,說明在ANR之前澳叉,進程就被LMK殺死或出現(xiàn)了Crash,所以無法接受到系統(tǒng)的廣播或者按鍵消息沐悦,因此會出現(xiàn)ANR

  • cpu負(fù)載Load: 7.58 / 6.21 / 4.83

    代表此時一分鐘有平均有7.58個進程在等待 1成洗、5、15分鐘內(nèi)系統(tǒng)的平均負(fù)荷 當(dāng)系統(tǒng)負(fù)荷持續(xù)大于1.0藏否,必須將值降下來 當(dāng)系統(tǒng)負(fù)荷達到5.0瓶殃,表面系統(tǒng)有很嚴(yán)重的問題

  • cpu使用率

    CPU usage from 18101ms to 0ms ago 28% 2085/system_server: 18% user + 10% kernel / faults: 8689 minor 24 major 11% 752/android.hardware.sensors@1.0-service: 4% user + 6.9% kernel / faults: 2 minor 9.8% 780/surfaceflinger: 6.2% user + 3.5% kernel / faults: 143 minor 4 major

上述表示Top進程的cpu占用情況。

注意

如果CPU使用量很少副签,說明主線程可能阻塞遥椿。

3、在bugreport.txt中根據(jù)pid和發(fā)生時間搜索到阻塞的log處

----- pid 10494 at 2019-11-18 15:28:29 -----
復(fù)制代碼

4淆储、往下翻找到“main”線程則可看到對應(yīng)的阻塞log

"main" prio=5 tid=1 Sleeping
| group="main" sCount=1 dsCount=0 flags=1 obj=0x746bf7f0 self=0xe7c8f000
| sysTid=10494 nice=-4 cgrp=default sched=0/0 handle=0xeb6784a4
| state=S schedstat=( 5119636327 325064933 4204 ) utm=460 stm=51 core=4 HZ=100
| stack=0xff575000-0xff577000 stackSize=8MB
| held mutexes=
復(fù)制代碼

上述關(guān)鍵字段的含義如下所示:

  • tid:線程號
  • sysTid:主進程線程號和進程號相同
  • Waiting/Sleeping:各種線程狀態(tài)
  • nice:nice值越小冠场,則優(yōu)先級越高,-17~16
  • schedstat:Running本砰、Runable時間(ns)與Switch次數(shù)
  • utm:該線程在用戶態(tài)的執(zhí)行時間(jiffies)
  • stm:該線程在內(nèi)核態(tài)的執(zhí)行時間(jiffies)
  • sCount:該線程被掛起的次數(shù)
  • dsCount:該線程被調(diào)試器掛起的次數(shù)
  • self:線程本身的地址

補充加油站:各種線程狀態(tài)

需要注意的是碴裙,這里的各種線程狀態(tài)指的是Native層的線程狀態(tài),關(guān)于Java線程狀態(tài)與Native線程狀態(tài)的對應(yīng)關(guān)系如下所示:

enum ThreadState {
  //                                   Thread.State   JDWP state
  kTerminated = 66,                 // TERMINATED     TS_ZOMBIE    Thread.run has returned, but Thread* still around
  kRunnable,                        // RUNNABLE       TS_RUNNING   runnable
  kTimedWaiting,                    // TIMED_WAITING  TS_WAIT      in Object.wait() with a timeout
  kSleeping,                        // TIMED_WAITING  TS_SLEEPING  in Thread.sleep()
  kBlocked,                         // BLOCKED        TS_MONITOR   blocked on a monitor
  kWaiting,                         // WAITING        TS_WAIT      in Object.wait()
  kWaitingForLockInflation,         // WAITING        TS_WAIT      blocked inflating a thin-lock
  kWaitingForTaskProcessor,         // WAITING        TS_WAIT      blocked waiting for taskProcessor
  kWaitingForGcToComplete,          // WAITING        TS_WAIT      blocked waiting for GC
  kWaitingForCheckPointsToRun,      // WAITING        TS_WAIT      GC waiting for checkpoints to run
  kWaitingPerformingGc,             // WAITING        TS_WAIT      performing GC
  kWaitingForDebuggerSend,          // WAITING        TS_WAIT      blocked waiting for events to be sent
  kWaitingForDebuggerToAttach,      // WAITING        TS_WAIT      blocked waiting for debugger to attach
  kWaitingInMainDebuggerLoop,       // WAITING        TS_WAIT      blocking/reading/processing debugger events
  kWaitingForDebuggerSuspension,    // WAITING        TS_WAIT      waiting for debugger suspend all
  kWaitingForJniOnLoad,             // WAITING        TS_WAIT      waiting for execution of dlopen and JNI on load code
  kWaitingForSignalCatcherOutput,   // WAITING        TS_WAIT      waiting for signal catcher IO to complete
  kWaitingInMainSignalCatcherLoop,  // WAITING        TS_WAIT      blocking/reading/processing signals
  kWaitingForDeoptimization,        // WAITING        TS_WAIT      waiting for deoptimization suspend all
  kWaitingForMethodTracingStart,    // WAITING        TS_WAIT      waiting for method tracing to start
  kWaitingForVisitObjects,          // WAITING        TS_WAIT      waiting for visiting objects
  kWaitingForGetObjectsAllocated,   // WAITING        TS_WAIT      waiting for getting the number of allocated objects
  kWaitingWeakGcRootRead,           // WAITING        TS_WAIT      waiting on the GC to read a weak root
  kWaitingForGcThreadFlip,          // WAITING        TS_WAIT      waiting on the GC thread flip (CC collector) to finish
  kStarting,                        // NEW            TS_WAIT      native thread started, not yet ready to run managed code
  kNative,                          // RUNNABLE       TS_RUNNING   running in a JNI native method
  kSuspended,                       // RUNNABLE       TS_RUNNING   suspended by GC or debugger
};
復(fù)制代碼

其它分析方法:Java線程調(diào)用分析方法

  • 先使用jps命令列出當(dāng)前系統(tǒng)中運行的所有Java虛擬機進程,拿到應(yīng)用進程的pid青团。
  • 然后再使用jstack命令查看該進程中所有線程的狀態(tài)以及調(diào)用關(guān)系譬巫,以及一些簡單的分析結(jié)果。

3督笆、關(guān)于ANR的一些常見問題

1芦昔、sp調(diào)用apply導(dǎo)致anr問題?

雖然apply并不會阻塞主線程娃肿,但是會將等待時間轉(zhuǎn)嫁到主線程咕缎。

2、檢測運行期間是否發(fā)生過異常退出料扰?

在應(yīng)用啟動時設(shè)定一個標(biāo)志凭豪,在主動自殺或崩潰后更新標(biāo)志 ,下次啟動時檢測此標(biāo)志即可判斷晒杈。

4嫂伞、理解ANR的觸發(fā)流程

broadcast跟service超時機制大抵相同,但有一個非常隱蔽的技能點拯钻,那就是通過靜態(tài)注冊的廣播超時會受SharedPreferences(簡稱SP)的影響帖努。

當(dāng)SP有未同步到磁盤的工作,則需等待其完成粪般,才告知系統(tǒng)已完成該廣播拼余。并且只有XML靜態(tài)注冊的廣播超時檢測過程會考慮是否有SP尚未完成,動態(tài)廣播并不受其影響亩歹。

  • 對于Service, Broadcast, Input發(fā)生ANR之后,最終都會調(diào)用AMS.appNotResponding匙监。
  • 對于provider,在其進程啟動時publish過程可能會出現(xiàn)ANR, 則會直接殺進程以及清理相應(yīng)信息,而不會彈出ANR的對話框。

1小作、AMS.appNotResponding流程

  • 輸出ANR Reason信息到EventLog. 也就是說ANR觸發(fā)的時間點最接近的就是EventLog中輸出的am_anr信息亭姥。
  • 收集并輸出重要進程列表中的各個線程的traces信息,該方法較耗時躲惰。
  • 輸出當(dāng)前各個進程的CPU使用情況以及CPU負(fù)載情況致份。
  • 將traces文件和 CPU使用情況信息保存到dropbox,即data/system/dropbox目錄(ANR信息最為重要的信息)础拨。
  • 根據(jù)進程類型,來決定直接后臺殺掉,還是彈框告知用戶氮块。

2、AMS.dumpStackTraces流程

1诡宗、收集firstPids進程的stacks:

  • 第一個是發(fā)生ANR進程滔蝉;
  • 第二個是system_server;
  • 其余的是mLruProcesses中所有的persistent進程塔沃。

2蝠引、收集Native進程的stacks。(dumpNativeBacktraceToFile)

  • 依次是mediaserver,sdcard,surfaceflinger進程。

3螃概、收集lastPids進程的stacks:

  • 依次輸出CPU使用率top 5的進程矫夯;
注意

上述導(dǎo)出每個進程trace時,進程之間會休眠200ms吊洼。

四训貌、移動端業(yè)務(wù)高可用方案建設(shè)

1、業(yè)務(wù)高可用重要性

關(guān)于業(yè)務(wù)高可用重要性有如下五點:

  • 高可用
  • 性能
  • 業(yè)務(wù)
  • 側(cè)重于用戶功能完整可用
  • 真實影響收入

2冒窍、業(yè)務(wù)高可用方案建設(shè)

業(yè)務(wù)高可用方案建設(shè)需要注意的點比較繁雜递沪,但是總體可以歸結(jié)為如下幾點:

  • 數(shù)據(jù)采集
  • 梳理項目主流程、核心路徑综液、關(guān)鍵節(jié)點
  • Aop自動采集款慨、統(tǒng)一上報
  • 報警策略:閾值報警、趨勢報警谬莹、特定指標(biāo)報警檩奠、直接上報(或底閾值)
  • 異常監(jiān)控
  • 單點追查:需要針對性分析的特定問題,全量日志回?fù)平炝迹瑢m椃治?/li>
  • 兜底策略
  • 配置中心笆凌、功能開關(guān)
  • 跳轉(zhuǎn)分發(fā)中心(組件化路由)

3圣猎、移動端容災(zāi)方案

災(zāi)包括:

  • 性能異常
  • 業(yè)務(wù)異常

傳統(tǒng)流程:

用戶反饋士葫、重新打包、渠道更新送悔、不可接受慢显。

容災(zāi)方案建設(shè)

關(guān)于容災(zāi)方案的建設(shè)主要可以細(xì)分為以下七點,下面欠啤,我們分別來了解下荚藻。

1、功能開關(guān)

配置中心洁段,服務(wù)端下發(fā)配置控制

針對場景
  • 功能新增
  • 代碼改動

2应狱、統(tǒng)跳中心

  • 界面切換通過路由,路由決定是否重定向
  • Native Bug不能熱修復(fù)則跳轉(zhuǎn)到臨時H5頁面

3祠丝、動態(tài)化修復(fù)

熱修復(fù)能力疾呻,可監(jiān)控、灰度写半、回滾岸蜗、清除。

4叠蝇、推拉結(jié)合璃岳、多場景調(diào)用保證到達率

5、Weex、RN增量更新

6铃慷、安全模式

微信讀書单芜、蘑菇街、淘寶犁柜、天貓等“重運營”的APP都使用了安全模式保障客戶端啟動流程缓溅,啟動失敗后給用戶自救機會。先介紹一下它的核心特點:

  • 根據(jù)Crash信息自動恢復(fù)赁温,多次啟動失敗重置應(yīng)用為安裝初始狀態(tài)
  • 嚴(yán)重Bug可阻塞性熱修復(fù)
安全模式設(shè)計

配置后臺:統(tǒng)一的配置后臺坛怪,具備灰度發(fā)布機制

1、客戶端能力:

  • 在APP連續(xù)Crash的情況下具備分級股囊、無感自修復(fù)能力
  • 具備同步熱修復(fù)能力
  • 具備指定觸發(fā)某項特定功能的能力
  • 具體功能注冊能力袜匿,方便后期擴展安全模式

2、數(shù)據(jù)統(tǒng)計及告警

  • 統(tǒng)一的數(shù)據(jù)平臺
  • 監(jiān)控告警功能稚疹,及時發(fā)現(xiàn)問題
  • 查看熱修復(fù)成功率等數(shù)據(jù)

3居灯、快速測試

  • 優(yōu)化預(yù)發(fā)布環(huán)境下測試
  • 優(yōu)化回歸驗證安全模式難點等
天貓安全模式原理

1、如何判斷異常退出内狗?

APP啟動時記錄一個flag值怪嫌,滿足以下條件時,將flag值清空

  • APP正常啟動10秒
  • 用戶正常退出應(yīng)用
  • 用戶主動從前臺切換到后臺

如果在啟動階段發(fā)生異常柳沙,則flag值不會清空岩灭,通過flag值就可以判斷客戶端是否異常退出,每次異常退出赂鲤,flag值都+1噪径。

2、安全模式的分級執(zhí)行策略

分為兩級安全模式数初,連續(xù)Crash 2次為一級安全模式找爱,連續(xù)Crash 2次及以上為二級安全模式。

業(yè)務(wù)線可以在一級安全模式中注冊行為泡孩,比如清空緩存數(shù)據(jù)车摄,再進入該模式時,會使用注冊行為嘗試修復(fù)客戶端 如果一級安全模式無法修復(fù)APP仑鸥,則進入二級安全模式將APP恢復(fù)到初次安裝狀態(tài)吮播,并將Document、Library锈候、Cache三個根目錄清空薄料。

3、熱修復(fù)執(zhí)行策略

只要發(fā)現(xiàn)配置中需要熱修復(fù)泵琳,APP就會同步阻塞進行熱修復(fù),保證修復(fù)的及時性

4摄职、灰度方案

灰度時誊役,配置中會包含灰度、正式兩份配置及其灰度概率 APP根據(jù)特定算法算出自己是否滿足灰度條件谷市,則使用灰度配置

易用性考量

1蛔垢、接入成本

完善文檔、接口簡潔

2迫悠、統(tǒng)一配置后臺

可按照APP鹏漆、版本配置

3、定制性

支持定制功能创泄,讓接入方來決定具體行為

4艺玲、灰度機制

5、數(shù)據(jù)分析

采用統(tǒng)一數(shù)據(jù)平臺鞠抑,為安全模式改進提供依據(jù)

6饭聚、快速測試

創(chuàng)建更多的針對性測試案例,如模擬連續(xù)Crash

7搁拙、異常熔斷

當(dāng)多次請求失敗則可讓網(wǎng)絡(luò)庫主動拒絕請求秒梳。

容災(zāi)方案集合路徑

功能開關(guān) -> 統(tǒng)跳中心 -> 動態(tài)修復(fù) -> 安全模式

五、穩(wěn)定性長效治理

要實現(xiàn)App穩(wěn)定性的長效治理箕速,我們需要從 開發(fā)階段 => 測試階段 => 合碼階段 => 發(fā)布階段 => 運維階段 這五個階段來做針對性地處理酪碘。

1、開發(fā)階段

  • 統(tǒng)一編碼規(guī)范盐茎、增強編碼功底兴垦、技術(shù)評審、CodeReview機制
  • 架構(gòu)優(yōu)化
  • 能力收斂
  • 統(tǒng)一容錯:如在網(wǎng)絡(luò)庫utils中統(tǒng)一對返回信息進行預(yù)校驗庭呜,如不合法就直接不走接下來的流程滑进。

2、測試階段

  • 功能測試募谎、自動化測試、回歸測試阴汇、覆蓋安裝
  • 特殊場景数冬、機型等邊界測試:如服務(wù)端返回異常數(shù)據(jù)、服務(wù)端宕機
  • 云測平臺:提供更全面的機型進行測試

3搀庶、合碼階段

  • 編譯檢測拐纱、靜態(tài)掃描
  • 預(yù)編譯流程、主流程自動回歸

4哥倔、發(fā)布階段

  • 多輪灰度
  • 分場景秸架、緯度全面覆蓋

5、運維階段

  • 靈敏監(jiān)控
  • 回滾咆蒿、降級策略
  • 熱修復(fù)东抹、本地容災(zāi)方案

六蚂子、穩(wěn)定性優(yōu)化問題

1、你們做了哪些穩(wěn)定性方面的優(yōu)化缭黔?

隨著項目的逐漸成熟食茎,用戶基數(shù)逐漸增多,DAU持續(xù)升高馏谨,我們遇到了很多穩(wěn)定性方面的問題别渔,對于我們技術(shù)同學(xué)遇到了很多的挑戰(zhàn),用戶經(jīng)常使用我們的App卡頓或者是功能不可用惧互,因此我們就針對穩(wěn)定性開啟了專項的優(yōu)化哎媚,我們主要優(yōu)化了三項:

  • Crash專項優(yōu)化
  • 性能穩(wěn)定性優(yōu)化
  • 業(yè)務(wù)穩(wěn)定性優(yōu)化

通過這三方面的優(yōu)化我們搭建了移動端的高可用平臺。同時喊儡,也做了很多的措施來讓App真正地實現(xiàn)了高可用抄伍。

2、性能穩(wěn)定性是怎么做的管宵?

  • 全面的性能優(yōu)化:啟動速度截珍、內(nèi)存優(yōu)化、繪制優(yōu)化
  • 線下發(fā)現(xiàn)問題箩朴、優(yōu)化為主
  • 線上監(jiān)控為主
  • Crash專項優(yōu)化

我們針對啟動速度岗喉,內(nèi)存、布局加載炸庞、卡頓钱床、瘦身、流量埠居、電量等多個方面做了多維的優(yōu)化查牌。

我們的優(yōu)化主要分為了兩個層次,即線上和線下滥壕,針對于線下呢纸颜,我們側(cè)重于發(fā)現(xiàn)問題,直接解決绎橘,將問題盡可能在上線之前解決為目的胁孙。而真正到了線上呢,我們最主要的目的就是為了監(jiān)控称鳞,對于各個性能緯度的監(jiān)控呢涮较,可以讓我們盡可能早地獲取到異常情況的報警。

同時呢冈止,對于線上最嚴(yán)重的性能問題性問題:Crash狂票,我們做了專項的優(yōu)化,不僅優(yōu)化了Crash的具體指標(biāo)熙暴,而且也盡可能地獲取了Crash發(fā)生時的詳細(xì)信息闺属,結(jié)合后端的聚合慌盯、報警等功能,便于我們快速地定位問題屋剑。

3润匙、業(yè)務(wù)穩(wěn)定性如何保障?

  • 數(shù)據(jù)采集 + 報警
  • 需要對項目的主流程與核心路徑進行埋點監(jiān)控唉匾,
  • 同時還需知道每一步發(fā)生了多少異常孕讳,這樣,我們就知道了所有業(yè)務(wù)流程的轉(zhuǎn)換率以及相應(yīng)界面的轉(zhuǎn)換率
  • 結(jié)合大盤巍膘,如果轉(zhuǎn)換率低于某個值厂财,進行報警
  • 異常監(jiān)控 + 單點追查
  • 兜底策略,如天貓安全模式

移動端業(yè)務(wù)高可用它側(cè)重于用戶功能完整可用峡懈,主要是為了解決一些線上一些異常情況導(dǎo)致用戶他雖然沒有崩潰璃饱,也沒有性能問題,但是呢肪康,只是單純的功能不可用的情況荚恶,我們需要對項目的主流程、核心路徑進行埋點監(jiān)控磷支,來計算每一步它真實的轉(zhuǎn)換率是多少谒撼,同時呢,還需要知道在每一步到底發(fā)生了多少異常雾狈。這樣我們就知道了所有業(yè)務(wù)流程的轉(zhuǎn)換率以及相應(yīng)界面的轉(zhuǎn)換率廓潜,有了大盤的數(shù)據(jù)呢,我們就知道了善榛,如果轉(zhuǎn)換率或者是某些監(jiān)控的成功率低于某個值辩蛋,那很有可能就是出現(xiàn)了線上異常,結(jié)合了相應(yīng)的報警功能移盆,我們就不需要等用戶來反饋了悼院,這個就是業(yè)務(wù)穩(wěn)定性保障的基礎(chǔ)。

同時呢味滞,對于一些特殊情況樱蛤,比如說,開發(fā)過程當(dāng)中或代碼中出現(xiàn)了一些catch代碼塊剑鞍,捕獲住了異常,讓程序不崩潰爽醋,這其實是不合理的蚁署,程序雖然沒有崩潰,當(dāng)時程序的功能已經(jīng)變得不可用蚂四,所以呢光戈,這些被catch的異常我們也需要上報上來哪痰,這樣我們才能知道用戶到底出現(xiàn)了什么問題而導(dǎo)致的異常。此外久妆,線上還有一些單點問題晌杰,比如說用戶點擊登錄一直進不去,這種就屬于單點問題筷弦,其實我們是無法找出其和其它問題的共性之處的肋演,所以呢,我們就必須要找到它對應(yīng)的詳細(xì)信息烂琴。

最后爹殊,如果發(fā)生了異常情況,我們還采取了一系列措施進行快速止損奸绷。(=>4)

4梗夸、如果發(fā)生了異常情況,怎么快速止損号醉?

  • 功能開關(guān)
  • 統(tǒng)跳中心
  • 動態(tài)修復(fù):熱修復(fù)反症、資源包更新
  • 自主修復(fù):安全模式

首先,需要讓App具備一些高級的能力畔派,我們對于任何要上線的新功能铅碍,要加上一個功能的開關(guān),通過配置中心下發(fā)的開關(guān)呢父虑,來決定是否要顯示新功能的入口。如果有異常情況士嚎,可以緊急關(guān)閉新功能的入口,那就可以讓這個App處于可控的狀態(tài)了爵嗅。

然后笨蚁,我們需要給App設(shè)立路由跳轉(zhuǎn),所有的界面跳轉(zhuǎn)都需要通過路由來分發(fā)括细,如果我們匹配到需要跳轉(zhuǎn)到有bug的這樣一個新功能時伪很,那我們就不跳轉(zhuǎn)了,或者是跳轉(zhuǎn)到統(tǒng)一的異常正處理中的界面奋单。如果這兩種方式都不可以锉试,那就可以考慮通過熱修復(fù)的方式來動態(tài)修復(fù),目前熱修復(fù)的方案其實已經(jīng)比較成熟了览濒,我們完全可以低成本地在我們的項目中添加熱修復(fù)的能力呆盖,當(dāng)然拖云,如果有些功能是由RN或WeeX來實現(xiàn)就更好了,那就可以通過更新資源包的方式來實現(xiàn)動態(tài)更新应又。而這些如果都不可以的話呢宙项,那就可以考慮自己去給應(yīng)用加上一個自主修復(fù)的能力,如果App啟動多次的話株扛,那就可以考慮清空所有的緩存數(shù)據(jù)尤筐,將App重置到安裝的狀態(tài),到了最嚴(yán)重的等級呢席里,可以阻塞主線程叔磷,此時一定要等App熱修復(fù)成功之后才允許用戶進入。

七奖磁、總結(jié)

Android穩(wěn)定性優(yōu)化是一個需要 長期投入改基,持續(xù)運營和維護 的一個過程,上文中我們不僅深入探討了Java Crash咖为、Native Crash和ANR的解決流程及方案秕狰,還分析了其內(nèi)部實現(xiàn)原理和監(jiān)控流程。到這里鸣哀,可以看到,要想做好穩(wěn)定性優(yōu)化挠羔,我們 必須對虛擬機運行破加、Linux信號處理和內(nèi)存分配 有一定程度的了解,只有深入了解這些底層知識锭环,我們才能比別人設(shè)計出更好的穩(wěn)定性優(yōu)化方案

作者:jsonchao
鏈接:https://juejin.cn/post/6844903972587716621
來源:掘金
著作權(quán)歸作者所有汽久。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)景醇,非商業(yè)轉(zhuǎn)載請注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末散劫,一起剝皮案震驚了整個濱河市获搏,隨后出現(xiàn)的幾起案子常熙,更是在濱河造成了極大的恐慌,老刑警劉巖墓贿,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異舱馅,居然都是意外死亡代嗤,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渠鸽,“玉大人憨奸,你說我怎么就攤上這事排宰。” “怎么了盐类?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵呻率,是天一觀的道長礼仗。 經(jīng)常有香客問我韭脊,道長,這世上最難降的妖魔是什么蔫饰? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮杖剪,結(jié)果婚禮上洛巢,老公的妹妹穿的比我還像新娘狼渊。我一直安慰自己城须,他們只是感情好砰琢,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般赞庶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肤京,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天温治,我揣著相機與錄音舟山,去河邊找鬼寒矿。 笑死,一個胖子當(dāng)著我的面吹牛啊终,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播傲须,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼蓝牲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了泰讽?” 一聲冷哼從身側(cè)響起例衍,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎已卸,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年玉锌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耍铜。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡臂寝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出酌儒,到底是詐尸還是另有隱情孽惰,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站掏愁,受9級特大地震影響萝衩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望验游。 院中可真熱鬧,春花似錦、人聲如沸谦铃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至女气,卻和暖如春杏慰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背炼鞠。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工缘滥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谒主。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓朝扼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親霎肯。 傳聞我的和親對象是個殘疾皇子擎颖,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 1. APP穩(wěn)定性問題匯總 2.1 卡頓/流暢度 概念與原理 View的繪制幀數(shù)保持60fps是最佳,這要求每幀的...
    小楠總閱讀 3,664評論 0 17
  • ANR(App Not Responding)基本上99%的App都有观游,即使是系統(tǒng)搂捧,也有system_anr,我相...
    LooperJing閱讀 37,121評論 15 118
  • 久違的晴天备典,家長會异旧。 家長大會開好到教室時,離放學(xué)已經(jīng)沒多少時間了提佣。班主任說已經(jīng)安排了三個家長分享經(jīng)驗吮蛹。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,520評論 16 22
  • 今天感恩節(jié)哎荤崇,感謝一直在我身邊的親朋好友。感恩相遇潮针!感恩不離不棄术荤。 中午開了第一次的黨會,身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,562評論 0 11
  • 可愛進取每篷,孤獨成精瓣戚。努力飛翔,天堂翱翔焦读。戰(zhàn)爭美好子库,孤獨進取。膽大飛翔矗晃,成就輝煌仑嗅。努力進取,遙望张症,和諧家園仓技。可愛游走...
    趙原野閱讀 2,726評論 1 1