深入探索Android內(nèi)存優(yōu)化

前言

成為一名優(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)存分析

項(xiàng)目地址點(diǎn)擊此處

在配置的時(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 github地址

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è)

項(xiàng)目地址

首先我們來(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í)行。

原文鏈接:https://jsonchao.github.io/2019/12/29/%E6%B7%B1%E5%85%A5%E6%8E%A2%E7%B4%A2Android%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末独旷,一起剝皮案震驚了整個(gè)濱河市署穗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嵌洼,老刑警劉巖案疲,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異麻养,居然都是意外死亡褐啡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)鳖昌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)备畦,“玉大人,你說(shuō)我怎么就攤上這事许昨《危” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵糕档,是天一觀的道長(zhǎng)莉恼。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么俐银? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任尿背,我火速辦了婚禮,結(jié)果婚禮上悉患,老公的妹妹穿的比我還像新娘残家。我一直安慰自己,他們只是感情好售躁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布坞淮。 她就那樣靜靜地躺著,像睡著了一般陪捷。 火紅的嫁衣襯著肌膚如雪回窘。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天市袖,我揣著相機(jī)與錄音啡直,去河邊找鬼。 笑死苍碟,一個(gè)胖子當(dāng)著我的面吹牛酒觅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播微峰,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼舷丹,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蜓肆?” 一聲冷哼從身側(cè)響起颜凯,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仗扬,沒(méi)想到半個(gè)月后症概,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡早芭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年彼城,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片退个。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡精肃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出帜乞,到底是詐尸還是另有隱情,我是刑警寧澤筐眷,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布黎烈,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏照棋。R本人自食惡果不足惜资溃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望烈炭。 院中可真熱鬧溶锭,春花似錦、人聲如沸符隙。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)霹疫。三九已至拱绑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間丽蝎,已是汗流浹背猎拨。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留屠阻,地道東北人红省。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像国觉,于是被迫代替她去往敵國(guó)和親吧恃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

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