前言
成為一名優(yōu)秀的Android開(kāi)發(fā)涌韩,需要一份完備的知識(shí)體系畔柔,在這里,讓我們一起成長(zhǎng)為自己所想的那樣~臣樱。
本篇是Android內(nèi)存優(yōu)化的進(jìn)階篇靶擦,難度會(huì)比較大,建議對(duì)內(nèi)存優(yōu)化不是非常熟悉的前仔細(xì)看看在前幾篇文章中雇毫,筆者曾經(jīng)寫(xiě)過(guò)的一篇Android性能優(yōu)化之內(nèi)存優(yōu)化玄捕,其中詳細(xì)分析了以下幾大模塊:
- Android的內(nèi)存管理機(jī)制
- 優(yōu)化內(nèi)存的意義
- 避免內(nèi)存泄漏
- 優(yōu)化內(nèi)存空間
- 圖片管理模塊的設(shè)計(jì)與實(shí)現(xiàn)
如果你對(duì)以上基礎(chǔ)內(nèi)容都比較了解了,那么我們便開(kāi)始接下來(lái)的Android內(nèi)存優(yōu)化探索之旅吧棚放。
一枚粘、內(nèi)存優(yōu)化相關(guān)概念
Android的給每個(gè)應(yīng)用進(jìn)程分配的內(nèi)存都是非常有限的,那么為什么不能把圖片下載來(lái)都放到磁盤(pán)中呢飘蚯?那是因?yàn)榉旁趦?nèi)存中馍迄,展示會(huì)更“快”,快的原因有兩點(diǎn):
- 硬件快:內(nèi)存本身讀取局骤、存入速度快攀圈。
- 復(fù)用快:解碼成果有效保存,復(fù)用時(shí)峦甩,直接使用解碼后對(duì)象赘来,而不是再做一次圖像解碼。
這里說(shuō)一下解碼的概念凯傲。Android系統(tǒng)要在屏幕上展示圖片的時(shí)候只認(rèn)“像素緩沖”犬辰,而這也是大多數(shù)操作系統(tǒng)的特征。而我們常見(jiàn)的jpg冰单,png等圖片格式幌缝,都是把“像素緩沖”使用不同的手段壓縮后的結(jié)果,所以這些格式的圖片诫欠,要在設(shè)備上展示狮腿,就必須經(jīng)過(guò)一次解碼,它的執(zhí)行速度會(huì)受圖片壓縮比呕诉、尺寸等因素影響缘厢。(官方建議:把從內(nèi)存淘汰的圖片,降低壓縮比存儲(chǔ)到本地甩挫,以備后用贴硫,這樣可以最大限度地降低以后復(fù)用時(shí)的解碼開(kāi)銷。)
接下來(lái),我們來(lái)了解一下內(nèi)存優(yōu)化的一些重要概念英遭。
手機(jī)RAM:
手機(jī)不使用PC的DDR內(nèi)存间护,采用的是LPDDR RAM,即”低功耗雙倍數(shù)據(jù)速率內(nèi)存“挖诸。
LPDDR系列的帶寬 = 時(shí)鐘頻率 ??內(nèi)存總線位數(shù) / 8
LPDDR4 = 1600MHZ ??64 / 8 ??雙倍速率 = 25.6GB/s汁尺。
那么內(nèi)存占用是否越少越好?
當(dāng)系統(tǒng)內(nèi)存充足的時(shí)候多律,我們可以多用一些獲得更好的性能痴突。當(dāng)系統(tǒng)內(nèi)存不足的時(shí)候,希望可以做到”用時(shí)分配狼荞,及時(shí)釋放“辽装。
內(nèi)存優(yōu)化的緯度
對(duì)于Android內(nèi)存優(yōu)化來(lái)說(shuō)又可以細(xì)分為兩個(gè)維度:
1、RAM優(yōu)化
主要是降低運(yùn)行時(shí)內(nèi)存相味。它的目的如下:
- 防止應(yīng)用發(fā)生OOM拾积。
- 降低應(yīng)用由于內(nèi)存過(guò)大被LMK機(jī)制殺死的概率。
- 避免不合理使用內(nèi)存導(dǎo)致GC次數(shù)增多丰涉,從而導(dǎo)致應(yīng)用發(fā)生卡頓拓巧。
2、ROM優(yōu)化
降低應(yīng)用占ROM的體積一死。APK瘦身肛度。它的目的為:
- 降低應(yīng)用占用空間,避免因ROM空間不足導(dǎo)致程序無(wú)法安裝
內(nèi)存問(wèn)題
那么摘符,內(nèi)存問(wèn)題主要是有哪幾類呢贤斜?下面我來(lái)一一敘述:
1策吠、內(nèi)存抖動(dòng)
內(nèi)存波動(dòng)圖形呈鋸齒張逛裤、GC導(dǎo)致卡頓。
這個(gè)問(wèn)題在Dalvik虛擬機(jī)上會(huì)更加明顯猴抹,而ART虛擬機(jī)在內(nèi)存管理跟回收策略上都做了大量?jī)?yōu)化带族,內(nèi)存分配和GC效率相比提升了5~10倍。
2蟀给、內(nèi)存泄漏
對(duì)象被持有導(dǎo)致無(wú)法釋放或不能按照對(duì)象正常的生命周期進(jìn)行釋放蝙砌。
可用內(nèi)存減少、頻繁GC跋理,容易導(dǎo)致內(nèi)存泄漏择克。
3、內(nèi)存溢出
OOM前普、程序異常肚邢。
二、常見(jiàn)工具選擇
在內(nèi)存優(yōu)化的上一篇我們已經(jīng)介紹過(guò)了相關(guān)的工具,這里再簡(jiǎn)單回憶一下骡湖。
1贱纠、Memory Profiler
它的作用如下:
- 實(shí)時(shí)圖表展示應(yīng)用內(nèi)存使用量
- 識(shí)別內(nèi)存泄漏、抖動(dòng)等
- 提供捕獲堆轉(zhuǎn)儲(chǔ)响蕴、強(qiáng)制GC以及根據(jù)內(nèi)存分配的能力
它的優(yōu)點(diǎn)即:
- 方便直觀
- 線下使用
2谆焊、Memory Analyzer
強(qiáng)大的Java Heap分析工具,查找內(nèi)存泄漏及內(nèi)存占用
生成整體報(bào)告浦夷、分析問(wèn)題等辖试。建議線下深入使用。
3军拟、LeakCanary
自動(dòng)內(nèi)存泄漏檢測(cè)神器剃执。僅用于線下集成。
它的缺點(diǎn)比較明顯懈息,雖然使用了idleHandler與多進(jìn)程肾档,但是dumphprof的SuspendAll Thread的特性依然會(huì)導(dǎo)致應(yīng)用卡頓。
在三星等手機(jī)辫继,系統(tǒng)會(huì)緩存最后一個(gè)Activity怒见,此時(shí)應(yīng)該采用更嚴(yán)格的檢測(cè)模式。
4姑宽、那么如何定制線上的LeakCanary遣耍?
定制LeakCanary其實(shí)就是對(duì)haha組件來(lái)進(jìn)行定制。haha庫(kù)是square出品的一款自動(dòng)分析Android堆棧的java庫(kù)炮车。haha庫(kù)的鏈接地址舵变。
它的基本用法如下所示:
// 導(dǎo)出堆棧文件
File heapDumpFile = ...
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
// 根據(jù)堆棧文件創(chuàng)建出內(nèi)存映射文件緩沖區(qū)
DataBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
// 根據(jù)文件緩存區(qū)創(chuàng)建出對(duì)應(yīng)的快照
Snapshot snapshot = Snapshot.createSnapshot(buffer);
// 從快照中獲取指定的類
ClassObj someClass = snapshot.findClass("com.example.SomeClass");
在實(shí)現(xiàn)線上版的LeakCanary的時(shí)候主要要做2個(gè)工作:
- 1、在過(guò)程中加上對(duì)大對(duì)象的分析過(guò)程瘦穆。
- 2纪隙、解決掉將hprof文件映射到內(nèi)存中的時(shí)候可能內(nèi)存暴漲的問(wèn)題。
5扛或、實(shí)現(xiàn)內(nèi)存泄漏監(jiān)控閉環(huán)
在實(shí)現(xiàn)了線上版的LeakCanary之后绵咱,就需要將線上版的LeakCanary與服務(wù)器和前端頁(yè)面結(jié)合起來(lái)。例如熙兔,當(dāng)LeakCanary上發(fā)現(xiàn)內(nèi)存泄漏時(shí)悲伶,手機(jī)將上傳內(nèi)存快照至服務(wù)器,此時(shí)服務(wù)器分析Hprof住涉,如果不是系統(tǒng)原因?qū)е抡`報(bào)則通過(guò)git得到該最近修改人麸锉,最后將內(nèi)存泄漏bug單提交給負(fù)責(zé)人。該負(fù)責(zé)人通過(guò)前端實(shí)現(xiàn)的bug單系統(tǒng)即可看到自己新增的bug舆声。
三花沉、Android內(nèi)存管理機(jī)制回顧
ART和Dalvik虛擬機(jī)使用分頁(yè)和內(nèi)存映射來(lái)管理內(nèi)存。下面我們先從Java的內(nèi)存分配開(kāi)始說(shuō)起。
1主穗、Java內(nèi)存分配
Java的內(nèi)存分配區(qū)域?yàn)槿缦聨撞糠郑?/p>
- 方法區(qū):主要存放靜態(tài)常量
- 虛擬機(jī)棧:Java變量引用
- 本地方法棧:native變量引用
- 堆:對(duì)象
- 程序計(jì)數(shù)器:計(jì)算當(dāng)前線程的當(dāng)前方法執(zhí)行到多少行
2泻拦、Java內(nèi)存回收算法
1、標(biāo)記-清除算法
流程可簡(jiǎn)述為兩步:
- 標(biāo)記所有需要回收的對(duì)象
- 統(tǒng)一回收所有被標(biāo)記的對(duì)象
它的優(yōu)點(diǎn)實(shí)現(xiàn)比較簡(jiǎn)單忽媒,缺點(diǎn)也很明顯:
- 標(biāo)記争拐、清除效率不高
- 產(chǎn)生大量?jī)?nèi)存碎片
2、復(fù)制算法
流程可簡(jiǎn)述為三步:
- 將內(nèi)存劃分為大小相等的兩塊
- 一塊內(nèi)存用完之后復(fù)制存活對(duì)象到另一塊
- 清理另一塊內(nèi)存
它的優(yōu)點(diǎn)為 實(shí)現(xiàn)簡(jiǎn)單晦雨,運(yùn)行高效架曹,每次僅需遍歷標(biāo)記一半的內(nèi)存區(qū)域。而缺點(diǎn)則會(huì)浪費(fèi)一半空間闹瞧,代價(jià)大绑雄。
3、標(biāo)記-整理算法
流程可簡(jiǎn)述為三步:
- 標(biāo)記過(guò)程與”標(biāo)記-清除算法“一樣
- 存活對(duì)象往一端進(jìn)行移動(dòng)
- 清理其余內(nèi)存
它的優(yōu)點(diǎn)如下:
- 避免標(biāo)記-清除導(dǎo)致的內(nèi)存碎片
- 避免復(fù)制算法的空間浪費(fèi)
4奥邮、分代收集算法
現(xiàn)在主流的虛擬機(jī)一般用的比較多的還是分帶收集算法万牺,它具有如下特點(diǎn):
- 結(jié)合多種算法優(yōu)勢(shì)
- 新生代對(duì)象存活率低,復(fù)制
- 老年代對(duì)象存活率高洽腺,標(biāo)記-整理
3脚粟、Android內(nèi)存管理機(jī)制
Android中的內(nèi)存是彈性分配的,分配值與最大值受具體設(shè)備影響蘸朋。
對(duì)于OOM場(chǎng)景其實(shí)由細(xì)分為兩種核无,一種是內(nèi)存真正不足
了,二另一種則是可用內(nèi)存不足藕坯。要注意一下這兩種的區(qū)分团南。
以Android中的虛擬機(jī)的角度來(lái)說(shuō),我們要清楚Dalvik與Art區(qū)別炼彪,Dalvik僅固定一種回收算法吐根,而Art回收算法可運(yùn)行期選擇,并且霹购,Art具備內(nèi)存整理能力佑惠,減少內(nèi)存空洞朋腋。
最后齐疙,LMK機(jī)制(Low Memory killer)保證了進(jìn)程資源的合理利用,它的實(shí)現(xiàn)原理主要是根據(jù)進(jìn)程分類和回收收益來(lái)綜合決定的旭咽。
四贞奋、內(nèi)存抖動(dòng)
當(dāng)內(nèi)存頻繁分配和回收導(dǎo)致內(nèi)存不穩(wěn)定,就會(huì)出現(xiàn)內(nèi)存抖動(dòng)穷绵,它通常表現(xiàn)為 頻繁GC轿塔、內(nèi)存曲線呈鋸齒狀。
它的危害也很嚴(yán)重,通常會(huì)導(dǎo)致頁(yè)面卡頓勾缭,甚至造成OOM揍障。
那么為什么內(nèi)存抖動(dòng)會(huì)導(dǎo)致OOM?
主要原因有兩點(diǎn):
- 頻繁創(chuàng)建對(duì)象俩由,導(dǎo)致內(nèi)存不足及碎片(不連續(xù))
- 不連續(xù)的內(nèi)存片無(wú)法被分配毒嫡,導(dǎo)致OOM
內(nèi)存抖動(dòng)解決實(shí)戰(zhàn)
點(diǎn)擊按鈕使用handler發(fā)送一個(gè)空消息,handler的handleMessage接收到消息后創(chuàng)建內(nèi)存抖動(dòng):即在for循環(huán)創(chuàng)建100個(gè)容量為10萬(wàn)的strings數(shù)組并在30ms后繼續(xù)發(fā)送空消息幻梯。
一般使用Memory Profiler或CPU Profiler結(jié)合代碼排查即可找到內(nèi)存抖動(dòng)出現(xiàn)的地方兜畸。
通常的技巧就是著重查看循環(huán)或頻繁調(diào)用的地方。
下面列舉一些導(dǎo)致內(nèi)存抖動(dòng)的常見(jiàn)案例:
1碘梢、字符串使用加號(hào)拼接:
- 使用StringBuilder替代咬摇。
- 初始化時(shí)設(shè)置容量,減少StringBuilder的擴(kuò)容煞躬。
2肛鹏、資源復(fù)用
- 使用全局緩存池,以重用頻繁申請(qǐng)和釋放的對(duì)象恩沛。
- 注意結(jié)束使用后龄坪,需要手動(dòng)釋放對(duì)象池中的對(duì)象。
3复唤、減少不合理的對(duì)象創(chuàng)建
- ondraw健田、getView中對(duì)象的創(chuàng)建盡量進(jìn)行復(fù)用。
- 避免在循環(huán)中不斷創(chuàng)建局部變量佛纫。
4妓局、使用合理的數(shù)據(jù)結(jié)構(gòu)
使用SparseArray類族來(lái)替代HashMap。
五呈宇、內(nèi)存優(yōu)化體系搭建
在開(kāi)始我們今天正式的主題之前好爬,我們先來(lái)回歸一下內(nèi)存泄漏的概念與解決技巧。
所謂的內(nèi)存泄漏就是內(nèi)存中存在已經(jīng)沒(méi)有用的對(duì)象甥啄。它的表現(xiàn)一般為 內(nèi)存抖動(dòng)存炮、可用內(nèi)存逐漸減少。
它的危害即會(huì)導(dǎo)致內(nèi)存不足蜈漓、GC頻繁穆桂、OOM。
內(nèi)存泄漏的分析一般可簡(jiǎn)述為兩步:
- 1、使用Memory Profiler初步觀察。
- 2锨并、通過(guò)Memory Analyzer結(jié)合代碼確認(rèn)泳姐。
1、MAT回顧
MAT查找內(nèi)存泄漏
首先找到當(dāng)前Activity布隔,在Histogram中選擇其List Objects中的 with incoming reference(哪些強(qiáng)引用引向了我)余爆,然后選擇當(dāng)前的一個(gè)Path to GC Roots/Merge to GC Roots的exclude All 弱軟虛引用斋配。最后找到最后的泄漏對(duì)象在左下角下會(huì)有一個(gè)小圓圈茴迁。
MAT的關(guān)鍵使用細(xì)節(jié)
要全面掌握MAT的用法寄悯,必須先了解下面的一些細(xì)節(jié):
- 善于使用Regex查找對(duì)應(yīng)泄漏類。
- 使用group by package查找對(duì)應(yīng)包下的具體類堕义。
其次热某,要明白with outgoing references和with incoming references的區(qū)別。
with outgoing references為它引用了哪些對(duì)象胳螟,with incoming references為哪些對(duì)象引用了它昔馋。
還需要了解Shallow Heap和Retained Heap的區(qū)別。
Shallow Heap為對(duì)象自身占用的內(nèi)存糖耸,而Retained Heap則還包含對(duì)象引用的對(duì)象所占用的內(nèi)存秘遏。
除此之外,MAT共有5個(gè)關(guān)鍵組件幫助我們?nèi)シ治鰞?nèi)存方面的問(wèn)題嘉竟,他們分別是Dominator_tree
邦危、Histogram、thread_overview舍扰、Top Consumers倦蚪、Leak Suspects。下面我們簡(jiǎn)單地了解一下它們边苹。
Dominator(支配者):
如果從GC Root到達(dá)對(duì)象A的路徑上必須經(jīng)過(guò)對(duì)象B陵且,那么B就是A的支配者。
Histogram和dominator_tree的區(qū)別:
- Histogram顯示Shallow Heap个束、Retained Heap慕购、Objects,而dominator_tree顯示的是Shallow Heap茬底、Retained Heap沪悲、Percentage。
- Histogram基于類的角度阱表,dominator_tree是基于實(shí)例的角度殿如。Histogram不會(huì)具體顯示每一個(gè)泄漏的對(duì)象,而dominator_tree會(huì)最爬。
thread_overview
查看有多少線程和線程的Shallow Heap涉馁、Retained Heap、Context Class Loader與is Daemon烂叔。
Top Consumers
通過(guò)圖形的形式列出占用內(nèi)存比較多的對(duì)象谨胞。
在下方的Biggest Objects還可以查看其相對(duì)比較詳細(xì)的信息固歪,如Shallow Heap蒜鸡、Retained Heap胯努。
Leak Suspects
列出有內(nèi)存泄漏的地方,點(diǎn)擊Details可以查看其產(chǎn)生內(nèi)存泄漏的引用鏈逢防。
最后叶沛,我列舉一些內(nèi)存泄漏優(yōu)化的技巧:
- 1、使用類似Hack的方式修復(fù)系統(tǒng)內(nèi)存泄漏:
LeakCanary的AndroidExcludeRefs列出了一些由于系統(tǒng)原因?qū)е乱脽o(wú)法釋放的例子忘朝,可使用類似Hack的方式去修復(fù)灰署。 - 2、Activity的兜底內(nèi)存回收策略:
在Activity的onDestory中遞歸釋放其引用到的Bitmap局嘁、DrawingCache等資源溉箕,降低發(fā)生內(nèi)存泄漏對(duì)應(yīng)用內(nèi)存的壓力。
2悦昵、建立線上內(nèi)存泄漏監(jiān)控組件:使用定制化的LeakCanary
在線上也可以使用類似LeakCanary的自動(dòng)化檢測(cè)方案肴茄,但是需要對(duì)生成的Hprof內(nèi)存快照文件做一些優(yōu)化,裁剪大部分圖片對(duì)應(yīng)的byte數(shù)據(jù)以減少文件開(kāi)銷但指,最后使用7zip壓縮寡痰,一般可節(jié)省90%大小。
3棋凳、建立線上OOM監(jiān)控組件:Probe
美團(tuán)Android內(nèi)存泄漏自動(dòng)化鏈路分析組件Probe
在OOM時(shí)生成Hprof內(nèi)存快照拦坠,然后通過(guò)單獨(dú)進(jìn)程對(duì)這個(gè)文件做進(jìn)一步分析。
它的缺點(diǎn)比較多剩岳,具體為如下幾點(diǎn):
- 在崩潰的時(shí)候生成內(nèi)存快照容易導(dǎo)致二次崩潰贞滨。
- 部分手機(jī)生成Hprof快照比較耗時(shí)。
- 部分OOM是由虛擬內(nèi)存不足導(dǎo)致拍棕。
在實(shí)現(xiàn)自動(dòng)化鏈路分析組件Probe的過(guò)程中主要要解決如下問(wèn)題:
1疲迂、鏈路分析時(shí)間過(guò)長(zhǎng)
- 使用鏈路歸并,將具有相同層級(jí)與結(jié)構(gòu)的鏈路進(jìn)行合并莫湘。
- 使用自適應(yīng)擴(kuò)容法尤蒿,通過(guò)不斷比較現(xiàn)有鏈路和新鏈路,結(jié)合擴(kuò)容因子幅垮,逐漸完善為完整的泄漏鏈路腰池。
2、分析進(jìn)程占用內(nèi)存過(guò)大
分析進(jìn)程占用的內(nèi)存跟內(nèi)存快照文件的大小不成正相關(guān)忙芒,而跟內(nèi)存快照文件的Instance數(shù)量呈正相關(guān)示弓。所以應(yīng)該盡可能排除不需要的Instance實(shí)例。
Prope分析流程
1呵萨、hprof 映射到內(nèi)存 -> 解析成Snapshot & 計(jì)數(shù)壓縮:
解析后的Snapshot中的Heap有四種類型奏属,具體為:
- DefaultHeap
- ImageHeap
- App Heap:包括ClassInstance、ClassObj潮峦、ArrayInstance囱皿、RootObj勇婴。
- System Heap
解析完后使用了計(jì)數(shù)壓縮策略,對(duì)相同的Instance使用計(jì)數(shù)嘱腥,以減少占用內(nèi)存耕渴。超過(guò)計(jì)數(shù)閾值的需要計(jì)入計(jì)數(shù)桶(計(jì)數(shù)桶記錄了丟棄個(gè)數(shù)和每個(gè)Instance的大小)齿兔。
2橱脸、生成Dominator Tree。
3分苇、計(jì)算RetainSize添诉。
4、生成Reference鏈 & 基礎(chǔ)數(shù)據(jù)類型增強(qiáng):
如果對(duì)象是基礎(chǔ)數(shù)據(jù)類型医寿,會(huì)將自身的RetainSize累加到父節(jié)點(diǎn)上吻商,將懷疑對(duì)象替換為它的父節(jié)點(diǎn)。
5糟红、鏈路歸并艾帐。
6、計(jì)數(shù)桶補(bǔ)償 & 基礎(chǔ)數(shù)據(jù)類型和父節(jié)點(diǎn)融合:
使用計(jì)數(shù)補(bǔ)償策略計(jì)算RetainSize盆偿,主要是判斷對(duì)象是否在計(jì)數(shù)桶中柒爸,如果在的話則將丟棄的個(gè)數(shù)和大小補(bǔ)償?shù)綄?duì)象上,累積計(jì)算RetainSize事扭,最后對(duì)RetainSize排序以查找可疑對(duì)象捎稚。
7、排序擴(kuò)容求橄。
8今野、查找泄露鏈路。
總體架構(gòu)圖如下:
4罐农、實(shí)現(xiàn)單機(jī)版的Profile - Memory自動(dòng)化內(nèi)存分析
在配置的時(shí)候要注意兩個(gè)問(wèn)題:
- 1条霜、liballoc-lib.so在構(gòu)建后工程的build->intermediates->cmake目錄下。將對(duì)應(yīng)的cpu abi目錄拷貝到新建的libs目錄下涵亏。
- 2宰睡、在DumpPrinter Java庫(kù)的build.gradle中的jar閉包中需要加入以下代碼以識(shí)別源碼路徑
sourceSets.main.java.srcDirs = ['src']
具體的使用步驟如下:
1、點(diǎn)擊”開(kāi)
始記錄“按鈕可以看到觸發(fā)對(duì)象分配的記錄气筋,說(shuō)明對(duì)象已經(jīng)開(kāi)始記錄對(duì)象的分配拆内。
12-26 10:54:03.963 30450-30450/com.dodola.alloctrack I/AllocTracker: ====current alloc count 388=====
2、然后宠默,點(diǎn)擊多次”生成1000個(gè)對(duì)象“按鈕麸恍,當(dāng)對(duì)象達(dá)到設(shè)置的最大數(shù)量的時(shí)候觸發(fā)內(nèi)存dump,會(huì)得到保存數(shù)據(jù)路徑的日志搀矫。
12-26 10:54:03.963 30450-30450/com.dodola.alloctrack I/AllocTracker: ====current alloc count 388=====
12-26 10:56:45.103 30450-30450/com.dodola.alloctrack I/AllocTracker: saveARTAllocationData write file to /storage/emulated/0/crashDump/1577329005
3抹沪、可以看到數(shù)據(jù)保存在sdk下的crashDump目錄下刻肄。
4、此時(shí)采够,通過(guò)gradle task :buildAlloctracker任務(wù)編譯出存放在tools/DumpPrinter-1.0.jar的dump工具肄方,然后采用如下命令來(lái)將數(shù)據(jù)解析到dump_log.txt文件中冰垄。
java -jar tools/DumpPrinter-1.0.jar dump文件路徑 > dump_log.txt
5蹬癌、最后,就可以在dump_log.txt文件中看到解析出來(lái)的數(shù)據(jù)虹茶,如下所示:
Found 4949 records:
tid=1 byte[] (94208 bytes)
dalvik.system.VMRuntime.newNonMovableArray (Native method)
android.graphics.Bitmap.nativeCreate (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:975)
android.graphics.Bitmap.createBitmap (Bitmap.java:946)
android.graphics.Bitmap.createBitmap (Bitmap.java:913)
android.graphics.drawable.RippleDrawable.updateMaskShaderIfNeeded (RippleDrawable.java:776)
android.graphics.drawable.RippleDrawable.drawBackgroundAndRipples (RippleDrawable.java:860)
android.graphics.drawable.RippleDrawable.draw (RippleDrawable.java:700)
android.view.View.getDrawableRenderNode (View.java:17736)
android.view.View.drawBackground (View.java:17660)
android.view.View.draw (View.java:17467)
android.view.View.updateDisplayListIfDirty (View.java:16469)
android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:3905)
android.view.ViewGroup.dispatchGetDisplayList (ViewGroup.java:3885)
android.view.View.updateDisplayListIfDirty (View.java:16429)
android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:3905)
5逝薪、圖片監(jiān)控體系搭建
在介紹圖片監(jiān)控體系的搭建之前,首先我們來(lái)回顧下Android Bitmap內(nèi)存分配的變化:
在Android 3.0之前
- Bitmap對(duì)象存放在Java Heap蝴罪,而像素?cái)?shù)據(jù)是存放在Native內(nèi)存中的董济。
- 如果不手動(dòng)調(diào)用recycle,Bitmap Native內(nèi)存的回收完全依賴finalize函數(shù)回調(diào)要门,但是回調(diào)時(shí)機(jī)是不可控的虏肾。
Android 3.0 ~ Android 7.0
將Bitmap對(duì)象和像素?cái)?shù)據(jù)統(tǒng)一放到Java Heap中,即使不調(diào)用recycle欢搜,Bitmap像素?cái)?shù)據(jù)也會(huì)隨著對(duì)象一起被回收封豪。
Bitmap全部放在Java Heap中的缺點(diǎn)很明顯:
- 1、Bitmap是內(nèi)存消耗的大戶炒瘟,而Max Java Heap一般限制為256吹埠、512MB,Bitmap過(guò)大過(guò)多容易導(dǎo)致OOM疮装。
- 2缘琅、容易引起大量GC,沒(méi)有充分利用系統(tǒng)的可用內(nèi)存廓推。
Android 8.0及之后
- 使用了能夠輔助回收Native內(nèi)存的NativeAllocationRegistry刷袍,以實(shí)現(xiàn)將像素?cái)?shù)據(jù)放到Native內(nèi)存中,并且可以和Bitmap對(duì)象一起快速釋放樊展,最后做个,在GC的時(shí)候還可以考慮這些Bitmap內(nèi)存以防止被濫用。
- Android 8.0為了解決圖片內(nèi)存占用過(guò)多和圖像繪制效率過(guò)慢的問(wèn)題新增了硬件位圖Hardware Bitmap滚局。
那么居暖,如何將圖片內(nèi)存存放在Native中呢?
- 1藤肢、調(diào)用libandroid_runtime.so中的Bitmap構(gòu)造函數(shù)太闺,申請(qǐng)一張空的Native Bitmap。對(duì)于不同Android版本而言嘁圈,這里的獲取過(guò)程都有一些差異需要適配省骂。
- 2蟀淮、申請(qǐng)一張普通的Java Bitmap。
- 3钞澳、將Java Bitmap的內(nèi)容繪制到Native Bitmap中怠惶。
- 4、釋放Java Bitmap內(nèi)存轧粟。
我們都知道策治,當(dāng)系統(tǒng)內(nèi)存不足,LMK會(huì)根據(jù)OOM_adj開(kāi)始?xì)⑦M(jìn)程兰吟,從后臺(tái)通惫、桌面、服務(wù)混蔼、前臺(tái)履腋,直到手機(jī)重啟。并且惭嚣,如果頻繁申請(qǐng)釋放Java Bitmap也很容易導(dǎo)致內(nèi)存抖動(dòng)遵湖。對(duì)于這種種問(wèn)題,我們?nèi)绾卧u(píng)估內(nèi)存對(duì)應(yīng)用性能的影響呢晚吞?
主要從以下兩個(gè)方面進(jìn)行評(píng)估:
- 1延旧、崩潰中異常退出和OOM的比例。
- 2载矿、低內(nèi)存設(shè)備更容易出現(xiàn)內(nèi)存不足和卡頓垄潮,需要查看應(yīng)用中用戶的手機(jī)內(nèi)存在2GB以下所占的比例。
對(duì)于具體的優(yōu)化策略闷盔,我們可以從以下幾個(gè)方面來(lái)進(jìn)行弯洗。
1、設(shè)備分級(jí)
內(nèi)存優(yōu)化首先需要根據(jù)設(shè)備環(huán)境來(lái)綜合考慮逢勾,讓高端設(shè)備使用更多的內(nèi)存牡整,做到針對(duì)設(shè)備性能的好壞使用不同的內(nèi)存分配和回收策略。
使用類似device-year-class的策略對(duì)設(shè)備進(jìn)行分級(jí)溺拱,對(duì)于低端機(jī)用戶可以關(guān)閉復(fù)雜的動(dòng)畫(huà)或”重功能“逃贝,使用565格式的圖片或更小的緩存內(nèi)存等。
業(yè)務(wù)開(kāi)發(fā)人員需要考慮功能是否對(duì)低端機(jī)開(kāi)啟迫摔,在系統(tǒng)資源不夠時(shí)主動(dòng)去做降級(jí)處理沐扳。
2、建立統(tǒng)一的緩存管理組件
建立統(tǒng)一的緩存管理組件句占,合理使用OnTrimMemory回調(diào)沪摄,根據(jù)系統(tǒng)不同的狀態(tài)去釋放相應(yīng)的內(nèi)存。
在實(shí)現(xiàn)過(guò)程中,需要解決使用static LRUCache來(lái)緩存大尺寸Bitmap等問(wèn)題杨拐。
并且祈餐,在通過(guò)實(shí)際的測(cè)試后,發(fā)現(xiàn)onTrimMemory的ComponetnCallbacks2.TRIM_MEMORY_COMPLETE并不等價(jià)于onLowMemory哄陶,因此建議仍然要去監(jiān)聽(tīng)onLowMemory回調(diào)帆阳。
3、低端機(jī)避免使用多進(jìn)程
一個(gè)空進(jìn)程也會(huì)占用10MB內(nèi)存屋吨,低端機(jī)應(yīng)該盡可能減少使用多進(jìn)程蜒谤。
針對(duì)低端機(jī)用戶可以推出4MB的輕量級(jí)版本,如今日頭條極速版离赫、Facebook Lite芭逝。
4塌碌、統(tǒng)一圖片庫(kù)
需要收攏圖片的調(diào)用渊胸,避免使用Bitmap.createBitmap、BitmapFactory相關(guān)的接口創(chuàng)建Bitmap台妆,應(yīng)該使用自己的圖片框架翎猛。
5、線下大圖片檢測(cè)
在開(kāi)發(fā)過(guò)程中接剩,如果檢測(cè)到不合規(guī)的圖片使用(如圖片寬度超過(guò)View的寬度甚至圖片寬度)切厘,應(yīng)該立刻提示圖片所在的Activity和堆棧,讓開(kāi)發(fā)人員更快發(fā)現(xiàn)并解決問(wèn)題懊缺。在灰度和線上環(huán)境疫稿,可以將異常信息上報(bào)到后臺(tái),還可以計(jì)算超寬率(圖片超過(guò)屏幕大小所占圖片總數(shù)的比例)鹃两。
常規(guī)實(shí)現(xiàn):
繼承ImageView遗座,重寫(xiě)實(shí)現(xiàn)計(jì)算圖片大小。但是侵入性強(qiáng)俊扳,并且不通用途蒋。
下面介紹一下ARTHook的方案。
ARTHook優(yōu)雅檢測(cè)大圖
ARTHook馋记,即掛鉤号坡,用額外的代碼勾住原有的方法,以修改執(zhí)行邏輯梯醒,主要用于以下幾方面:
- 1宽堆、AOP變成
- 2、運(yùn)行時(shí)插樁
- 3茸习、性能分析
- 4畜隶、安全審計(jì)
具體我們是使用Epic來(lái)進(jìn)行Hook,Epic是一個(gè)虛擬機(jī)層面,以Java方法為粒度的運(yùn)行時(shí)Hook框架代箭。簡(jiǎn)單來(lái)說(shuō)墩划,它就是ART上的Dexposed,并且它目前支持Android 4.0~10.0嗡综。
Epic的使用可簡(jiǎn)述為:
1乙帮、在build.gradle中添加
compile 'me.weishu:epic:0.6.0'
2、繼承XC_MethodHook极景,實(shí)現(xiàn)Hook方法前后的邏輯察净。如監(jiān)控Java線程的創(chuàng)建和銷毀:
class ThreadMethodHook extends XC_MethodHook{
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Thread t = (Thread) param.thisObject;
Log.i(TAG, "thread:" + t + ", started..");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Thread t = (Thread) param.thisObject;
Log.i(TAG, "thread:" + t + ", exit..");
}
}
3、注入Hook好的方法:
DexposedBridge.findAndHookMethod(Thread.class, "run", new ThreadMethodHook());
知道了Epic的基本使用方法之后盼樟,我們便可以利用它來(lái)進(jìn)行大圖片的監(jiān)控報(bào)警了氢卡。
以Awesome-WanAndroid項(xiàng)目為例,首先晨缴,在WanAndroidApp的onCreate方法中添加如下代碼:
DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 這里找到所有通過(guò)ImageView的setImageBitmap方法設(shè)置的切入點(diǎn)镜沽,
// 其中最后一個(gè)參數(shù)ImageHook對(duì)象是繼承了XC_MethodHook類以便于
// 重寫(xiě)afterHookedMethod方法拿到相應(yīng)的參數(shù)進(jìn)行監(jiān)控邏輯的判斷
DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
}
});
接下來(lái),我們來(lái)實(shí)現(xiàn)我們的ImageHook類委刘,如下所示:
public class ImageHook extends XC_MethodHook {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 實(shí)現(xiàn)我們的邏輯
ImageView imageView = (ImageView) param.thisObject;
checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
}
private static void checkBitmap(Object thiz, Drawable drawable) {
if (drawable instanceof BitmapDrawable && thiz instanceof View) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
final View view = (View) thiz;
int width = view.getWidth();
int height = view.getHeight();
if (width > 0 && height > 0) {
// 圖標(biāo)寬高都大于view的2倍以上谁鳍,則警告
if (bitmap.getWidth() >= (width << 1)
&& bitmap.getHeight() >= (height << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
}
} else {
// 當(dāng)寬高度等于0時(shí),說(shuō)明ImageView還沒(méi)有進(jìn)行繪制稍途,使用ViewTreeObserver進(jìn)行大圖檢測(cè)的處理阁吝。
final Throwable stackTrace = new RuntimeException();
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int w = view.getWidth();
int h = view.getHeight();
if (w > 0 && h > 0) {
if (bitmap.getWidth() >= (w << 1)
&& bitmap.getHeight() >= (h << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
}
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
}
});
}
}
}
}
private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
String warnInfo = "Bitmap size too large: " +
"\n real size: (" + bitmapWidth + ',' + bitmapHeight + ')' +
"\n desired size: (" + viewWidth + ',' + viewHeight + ')' +
"\n call stack trace: \n" + Log.getStackTraceString(t) + '\n';
LogHelper.i(warnInfo);
}
}
在上面,我們重寫(xiě)了ImageHook的afterHookedMethod方法械拍,拿到了當(dāng)前的ImageView和要設(shè)置的Bitmap對(duì)象突勇,如果當(dāng)前ImageView的寬高大于0,我們便進(jìn)行大圖檢測(cè)的處理:ImageView的寬高都大于View的2倍以上坷虑,則警告甲馋,如果當(dāng)前ImageView的寬高等于0,則說(shuō)明ImageView還沒(méi)有進(jìn)行繪制猖吴,則使用ImageView的ViewTreeObserer獲取其寬高進(jìn)行大圖檢測(cè)的處理摔刁。至此,我們的大圖檢測(cè)檢測(cè)組件就實(shí)現(xiàn)了海蔽。
ARTHook方案實(shí)現(xiàn)小結(jié)
- 1共屈、無(wú)侵入性
- 2、通用性強(qiáng)
- 3党窜、兼容性問(wèn)題大拗引,開(kāi)源方案不能帶到線上環(huán)境。
6幌衣、線下重復(fù)圖片檢測(cè)
首先我們來(lái)了解一下這里的重復(fù)圖片所指的概念:
即Bitmap像素?cái)?shù)據(jù)完全一致矾削,但是有多個(gè)不同的對(duì)象存在壤玫。
使用內(nèi)存Hprof分析工具,自動(dòng)將重復(fù)Bitmap的圖片和引用堆棧輸出哼凯。具體實(shí)現(xiàn)步驟如下:
- 1欲间、獲取 android.graphics.Bitmap 實(shí)例對(duì)象的 mBuffer 為 ArrayInstance ,通過(guò) getValues 獲取數(shù)據(jù)為 Object 類型断部,后面計(jì)算 md5 需要為 byte[] 類型猎贴,所以通過(guò)反射的方式調(diào)用 ArrayInstance#asRawByteArray 直接返回 byte[] 數(shù)據(jù)。
- 2蝴光、根據(jù) mBuffer 的數(shù)據(jù)生成 png 圖片文件她渴,參考了 https://github.com/JetBrains/adt-tools-base/blob/master/ddmlib/src/main/java/com/android/ddmlib/BitmapDecoder.java 實(shí)現(xiàn)。
- 3蔑祟、獲取堆棧信息趁耗,直接使用LeakCanary獲取stack的方法,使用leakcanary-analyzer-1.6.2.jar 和 leakcanary-watcher-1.6.2.jar 這兩個(gè)庫(kù)文件疆虚。并用反射的方式調(diào)用了HeapAnalyzer#findLeakTrace 方法苛败。
其中,獲取堆棧的信息也可以直接使用haha庫(kù)來(lái)進(jìn)行獲取装蓬。這里簡(jiǎn)單說(shuō)一下使用haha庫(kù)獲取堆棧的流程著拭。
- 1纱扭、預(yù)備一個(gè)已經(jīng)存在重復(fù)bitmap的hprof文件牍帚。
- 2、利用HAHA庫(kù)上的MemoryMappedFileBuffer讀取hrpof文件 [關(guān)鍵代碼 new MemoryMappedFileBuffer(heapDumpFile) ]
- 3乳蛾、解析生成snapshot暗赶,獲取heap,這里我只獲取了app heap [關(guān)鍵代碼 snapshot.getHeaps(); heap.getName().equals(“app”) ]
- 4肃叶、從snapshot中根據(jù)指定class查找出所有的Bitmap Classes [關(guān)鍵代碼snapshot.findClasses(Bitmap.class.getName()) ]
- 5蹂随、從heap中獲得所有的Bitmap實(shí)例instance [關(guān)鍵代碼 clazz.getHeapInstances(heap.getId()) ]
- 6、根據(jù)instance中獲取所有的屬性信息Field[]因惭,并從Field[]查找出我們需要的”mWidth” “mHeight” “mBuffer”信息
- 7岳锁、通過(guò)”mBuffer”屬性即可獲取他們的hashcode來(lái)判斷相同
- 8、最后通過(guò)instance中mNextInstanceToGcRoot獲取整個(gè)引用鏈信息并打印蹦魔。
在實(shí)現(xiàn)圖片內(nèi)存監(jiān)控的過(guò)程中激率,應(yīng)注意一下兩點(diǎn):
- 1、在線上可以按照不同的系統(tǒng)勿决、屏幕分辨率等緯度去分析圖片內(nèi)存的占用情況乒躺。
- 2、在OOM崩潰時(shí)低缩,可以將圖片總內(nèi)存嘉冒、Top N圖片占用內(nèi)存寫(xiě)入崩潰日志。
7、建立全局Bitmap監(jiān)控
為了建立全局的Bitmap監(jiān)控讳推,我們必須對(duì)Bitmap的分配和回收進(jìn)行追蹤顶籽。我們先來(lái)看看Bitmap有哪些特點(diǎn):
- 創(chuàng)建場(chǎng)景比較單一:在Java層調(diào)用Bitmap.create或BitmapFactory等方法創(chuàng)建,可以封裝一層對(duì)Bitmap創(chuàng)建的接口银觅,注意要包含調(diào)用外部庫(kù)產(chǎn)生的Bitmap蜕衡。
- 創(chuàng)建頻率比較低。
- 和Java對(duì)象的生命周期一樣服從GC设拟,可以使用WeakReference來(lái)追蹤Bitmap的銷毀慨仿。
根據(jù)以上特點(diǎn),我們可以建立一套Bitmap的高性價(jià)比監(jiān)控組件:
- 1纳胧、首先镰吆,在接口層將所有創(chuàng)建出來(lái)的Bitmap放入一個(gè)WeakHashMap中,并記錄創(chuàng)建Bitmap的數(shù)據(jù)跑慕、堆棧等信息万皿,然后每隔一定時(shí)間查看WeakHashMap中有哪些Bitmap仍然存活來(lái)判斷是否出現(xiàn)Bitmap濫用或泄漏。
- 2核行、這個(gè)方案性能消耗很低牢硅,可以在正式環(huán)境中進(jìn)行。注意正式與測(cè)試環(huán)境需要采用不同程度的監(jiān)控芝雪。
6减余、建立全局的線程監(jiān)控組件
每個(gè)線程初始化都需要mmap一定的棧大小,在默認(rèn)情況下初始化一個(gè)線程需要mmap 1MB左右的內(nèi)存空間惩系,在32bit的應(yīng)用中有4g的vmsize位岔,實(shí)際能使用的有3g+,這樣一個(gè)進(jìn)程最大能創(chuàng)建的線程數(shù)可以達(dá)到3000個(gè)堡牡,但是linux對(duì)每個(gè)進(jìn)程可創(chuàng)建的線程數(shù)也有一定的限制(/proc/pid/limits)抒抬,并且不同廠商也能修改這個(gè)限制,超過(guò)該限制就會(huì)OOM晤柄。
對(duì)線程數(shù)量的限制擦剑,一定程度上可以避免OOM的發(fā)生。
線程監(jiān)控組件的實(shí)現(xiàn)原理
在線下或灰度的環(huán)境下通過(guò)一個(gè)定時(shí)器每隔10分鐘dump出應(yīng)用所有的線程相關(guān)信息芥颈,當(dāng)線程數(shù)超過(guò)當(dāng)前閾值時(shí)惠勒,將當(dāng)前的線程信息上報(bào)并預(yù)警。
7浇借、建立線上應(yīng)用內(nèi)存監(jiān)控體系
具體的相關(guān)數(shù)據(jù)獲取方式如下:
- 1捉撮、首先,ActivityManager的getProcessMemoryInfo -> Debug.MemoryInfo數(shù)據(jù)妇垢。
- 2巾遭、通過(guò)hook Debug.MemoryInfo的getMemoryStat方法(os v23及以上)可以獲得Memory Profiler中的多項(xiàng)數(shù)據(jù)肉康,進(jìn)而獲得細(xì)分內(nèi)存使用情況。
- 3灼舍、通過(guò)Runtime獲取DalvikHeap吼和。
- 4、通過(guò)Debug.getNativeHeapAllocatedSize獲取NativeHeap骑素。
對(duì)于監(jiān)控場(chǎng)景炫乓,需要?jiǎng)澐譃閮纱箢悾?/p>
1、常規(guī)內(nèi)存監(jiān)控
根據(jù)斐波那契數(shù)列每隔一段時(shí)間(max:30min)獲取內(nèi)存的使用情況献丑。內(nèi)存監(jiān)控方法有多種實(shí)現(xiàn)方式末捣,我們先來(lái)介紹幾種常規(guī)方式。
針對(duì)場(chǎng)景進(jìn)行線上Dump內(nèi)存的方式:
具體使用Debug.dumpHprofData()實(shí)現(xiàn)创橄。
其實(shí)現(xiàn)的流程為:
- 1箩做、超過(guò)最大內(nèi)存的80%
- 2、內(nèi)存Dump
- 3妥畏、回傳文件
- 4邦邦、MAT手動(dòng)分析
但是有如下缺點(diǎn):
- 1、Dump文件太大醉蚁,和對(duì)象數(shù)正相關(guān)燃辖,可以進(jìn)行裁剪。
- 2网棍、上傳失敗率高黔龟,分析困難。
LeakCanary帶到線上的方式:
預(yù)設(shè)泄漏懷疑點(diǎn)确沸,一旦發(fā)現(xiàn)泄漏進(jìn)行回傳捌锭。但這種實(shí)現(xiàn)方式缺點(diǎn)比較明顯:
- 不適合所有情況,需要預(yù)設(shè)懷疑點(diǎn)罗捎。
- 分析比較耗時(shí),容易導(dǎo)致OOM拉盾。
定制LeakCanary方式
定制LeakCanary需要解決以上產(chǎn)生的一些問(wèn)題桨菜,下面這里分別列出對(duì)應(yīng)的解決方案:
- 1、預(yù)設(shè)懷疑點(diǎn)->自動(dòng)找懷疑點(diǎn)捉偏。
- 2倒得、分析泄漏鏈路慢->分析Retain size大的對(duì)象。
- 3夭禽、分析OOM->對(duì)象裁剪霞掺,不全部加載到內(nèi)存。
2讹躯、低內(nèi)存監(jiān)控
- 利用onLowMemory菩彬、onTrimMemory監(jiān)聽(tīng)物理內(nèi)存警告缠劝。
- 代碼設(shè)置超過(guò)虛擬內(nèi)存大小最大限制的90%則直接觸發(fā)內(nèi)存警告。
- 對(duì)于監(jiān)控指標(biāo)骗灶,一般為:發(fā)生頻率惨恭、發(fā)生時(shí)各項(xiàng)內(nèi)存使用狀況、發(fā)生時(shí)App的當(dāng)前場(chǎng)景耙旦。
并且脱羡,為了準(zhǔn)確衡量?jī)?nèi)存性能,我們引入了內(nèi)存異常率和觸頂率的指標(biāo)免都。
內(nèi)存異常率
內(nèi)存UV異常率 = PSS 超過(guò)400MB的UV / 采集UV锉罐,PSS獲取:通過(guò)Debug.MemoryInfo绕娘。
如果出現(xiàn)新的內(nèi)存使用不當(dāng)或內(nèi)存泄漏的場(chǎng)景氓鄙,這個(gè)指標(biāo)會(huì)有所上漲。
觸頂率
內(nèi)存UV觸頂率 = Java堆占用超過(guò)最大堆限制的85%的UV / 采集UV
計(jì)算觸頂率的代碼如下所示:
long javaMax = Runtime.maxMemory();
long javaTotal = Runtime.totalMemory();
long javaUsed = javaTotal - runtime.freeMemory();
float proportion = (float) javaUsed / javaMax;
如果超過(guò)85%最大堆限制业舍,GC會(huì)變得更加頻發(fā)抖拦,容易造成OOM和卡頓。
這里小結(jié)一下舷暮,客戶端只負(fù)責(zé)上報(bào)數(shù)據(jù)态罪,由后臺(tái)來(lái)計(jì)算平均PSS、圖片內(nèi)存下面、Java內(nèi)存复颈、異常率、觸頂率等指標(biāo)值沥割,這樣便可以通過(guò)版本對(duì)比來(lái)監(jiān)控是否有新增內(nèi)存問(wèn)題耗啦。因此,建立線上監(jiān)控完整方案需包含以下幾點(diǎn):
- 待機(jī)內(nèi)存机杜、重點(diǎn)模塊內(nèi)存帜讲、OOM率。
- 整體及重點(diǎn)模塊GC次數(shù)椒拗、GC時(shí)間似将。
- 增強(qiáng)的LeakCanry自動(dòng)化內(nèi)存泄漏分析。
- 低內(nèi)存監(jiān)控模塊的設(shè)置蚀苛。
8在验、GC監(jiān)控組件搭建
通過(guò)Debug.startAllocCounting來(lái)監(jiān)控GC情況,注意有一定性能影響堵未。
在Android 6.0之前可以拿到內(nèi)存分配次數(shù)和大小以及GC次數(shù)腋舌,代碼如下所示:
long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();
并且,在Android 6.0后可以拿到更精準(zhǔn)的GC信息:
Debug.getRuntimeStat("art.gc.gc-count");
Debug.getRuntimeStat("art.gc.gc-time");
Debug.getRuntimeStat("art.gc.blocking-gc-count");
Debug.getRuntimeStat("art.gc.blocking-gc-time");
一般關(guān)注阻塞式GC的次數(shù)和耗時(shí)渗蟹,因?yàn)樗鼤?huì)暫停線程块饺,可能導(dǎo)致應(yīng)用發(fā)生卡頓赞辩。建議僅對(duì)重度場(chǎng)景使用。
9刨沦、設(shè)置內(nèi)存兜底策略
設(shè)置內(nèi)存兜底策略的目的诗宣,是為了在用戶無(wú)感知的情況下,在接近觸發(fā)系統(tǒng)異常前想诅,選擇合適的場(chǎng)景殺死進(jìn)程并將其重啟召庞,從而使得應(yīng)用內(nèi)存占用回到正常情況。
一般進(jìn)行執(zhí)行內(nèi)存兜底策略時(shí)需要滿足以下條件:
- 是否在主界面退到后臺(tái)且位于后臺(tái)時(shí)間超過(guò)30min来破。
- 當(dāng)前時(shí)間為早上2~5點(diǎn)篮灼。
- 不存在前臺(tái)服務(wù)(通知欄、音樂(lè)播放欄等情況)徘禁。
- java heap必須大于當(dāng)前進(jìn)程最大可分配的85% || native內(nèi)存大于800MB
- vmsize超過(guò)了4G(32bit)的85%诅诱。
- 非大量的流量消耗(不超過(guò)1M/min) && 進(jìn)程無(wú)大量CPU調(diào)度情況。
滿足以上條件則殺死當(dāng)前主進(jìn)程并通過(guò)push進(jìn)程重新拉起及初始化送朱。
10娘荡、內(nèi)存優(yōu)化的一些策略
下面列舉一些我在內(nèi)存優(yōu)化過(guò)程中常用的一些策略。
1驶沼、使bitmap資源在native中分配:
對(duì)于Android 2.x系統(tǒng)炮沐,使用反射將BitmapFactory.Options里面隱藏的inNativeAlloc打開(kāi)。
對(duì)于Android 4.x系統(tǒng)回怜,使用Fresco將bitmap資源在native中分配大年。
2、使用inSampleSize避免不必要的大圖加載玉雾。
3翔试、使用Glide、Fresco等圖片加載庫(kù)复旬,通過(guò)定制垦缅,在加載bitmap時(shí),若發(fā)生OOM赢底,則使用try catch將其捕獲失都,然后清除圖片cache,嘗試降低bitmap format(ARGB8888幸冻、RGB565、ARGB4444咳焚、ALPHA8)洽损。
4、前臺(tái)每隔3分鐘去獲取當(dāng)前應(yīng)用內(nèi)存占最大內(nèi)存的比例革半,超過(guò)設(shè)定的危險(xiǎn)閾值(如80%)則主動(dòng)釋放應(yīng)用cache(Bitmap為大頭)碑定,并且顯示地除去應(yīng)用的memory流码,以加速內(nèi)存收集的過(guò)程。
計(jì)算當(dāng)前應(yīng)用內(nèi)存占最大內(nèi)存的比例的代碼如下:
max = Runtime.getRuntime().maxMemory();
available = Runtime.getRuntime.totalMemory() - Runtime.getFreeMemory();
ratio =available / max;
顯示地除去應(yīng)用的memory延刘,以加速內(nèi)存收集的過(guò)程的代碼如下:
WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
5漫试、由于webview存在內(nèi)存系統(tǒng)泄漏,還有圖庫(kù)占用內(nèi)存過(guò)多的問(wèn)題碘赖,可以采用單獨(dú)的進(jìn)程驾荣。
6、應(yīng)用發(fā)生OOM時(shí)普泡,需要上傳更加詳細(xì)的內(nèi)存相關(guān)信息播掷。
7、當(dāng)應(yīng)用使用的Service不再使用時(shí)應(yīng)該銷毀它撼班,建議使用IntentServcie歧匈。
8、當(dāng)UI隱藏時(shí)釋放內(nèi)存
當(dāng)用戶切換到其它應(yīng)用并且你的應(yīng)用UI不再可見(jiàn)時(shí)砰嘁,應(yīng)該釋放應(yīng)用UI所占用的所有內(nèi)存資源件炉。這能夠顯著增加系統(tǒng)緩存進(jìn)程的能力,能夠提升用戶體驗(yàn)矮湘。
在所有UI組件都隱藏的時(shí)候會(huì)接收到Activity的onTrimMemory()回調(diào)并帶有參數(shù)TRIM_MEMORY_UI_HIDDEN斟冕。
9、謹(jǐn)慎使用第三方庫(kù)板祝,避免為了使用其中一兩個(gè)功能而導(dǎo)入一個(gè)大而全的解決方案宫静。
六、線下Native內(nèi)存泄漏監(jiān)控搭建
在Android 8.0之后券时,可以使用Address Sanitizer孤里、Malloc調(diào)試和Malloc鉤子進(jìn)行native內(nèi)存分析,參見(jiàn)native_memory
對(duì)于線下Native內(nèi)存泄漏監(jiān)控的建立橘洞,主要針對(duì)是否能重編so的情況來(lái)進(jìn)行記錄分配的內(nèi)存信息捌袜。
針對(duì)無(wú)法重編so的情況
- 使用PLT Hook攔截庫(kù)的內(nèi)存分配函數(shù),然后重定向到我們自己的實(shí)現(xiàn)后去記錄分配的內(nèi)存地址炸枣、大小虏等、來(lái)源so庫(kù)路徑等信息。
- 定期掃描分配與釋放釋放配對(duì)适肠,對(duì)于不配對(duì)的分配輸出上述記錄的信息霍衫。
針對(duì)可重編的so情況
- 通過(guò)GCC的”-finstrument-functions“參數(shù)給所有函數(shù)插樁,然后在樁中模擬調(diào)用棧的入棧與出棧操作侯养。
- 通過(guò)ld的”–warp“參數(shù)攔截內(nèi)存分配和釋放函數(shù)敦跌,重定向到我們自己的實(shí)現(xiàn)后記錄分配的內(nèi)存地址、大小逛揩、來(lái)源so以及插樁調(diào)用棧此刻的內(nèi)容柠傍。
- 定期掃描分配與釋放是否配對(duì)麸俘,對(duì)于不配對(duì)的分配輸出我們記錄的信息。
七惧笛、內(nèi)存優(yōu)化演進(jìn)
1从媚、自動(dòng)化測(cè)試階段
內(nèi)存達(dá)到閾值后自動(dòng)觸發(fā)Hprof Dump,將得到的Hprof存檔后由人工通過(guò)MAT進(jìn)行分析患整。
2拜效、LeakCanary
檢測(cè)和分析報(bào)告都在一起,批量自動(dòng)化測(cè)試和事后分析不太方便并级。
3拂檩、使用基于LeakCannary的改進(jìn)版ResourceCanary
它的主要特點(diǎn)如下:
1、分離檢測(cè)和分析兩部分流程
自動(dòng)化測(cè)試由測(cè)試平臺(tái)進(jìn)行嘲碧,分析則由監(jiān)控平臺(tái)的服務(wù)端離線完成大磺,再通知相關(guān)開(kāi)發(fā)解決問(wèn)題房轿。
2利耍、裁剪Hprof文件肺蔚,以降低后臺(tái)存儲(chǔ)Hprof的開(kāi)銷
獲取需要的類和對(duì)象相關(guān)的字符串信息即可,其它數(shù)據(jù)都可以在客戶端裁剪履婉,一般能Hprof大小會(huì)減小至原來(lái)的1/10左右煤篙。
小結(jié)
在研發(fā)階段需要不斷實(shí)現(xiàn)更多的工具和組件,以此系統(tǒng)化地提升自動(dòng)化程度毁腿,以最終提升發(fā)現(xiàn)問(wèn)題的效率辑奈。
八、內(nèi)存優(yōu)化工具
除了常用的內(nèi)存分析工具M(jìn)emory Profiler已烤、MAT鸠窗、LeakCanary之外,還有一些其它的內(nèi)存分析工具胯究,下面我將一一為大家進(jìn)行介紹稍计。
1、top
top命令是Linux下常用的性能分析工具裕循,能夠?qū)崟r(shí)顯示系統(tǒng)中各個(gè)進(jìn)程的資源占用狀況臣嚣,類似于Windows的任務(wù)管理器。top命令提供了實(shí)時(shí)的對(duì)系統(tǒng)處理器的狀態(tài)監(jiān)視剥哑。它將顯示系統(tǒng)中CPU最“敏感”的任務(wù)列表硅则。該命令可以按CPU使用、內(nèi)存使用和執(zhí)行時(shí)間對(duì)任務(wù)進(jìn)行排序株婴。
接下來(lái)抢埋,我們輸入以下命令查看top命令的用法:
quchao@quchaodeMacBook-Pro ~ % adb shell top --help
usage: top [-Hbq] [-k FIELD,] [-o FIELD,] [-s SORT] [-n NUMBER] [-d SECONDS] [-p PID,] [-u USER,]
Show process activity in real time.
-H Show threads
-k Fallback sort FIELDS (default -S,-%CPU,-ETIME,-PID)
-o Show FIELDS (def PID,USER,PR,NI,VIRT,RES,SHR,S,%CPU,%MEM,TIME+,CMDLINE)
-O Add FIELDS (replacing PR,NI,VIRT,RES,SHR,S from default)
-s Sort by field number (1-X, default 9)
-b Batch mode (no tty)
-d Delay SECONDS between each cycle (default 3)
-n Exit after NUMBER iterations
-p Show these PIDs
-u Show these USERs
-q Quiet (no header lines)
Cursor LEFT/RIGHT to change sort, UP/DOWN move list, space to force
update, R to reverse sort, Q to exit.
這里使用top僅顯示一次進(jìn)程信息,以便來(lái)講解進(jìn)程信息中各字段的含義督暂。
前四行是當(dāng)前系統(tǒng)情況整體的統(tǒng)計(jì)信息區(qū)揪垄。下面我們看每一行信息的具體意義。
第一行逻翁,Tasks — 任務(wù)(進(jìn)程)饥努,具體信息說(shuō)明如下:
系統(tǒng)現(xiàn)在共有729個(gè)進(jìn)程,其中處于運(yùn)行中的有1個(gè)八回,715個(gè)在休眠(sleep)酷愧,stoped狀態(tài)的有0個(gè),zombie狀態(tài)(僵尸)的有8個(gè)缠诅。
第二行,內(nèi)存狀態(tài)溶浴,具體信息如下:
5847124k total — 物理內(nèi)存總量(5.8GB)
5758016k used — 使用中的內(nèi)存總量(5.7GB)
89108k free — 空閑內(nèi)存總量(89MB)
112428k buffers — 緩存的內(nèi)存量 (112M)
第三行,swap交換分區(qū)信息管引,具體信息說(shuō)明如下:
2621436k total — 交換區(qū)總量(2.6GB)
612572k used — 使用的交換區(qū)總量(612MB)
2008864k free — 空閑交換區(qū)總量(2GB)
2657696k cached — 緩沖的交換區(qū)總量(2.6GB)
第四行士败,cpu狀態(tài)信息,具體屬性說(shuō)明如下:
800%cpu - 8核CPU褥伴。
39%user - 39%CPU被用戶進(jìn)程使用谅将。
0%nice - 優(yōu)先值為負(fù)的進(jìn)程占0%。
42%sys — 內(nèi)核空間占用CPU的百分比為42%重慢。
712%idle - 除IO等待時(shí)間以外的其它等待時(shí)間為712%饥臂。
0%iow - IO等待時(shí)間占0%。
0%irq - 硬中斷時(shí)間占0%似踱。
6%sirq - 軟中斷時(shí)間占0%隅熙。
對(duì)于內(nèi)存監(jiān)控,在top里我們要時(shí)刻監(jiān)控第三行swap交換分區(qū)的used核芽,如果這個(gè)數(shù)值在不斷的變化囚戚,說(shuō)明內(nèi)核在不斷進(jìn)行內(nèi)存和swap的數(shù)據(jù)交換,這是真正的內(nèi)存不夠用了狞洋。
在第五行及以下弯淘,就是各進(jìn)程(任務(wù))的狀態(tài)監(jiān)控,項(xiàng)目列信息說(shuō)明如下:
PID — 進(jìn)程id吉懊。
USER — 進(jìn)程所有者庐橙。
PR — 進(jìn)程優(yōu)先級(jí)。
NI — nice值借嗽。負(fù)值表示高優(yōu)先級(jí)态鳖,正值表示低優(yōu)先級(jí)。
VIRT — 進(jìn)程使用的虛擬內(nèi)存總量恶导。VIRT = SWAP + RES浆竭。
RES — 進(jìn)程使用的、未被換出的物理內(nèi)存大小。RES = CODE + DATA邦泄。
SHR — 共享內(nèi)存大小删窒。
S — 進(jìn)程狀態(tài)。D=不可中斷的睡眠狀態(tài)顺囊、R=運(yùn)行肌索、 S=睡眠、T=跟蹤/停止特碳、Z=僵尸進(jìn)程诚亚。
%CPU — 上次更新到現(xiàn)在的CPU時(shí)間占用百分比。
%MEM — 進(jìn)程使用的物理內(nèi)存百分比午乓。
TIME+ — 進(jìn)程使用的CPU時(shí)間總計(jì)站宗,單位1/100秒。
ARGS — 進(jìn)程名稱(命令名/命令行)益愈。
這里可以看到第一行的就是Awesome-WanAndroid這個(gè)應(yīng)用的進(jìn)程梢灭,它的進(jìn)程名稱為json.chao.com.w+,PID為23104腕唧,進(jìn)程所有者USER為u0_a714或辖,進(jìn)程優(yōu)先級(jí)PR為10,nice置NI為-10枣接。進(jìn)程使用的虛擬內(nèi)存總量VIRT為4.3GB颂暇,進(jìn)程使用的、未被換出的物理內(nèi)存大小RES為138M但惶,共享內(nèi)存大小SHR為66M耳鸯,進(jìn)程狀態(tài)S是睡眠狀態(tài),上次更新到現(xiàn)在的CPU時(shí)間占用百分比%CPU為21.2膀曾。進(jìn)程使用的物理內(nèi)存百分比%MEM為2.4%县爬,進(jìn)程使用的CPU時(shí)間TIME+為1:47.58/100小時(shí)。
2添谊、dumpsys meminfo
在講解dumpsys meminfo命令之前财喳,我們必須先了解下Android中的幾個(gè)內(nèi)存指標(biāo)的概念:
內(nèi)存指標(biāo) | 英文全稱 | 含義 | 等價(jià) | |
---|---|---|---|---|
USS | Unique Set Size | 物理內(nèi)存 | 進(jìn)程獨(dú)占的內(nèi)存 | |
PSS | Proportional Set Size | 物理內(nèi)存 | PSS = USS + 按比例包含共享庫(kù) | |
RSS | Resident Set Size | 物理內(nèi)存 | RSS= USS+ 包含共享庫(kù) | |
VSS | Virtual Set Size | 虛擬內(nèi)存 | VSS= RSS+ 未分配實(shí)際物理內(nèi)存 |
從上可知,它們之間內(nèi)存的大小關(guān)系為VSS >= RSS >= PSS >= USS斩狱。
RSS與PSS相似耳高,也包含進(jìn)程共享內(nèi)存,但比較麻煩的是RSS并沒(méi)有把共享內(nèi)存大小全都平分到使用共享的進(jìn)程頭上所踊,以至于所有進(jìn)程的RSS相加會(huì)超過(guò)物理內(nèi)存很多泌枪。而VSS是虛擬地址,它的上限與進(jìn)程的可訪問(wèn)地址空間有關(guān)秕岛,和當(dāng)前進(jìn)程的內(nèi)存使用關(guān)系并不大碌燕。比如有很多的map內(nèi)存也被算在其中误证,我們都知道,file的map內(nèi)存對(duì)應(yīng)的可能是一個(gè)文件或硬盤(pán)修壕,或者某個(gè)奇怪的設(shè)備愈捅,它與進(jìn)程使用內(nèi)存并沒(méi)有多少關(guān)系。
而PSS叠殷、USS最大的不同在于“共享內(nèi)存“(比如兩個(gè)App使用MMAP方式打開(kāi)同一個(gè)文件改鲫,那么打開(kāi)文件而使用的這部分內(nèi)存就是共享的),USS不包含進(jìn)程間共享的內(nèi)存林束,而PSS包含。這也造成了USS因?yàn)槿鄙俟蚕韮?nèi)存稽亏,所有進(jìn)程的USS相加要小于物理內(nèi)存大小的原因壶冒。
最早的時(shí)候官方就推薦使用PSS曲線圖來(lái)衡量App的物理內(nèi)存占用,而Android 4.4之后才加入U(xiǎn)SS截歉。但是PSS胖腾,有個(gè)很大的問(wèn)題,就是”共享內(nèi)存“瘪松,考慮一種情況咸作,如果A進(jìn)程與B進(jìn)程都會(huì)使用一個(gè)共享SO庫(kù),那么so庫(kù)中初始化所用掉的那部分內(nèi)存就會(huì)平分到A與B的頭上宵睦。但是A是在B之后啟動(dòng)的记罚,那么對(duì)于B的PSS曲線而言,在A啟動(dòng)的那一刻壳嚎,即使B沒(méi)有做任何事情桐智,也會(huì)出現(xiàn)一個(gè)比較大的階梯狀下滑,這會(huì)給用曲線圖分析軟件內(nèi)存的行為造成致命的麻煩烟馅。
USS雖然沒(méi)有這個(gè)問(wèn)題说庭,但是由于Dalvik虛擬機(jī)申請(qǐng)內(nèi)存牽扯到GC時(shí)延和多種GC策略,這些都會(huì)影響到曲線的異常波動(dòng)郑趁。比如異步GC是Android 4.0以上系統(tǒng)很重要的特性刊驴,但是GC什么時(shí)候結(jié)束?曲線什么時(shí)候”降低“寡润?就無(wú)法預(yù)計(jì)了捆憎。還有GC策略,什么時(shí)候開(kāi)始增加Dalvik虛擬機(jī)的預(yù)申請(qǐng)內(nèi)幕才能大性么(Dalvik啟動(dòng)時(shí)是由一個(gè)標(biāo)稱的start內(nèi)存大小的,為Java代碼運(yùn)行時(shí)預(yù)留攻礼,避免Java運(yùn)行時(shí)再申請(qǐng)而造成卡頓),但是這個(gè)預(yù)申請(qǐng)大小是動(dòng)態(tài)變化的栗柒,這一點(diǎn)也會(huì)造成USS忽大忽小礁扮。
了解完Android內(nèi)存的性能指標(biāo)之后知举,下面我們便來(lái)說(shuō)說(shuō)dumpsys meminfo這個(gè)命令的用法,首先我們輸入adb shell dumpsys meminfo -h查看它的幫助文檔:
quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo -h
meminfo dump options: [-a] [-d] [-c] [-s] [--oom] [process]
-a: include all available information for each process.
-d: include dalvik details.
-c: dump in a compact machine-parseable representation.
-s: dump only summary of application memory usage.
-S: dump also SwapPss.
--oom: only show processes organized by oom adj.
--local: only collect details locally, don't call process.
--package: interpret process arg as package, dumping all
processes that have loaded that package.
--checkin: dump data for a checkin
If [process] is specified it can be the name or
pid of a specific process to dump.
接著太伊,我們之間輸入adb shell dumpsys meminfo命令:
quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo
Applications Memory Usage (in Kilobytes):
Uptime: 257501238 Realtime: 257501238
// 根據(jù)進(jìn)程PSS占用值從大到小排序
Total PSS by process:
308,049K: com.tencent.mm (pid 3760 / activities)
225,081K: system (pid 2088)
189,038K: com.android.systemui (pid 2297 / activities)
188,877K: com.miui.home (pid 2672 / activities)
176,665K: com.plan.kot32.tomatotime (pid 22744 / activities)
175,231K: json.chao.com.wanandroid (pid 23104 / activities)
126,918K: com.tencent.mobileqq (pid 23741)
...
// 以oom來(lái)劃分雇锡,會(huì)詳細(xì)列舉所有的類別的進(jìn)程
Total PSS by OOM adjustment:
432,013K: Native
76,700K: surfaceflinger (pid 784)
59,084K: android.hardware.camera.provider@2.4-service (pid 743)
26,524K: transport (pid 23418)
25,249K: logd (pid 597)
11,413K: media.codec (pid 1303)
10,648K: rild (pid 1304)
9,283K: media.extractor (pid 1297)
...
661,294K: Persistent
225,081K: system (pid 2088)
189,038K: com.android.systemui (pid 2297 / activities)
103,050K: com.xiaomi.finddevice (pid 3134)
39,098K: com.android.phone (pid 2656)
25,583K: com.miui.daemon (pid 3078)
...
219,795K: Foreground
175,231K: json.chao.com.wanandroid (pid 23104 / activities)
44,564K: com.miui.securitycenter.remote (pid 2986)
246,529K: Visible
71,002K: com.sohu.inputmethod.sogou.xiaomi (pid 4820)
52,305K: com.miui.miwallpaper (pid 2579)
40,982K: com.miui.powerkeeper (pid 3218)
24,604K: com.miui.systemAdSolution (pid 7986)
14,198K: com.xiaomi.metoknlp (pid 3506)
13,820K: com.miui.voiceassist:core (pid 8722)
13,222K: com.miui.analytics (pid 8037)
7,046K: com.miui.hybrid:entrance (pid 7922)
5,104K: com.miui.wmsvc (pid 7887)
4,246K: com.android.smspush (pid 8126)
213,027K: Perceptible
89,780K: com.eg.android.AlipayGphone (pid 8238)
49,033K: com.eg.android.AlipayGphone:push (pid 8204)
23,181K: com.android.thememanager (pid 11057)
13,253K: com.xiaomi.joyose (pid 5558)
10,292K: com.android.updater (pid 3488)
9,807K: com.lbe.security.miui (pid 23060)
9,734K: com.google.android.webview:sandboxed_process0 (pid 11150)
7,947K: com.xiaomi.location.fused (pid 3524)
308,049K: Backup
308,049K: com.tencent.mm (pid 3760 / activities)
74,250K: A Services
59,701K: com.tencent.mm:push (pid 7234)
9,247K: com.android.settings:remote (pid 27053)
5,302K: com.xiaomi.drivemode (pid 27009)
199,638K: Home
188,877K: com.miui.home (pid 2672 / activities)
10,761K: com.miui.hybrid (pid 7945)
53,934K: B Services
35,583K: com.tencent.mobileqq:MSF (pid 14119)
6,753K: com.qualcomm.qti.autoregistration (pid 8786)
4,086K: com.qualcomm.qti.callenhancement (pid 26958)
3,809K: com.qualcomm.qti.StatsPollManager (pid 26993)
3,703K: com.qualcomm.qti.smcinvokepkgmgr (pid 26976)
692,588K: Cached
176,665K: com.plan.kot32.tomatotime (pid 22744 / activities)
126,918K: com.tencent.mobileqq (pid 23741)
72,928K: com.tencent.mm:tools (pid 18598)
68,208K: com.tencent.mm:sandbox (pid 27333)
55,270K: com.tencent.mm:toolsmp (pid 18842)
24,477K: com.android.mms (pid 27192)
23,865K: com.xiaomi.market (pid 27825)
...
// 按內(nèi)存的類別來(lái)進(jìn)行劃分
Total PSS by category:
957,931K: Native
284,006K: Dalvik
199,750K: Unknown
193,236K: .dex mmap
191,521K: .art mmap
110,581K: .oat mmap
101,472K: .so mmap
94,984K: EGL mtrack
87,321K: Dalvik Other
84,924K: Gfx dev
77,300K: GL mtrack
64,963K: .apk mmap
17,112K: Other mmap
12,935K: Ashmem
3,364K: Stack
2,343K: .ttf mmap
1,375K: Other dev
1,071K: .jar mmap
20K: Cursor
0K: Other mtrack
// 手機(jī)整體內(nèi)存使用情況
Total RAM: 5,847,124K (status normal)
Free RAM: 3,711,324K ( 692,588K cached pss + 2,428,616K cached kernel + 117,492K cached ion + 472,628K free)
Used RAM: 2,864,761K (2,408,529K used pss + 456,232K kernel)
Lost RAM: 184,330K
ZRAM: 174,628K physical used for 625,388K in swap (2,621,436K total swap)
Tuning: 256 (large 512), oom 322,560K, restore limit 107,520K (high-end-gfx)
根據(jù)dumpsys meminfo的輸出結(jié)果,可歸結(jié)為如下表格:
劃分類型 | 排序指標(biāo) | 含義 | |
---|---|---|---|
process | PSS | 以進(jìn)程的PSS從大到小依次排序顯示僚焦,每行顯示一個(gè)進(jìn)程锰提,一般用來(lái)做初步的競(jìng)品分析 | |
OOM adj | PSS | 展示當(dāng)前系統(tǒng)內(nèi)部運(yùn)行的所有Android進(jìn)程的內(nèi)存狀態(tài)和被殺順序,越靠近下方的進(jìn)程越容易被殺芳悲,排序按照一套復(fù)雜的算法立肘,算法涵蓋了前后臺(tái)、服務(wù)或節(jié)目名扛、可見(jiàn)與否谅年、老化等 | |
category | PSS | 以Dalvik/Native/.art mmap/.dex map等劃分并按降序列出各類進(jìn)程的總PSS分布情況 | |
total | - | 總內(nèi)存、剩余內(nèi)存肮韧、可用內(nèi)存融蹂、其他內(nèi)存 |
此外,為了查看單個(gè)App進(jìn)程的內(nèi)存信息弄企,我們可以輸入如下命令:
dumpsys meminfo <pid> // 輸出指定pid的某一進(jìn)程
dumpsys meminfo --package <packagename> // 輸出指定包名的進(jìn)程超燃,可能包含多個(gè)進(jìn)程
這里我們輸入adb shell dumpsys meminfo 23104這條命令,其中23104為Awesome-WanAndroid App的pid拘领,結(jié)果如下所示:
quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo 23104
Applications Memory Usage (in Kilobytes):
Uptime: 258375231 Realtime: 258375231
** MEMINFO in pid 23104 [json.chao.com.wanandroid] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 46674 46620 0 164 80384 60559 19824
Dalvik Heap 6949 6912 16 23 12064 6032 6032
Dalvik Other 7672 7672 0 0
Stack 108 108 0 0
Ashmem 134 132 0 0
Gfx dev 16036 16036 0 0
Other dev 12 0 12 0
.so mmap 3360 228 1084 27
.jar mmap 8 8 0 0
.apk mmap 28279 11328 11584 0
.ttf mmap 295 0 80 0
.dex mmap 7780 20 4908 0
.oat mmap 660 0 92 0
.art mmap 8509 8028 104 69
Other mmap 982 8 848 0
EGL mtrack 29388 29388 0 0
GL mtrack 14864 14864 0 0
Unknown 2532 2500 8 20
TOTAL 174545 143852 18736 303 92448 66591 25856
App Summary
Pss(KB)
------
Java Heap: 15044
Native Heap: 46620
Code: 29332
Stack: 108
Graphics: 60288
Private Other: 11196
System: 11957
TOTAL: 174545 TOTAL SWAP PSS: 303
Objects
Views: 171 ViewRootImpl: 1
AppContexts: 3 Activities: 1
Assets: 18 AssetManagers: 6
Local Binders: 32 Proxy Binders: 27
Parcel memory: 11 Parcel count: 45
Death Recipients: 1 OpenSSL Sockets: 0
WebViews: 0
SQL
MEMORY_USED: 371
PAGECACHE_OVERFLOW: 72 MALLOC_SIZE: 117
DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 60 109 151/32/18 /data/user/0/json.chao.com.wanandroid/databases/bugly_db_
4 20 19 0/15/1 /data/user/0/json.chao.com.wanandroid/databases/aws_wan_android.db
該命令輸出的進(jìn)程內(nèi)存概括意乓,我們應(yīng)該著重關(guān)注幾個(gè)點(diǎn),下面我將進(jìn)行一一講解院究。
1洽瞬、查看Native Heap的Heap Alloc與Dalvik Heap的Heap Alloc
我們可以查看Native Heap的Heap Alloc的數(shù)值變化,它表示native的內(nèi)存占用业汰,如果持續(xù)上升伙窃,則可能有泄漏,而Dalvik Heap的Heap Alloc則表示Java層的內(nèi)存占用样漆。
2为障、查看Views、Activities放祟、AppContexts數(shù)量變化情況
如果Views與Activities鳍怨、AppContexts持續(xù)上升,則表明有內(nèi)存泄漏的風(fēng)險(xiǎn)跪妥。
3鞋喇、SQL的MEMORY_USED與PAGECACHE_OVERFLOW
SQL的MEMOERY_USED表示數(shù)據(jù)庫(kù)使用的內(nèi)存,而PAGECACHE_OVERFLOW則表示溢出也使用的緩存眉撵,這個(gè)數(shù)值越小越好侦香。
4落塑、查看DATABASES信息
其中pgsz表示數(shù)據(jù)庫(kù)分頁(yè)大小,這里全是4KB罐韩;Lookaside(b)表示使用了多少個(gè)Lookaside的slots憾赁,可理解為內(nèi)存占用的大小散吵;而cache一欄中的 151/32/18 則分別表示分頁(yè)緩存命中次數(shù)/未命中次數(shù)/分頁(yè)緩存?zhèn)€數(shù)龙考,這里的未命中次數(shù)不應(yīng)該大于命中次數(shù)。
3矾睦、LeakInspector
LeakInspector是騰訊內(nèi)部的使用的一站式內(nèi)存泄漏解決方案晦款,它是Android手機(jī)經(jīng)過(guò)長(zhǎng)期積累和提煉、集內(nèi)存泄漏檢測(cè)顷锰、自動(dòng)修復(fù)系統(tǒng)Bug柬赐、自動(dòng)回收已泄露Activity內(nèi)資源、自動(dòng)分析GC鏈官紫、白名單過(guò)濾等功能于一體,并深度對(duì)接研發(fā)流程州藕、自動(dòng)分析責(zé)任人并提缺陷單的全鏈路體系束世。
那么LeakInspector與LeakCanary又有什么不同呢?
它們之間主要有四個(gè)方面的不同:
一床玻、檢測(cè)能力與原理方面不同
1毁涉、檢測(cè)能力
它們都支持對(duì)Activity、Fragment及其它自定義類的泄漏檢測(cè)锈死,但是LeakInspector還增加了Btiamp的檢測(cè)能力:
- 檢測(cè)有沒(méi)有在View上decode超過(guò)該View尺寸的圖片贫堰,若有則上報(bào)出現(xiàn)問(wèn)題的Activity及與其對(duì)應(yīng)的View id,并記錄它的個(gè)數(shù)與平均占用內(nèi)存的大小待牵。
- 檢測(cè)圖片尺寸是否超過(guò)所有手機(jī)屏幕大小其屏,違規(guī)則報(bào)警。
這一個(gè)部分的實(shí)現(xiàn)原理缨该,主要是采用ARTHook來(lái)實(shí)現(xiàn)偎行,還不清楚的朋友請(qǐng)?jiān)僮屑?xì)看看大圖檢測(cè)的部分。
2贰拿、檢測(cè)原理
兩個(gè)工具的泄漏檢測(cè)原理都是在onDestroy時(shí)檢查弱引用蛤袒,不同之處在于LeakInspector直接使用WeakReference來(lái)檢測(cè)對(duì)象是否已經(jīng)被釋放,而LeakCanary則使用ReferenceQueue膨更,兩者效果是一樣的妙真。
并且針對(duì)Activity,我們通常都會(huì)使用Application的registerActivityLifecycleCallbacks來(lái)注冊(cè)Activity的生命周期荚守,以重寫(xiě)onActivityDestroyed方法實(shí)現(xiàn)珍德。但是在Android 4.0以下练般,系統(tǒng)并沒(méi)有提供這個(gè)方法,為了避免手動(dòng)在每一個(gè)Activity的onDestroy中去添加這份代碼菱阵,我們可以使用發(fā)射Instrumentation來(lái)截獲onDestory踢俄,以降低接入成本。代碼如下所示:
Class<?> clazz = Class.forName("android.app.ActivityThread");
Method method = clazz.getDeclaredMethod("currentActivityThread", null);
method.setAccessible(true);
sCurrentActivityThread = method.invoke(null, null);
Field field = sCurrentActivityThread.getClass().getDeclaredField("mInstumentation");
field.setAccessible(true);
field.set(sCurrentActivityThread, new MonitorInstumentation());
二晴及、泄漏現(xiàn)場(chǎng)處理方面不同
1都办、dump采集
兩者都能采集dump,但是LeakInspector提供了回調(diào)方法虑稼,我們可以增加更多的自定義信息琳钉,如運(yùn)行時(shí)Log、trace蛛倦、dumpsys meminfo等信息歌懒,以輔助分析定位問(wèn)題。
2溯壶、白名單定義
這里的白名單是為了處理一些系統(tǒng)引起的泄漏問(wèn)題及皂,以及一些因?yàn)闃I(yè)務(wù)邏輯要開(kāi)后門(mén)的情形而設(shè)置的。分析時(shí)如果碰到白名單上標(biāo)識(shí)的類且改,則不對(duì)這個(gè)泄漏做后續(xù)的處理验烧。二者的配置差異如下所示:
(1)LeakInspector的白名單以XML配置的形式存放在服務(wù)器上。
- 優(yōu)點(diǎn):跟產(chǎn)品甚至不同版本的應(yīng)用綁定又跛,我們可以很方便地修改相應(yīng)的配置碍拆。
- 缺點(diǎn):白名單里的類不區(qū)分系統(tǒng)版本一刀切。
而LeakCanary的白名單是直接寫(xiě)死在其源碼的AndroidExcludedRefs類里慨蓝。
- 優(yōu)點(diǎn):定義非常詳細(xì)感混,并區(qū)分系統(tǒng)版本。
- 缺點(diǎn):每次修改必定得重新編譯礼烈。
(2)LeakCanary的系統(tǒng)白名單里定義的類比LeakInspector中定義的多很多弧满,因?yàn)樗鼪](méi)有自動(dòng)修復(fù)系統(tǒng)泄漏功能。
3济丘、自動(dòng)修復(fù)系統(tǒng)泄漏
針對(duì)系統(tǒng)泄漏谱秽,LeakInspector通過(guò)反射自動(dòng)修復(fù)了目前碰到的一些系統(tǒng)泄漏,只要在onDestory里面調(diào)研一個(gè)修復(fù)系統(tǒng)泄漏的方法即可摹迷。而LeakCanary雖然能識(shí)別系統(tǒng)泄漏疟赊,但是它僅僅對(duì)該類問(wèn)題給出了分析,沒(méi)有提供實(shí)際可用的解決方案峡碉。
4近哟、回收資源
如果檢測(cè)到發(fā)生了內(nèi)存泄漏,LeakInspector會(huì)對(duì)整個(gè)Activity的View進(jìn)行遍歷鲫寄,把圖片資源等一些占內(nèi)存的數(shù)據(jù)釋放掉吉执,保證此次泄漏只會(huì)泄漏一個(gè)Activity的空殼疯淫,盡量減少對(duì)內(nèi)存的影響。代碼大致如下所示:
if (View instanceof ImageView) {
// ImageView ImageButton處理
recycleImageView(app, (ImageView) view);
} else if (view instanceof TextView) {
// 釋放TextView戳玫、Button周邊圖片資源
recycleTextView((TextView) view);
} else if (View instanceof ProgressBar) {
recycleProgressBar((ProgressBar) view);
} else {
if (view instancof android.widget.ListView) {
recycleListView((android.widget.ListView) view);
} else if (view instanceof android.support.v7.widget.RecyclerView) {
recycleRecyclerView((android.support.v7.widget.RecyclerView) view);
} else if (view instanceof FrameLayout) {
recycleFrameLayout((FrameLayout) view);
} else if (view instanceof LinearLayout) {
recycleLinearLayout((LinearLayout) view);
}
if (view instanceof ViewGroup) {
recycleViewGroup(app, (ViewGroup) view);
}
}
這里以recycleTextView為例熙掺,它回收資源的方式如下所示:
private static void recycleTextView(TextView tv) {
Drawable[] ds = tv.getCompoundDrawables();
for (Drawable d : ds) {
if (d != null) {
d.setCallback(null);
}
}
tv.setCompoundDrawables(null, null, null, null);
// 取消焦點(diǎn),讓Editor$Blink這個(gè)Runnable不再被post咕宿,解決內(nèi)存泄漏币绩。
tv.setCursorVisible(false);
}
三、后期處理不同
1府阀、分析與展示
采集dump之后缆镣,LeakInspector會(huì)上傳dump文件,并調(diào)用MAT命令行來(lái)進(jìn)行分析试浙,得到這次泄漏的GC鏈董瞻;而LeakCanary則用開(kāi)源組件HAHA來(lái)分析得到一個(gè)GC鏈。但是LeakCanary得到的GC鏈包含被hold主的類對(duì)象田巴,一般都不需要用MAT打開(kāi)Hporf即可解決問(wèn)題钠糊;而LeakInpsector得到的GC鏈李只有類名,還需要MAT打開(kāi)Hprof才能具體去定位問(wèn)題壹哺,不是很方便眠蚂。
2、后續(xù)跟進(jìn)閉環(huán)
LeakInspector在dump分析結(jié)束之后斗躏,會(huì)提交缺陷單,并且把缺陷單分配給對(duì)應(yīng)類的負(fù)責(zé)人昔脯;如果發(fā)現(xiàn)重復(fù)的問(wèn)題則更新舊單啄糙,同時(shí)具備重新打開(kāi)單等狀態(tài)轉(zhuǎn)換羅家。而LeakCanary僅會(huì)在通知欄提醒用戶云稚,需要用戶自己記錄該問(wèn)題并做后續(xù)處理隧饼。
四、配合自動(dòng)化測(cè)試方面不同
LeakInspector跟自動(dòng)化測(cè)試可以無(wú)縫結(jié)合静陈,當(dāng)自動(dòng)化腳本執(zhí)行中發(fā)現(xiàn)內(nèi)存泄漏燕雁,可以由它采集dump并發(fā)送到服務(wù)進(jìn)行分析,最后提單鲸拥,整個(gè)流程是不需要人力介入的拐格。而LeakCanary則把分析結(jié)果通過(guò)通知欄告知用戶,需要人工介入才能進(jìn)入下一個(gè)流程刑赶。
4捏浊、JHat
JHat是Oracle推出的一款Hprof分析軟件,它和MAT并稱為Java內(nèi)存靜態(tài)分析利器撞叨。不同于MAT的單人界面式分析,jHat使用多人界面式分析。它被內(nèi)置在JDK中蔬充,在命令行中輸入jhat命令可查看沒(méi)有有相應(yīng)的命令仗嗦。
quchao@quchaodeMacBook-Pro ~ % jhat
ERROR: No arguments supplied
Usage: jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>
-J<flag> Pass <flag> directly to the runtime system. For
example, -J-mx512m to use a maximum heap size of 512MB
-stack false: Turn off tracking object allocation call stack.
-refs false: Turn off tracking of references to objects
-port <port>: Set the port for the HTTP server. Defaults to 7000
-exclude <file>: Specify a file that lists data members that should
be excluded from the reachableFrom query.
-baseline <file>: Specify a baseline object dump. Objects in
both heap dumps with the same ID and same class will
be marked as not being "new".
-debug <int>: Set debug level.
0: No debug output
1: Debug hprof file parsing
2: Debug hprof file parsing, no server
-version Report version number
-h|-help Print this help and exit
<file> The file to read
For a dump file that contains multiple heap dumps,
you may specify which dump in the file
by appending "#<number>" to the file name, i.e. "foo.hprof#3".
如上,則表明存在jhat命令涛菠。它的使用很簡(jiǎn)單,直在命令行輸入jhat xxx.hprof即可:
quchao@quchaodeMacBook-Pro ~ % jhat Documents/heapdump/new-33.hprof
Snapshot read, resolving...
Resolving 408200 objects...
Chasing references, expect 81 dots.................................................................................
Eliminating duplicate references.................................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
jHat的執(zhí)行過(guò)程是解析Hprof文件,然后啟動(dòng)httpsrv服務(wù)苫亦,默認(rèn)是在7000端口監(jiān)聽(tīng)Web客戶端鏈接,維護(hù)Hprof解析后數(shù)據(jù)奕锌,以持續(xù)供給Web客戶端的查詢操作著觉。
啟動(dòng)服務(wù)器后,我們打開(kāi)入口地址127.0.0.1:7000即可查看All Classes界面:
jHat還有兩個(gè)比較重要的功能:
1惊暴、統(tǒng)計(jì)表
打開(kāi)127.0.0.1:7000/histo/:
2饼丘、OQL查詢
OQL是一種模仿SQL語(yǔ)句的查詢語(yǔ)句,通常用來(lái)查詢某個(gè)類的實(shí)例數(shù)量辽话,打開(kāi)127.0.0.1:7000/oql/并輸入java.lang.String查詢String實(shí)例的數(shù)量肄鸽,如下所示:
JHat比MAT更加靈活,且符合大型團(tuán)隊(duì)安裝簡(jiǎn)單油啤、團(tuán)隊(duì)協(xié)作的需求你典徘,并不適合中小型高效溝通型團(tuán)隊(duì)使用。
5益咬、GC Log
GC Log分為Dalvik和ART的GC日志逮诲,關(guān)于Dalvik的GC日志,在前篇Android性能優(yōu)化之內(nèi)存優(yōu)化已經(jīng)詳細(xì)講解過(guò)了幽告,接下來(lái)我們說(shuō)說(shuō)ART的GC日志梅鹦。
ART的日志與Dalvik的日志差距非常大,除了格式不同之外冗锁,打印的時(shí)間也不同齐唆,非要在慢GC時(shí)才打印除了。下面我們看看這條ART GC Log:
Explicit | (full) | concurrent mark sweep GC | freed 104710 (7MB) AllocSpace objects, | 21(416KB) LOS objects冻河, | 33% free,25MB/38MB | paused 1.230ms total 67.216ms | |
---|---|---|---|---|---|---|---|
GC產(chǎn)生的原因 | GC類型 | 采集方法 | 釋放的數(shù)量和占用的空間 | 釋放的大對(duì)象數(shù)量和所占用的空間 | 堆中空閑空間的百分比和(對(duì)象的個(gè)數(shù))/(堆的總空間) | 暫停耗時(shí) |
GC產(chǎn)生的原因如下:
- Concurrent箍邮、Alloc、Explicit跟Dalvik的基本一樣叨叙,這里就不重復(fù)介紹了锭弊。
- NativeAlloc:Native內(nèi)存分配時(shí),比如為Bitmaps或者RenderScript分配對(duì)象摔敛, 這會(huì)導(dǎo)致Native內(nèi)存壓力廷蓉,從而觸發(fā)GC。
- Background:后臺(tái)GC,觸發(fā)是為了給后面的內(nèi)存申請(qǐng)預(yù)留更多空間桃犬。
- CollectorTransition:由堆轉(zhuǎn)換引起的回收刹悴,這是運(yùn)行時(shí)切換GC而引起的。收集器轉(zhuǎn)換包括將所有對(duì)象從空閑列表空間復(fù)制到碰撞指針空間(反之亦然)攒暇。當(dāng)前土匀,收集器轉(zhuǎn)換僅在以下情況下出現(xiàn):在內(nèi)存較小的設(shè)備上,App將進(jìn)程狀態(tài)從可察覺(jué)的暫停狀態(tài)變更為可察覺(jué)的非暫停狀態(tài)(反之亦然)形用。
- HomogeneousSpaceCompact:齊性空間壓縮是指空閑列表到壓縮的空閑列表空間就轧,通常發(fā)生在當(dāng)App已經(jīng)移動(dòng)到可察覺(jué)的暫停進(jìn)程狀態(tài)。這樣做的主要原因是減少了內(nèi)存使用并對(duì)堆內(nèi)存進(jìn)行碎片整理田度。
- DisableMovingGc:不是真正的觸發(fā)GC原因妒御,發(fā)生并發(fā)堆壓縮時(shí),由于使用了
- GetPrimitiveArrayCritical镇饺,收集會(huì)被阻塞乎莉。一般情況下,強(qiáng)烈建議不要使用
- GetPrimitiveArrayCritical奸笤,因?yàn)樗谝苿?dòng)收集器方面具有限制惋啃。
- HeapTrim:不是觸發(fā)GC原因,但是請(qǐng)注意监右,收集會(huì)一直被阻塞边灭,直到堆內(nèi)存整理完畢。
GC類型如下:
- Full:與Dalvik的FULL GC差不多健盒。
- Partial:跟Dalvik的局部GC差不多绒瘦,策略時(shí)不包含Zygote Heap。
- Sticky:另外一種局部中的局部GC扣癣,選擇局部的策略是上次垃圾回收后新分配的對(duì)象椭坚。
GC采集的方法如下:
- mark sweep:先記錄全部對(duì)象,然后從GC ROOT開(kāi)始找出間接和直接的對(duì)象并標(biāo)注搏色。利用之前記錄的全部對(duì)象和標(biāo)注的對(duì)象對(duì)比,其余的對(duì)象就應(yīng)該需要垃圾回收了券册。
- concurrent mark sweep:使用mark sweep采集器的并發(fā)GC频轿。
- mark compact:在標(biāo)記存活對(duì)象的時(shí)候,所有的存活對(duì)象壓縮到內(nèi)存的一端烁焙,而另一端可以更加高效地被回收航邢。
- semispace:在做垃圾掃描的時(shí)候,把所有引用的對(duì)象從一個(gè)空間移到另外一個(gè)空間骄蝇,然后直接GC剩余在舊空間中的對(duì)象即可膳殷。
通過(guò)GC日志,我們可以知道GC的量和它對(duì)卡頓的影響九火,也可以初步定位一些如主動(dòng)調(diào)用GC赚窃、可分配的內(nèi)存不足册招、過(guò)多使用Weak Reference等問(wèn)題。
6勒极、自帶防泄漏功能的線程池組件
我們?cè)谧鲎泳€程操作的時(shí)候是掰,喜歡使用匿名內(nèi)部類Runnable來(lái)操作,但是,如果某個(gè)Activity放在線程池中的任務(wù)不能及時(shí)執(zhí)行完畢,在Activity銷毀時(shí)很容易導(dǎo)致內(nèi)存泄漏辱匿。因?yàn)檫@個(gè)匿名內(nèi)部類Runnable類持有一個(gè)指向Outer類的引用键痛,這樣一來(lái)如果Activity里面的Runnable不能及時(shí)執(zhí)行,就會(huì)使它外圍的Activity無(wú)法釋放匾七,產(chǎn)生內(nèi)存泄漏絮短。從上面的分析可知,只要在Activity退出時(shí)沒(méi)有這個(gè)引用即可昨忆,那我們就通過(guò)反射丁频,在Runnable進(jìn)入線程池前先干掉它,代碼如下所示:
Field f = job.getClass().getDeclaredField("this$0");
f.setAccessible(true);
f.set(job, null);
這個(gè)任務(wù)就是我們的Runnable對(duì)象扔嵌,而”this$0“就是上面所指的外部類的引用了限府。這里注意使用WeakReference裝起來(lái),要執(zhí)行了先get一下痢缎,如果是null則說(shuō)明Activity已經(jīng)回收胁勺,任務(wù)就放棄執(zhí)行。