Android性能優(yōu)化系列之內(nèi)存優(yōu)化疯汁,這一篇就夠了

內(nèi)存的分配策略概述

程序運(yùn)行時(shí)的內(nèi)存分配有三種策略,分別是靜態(tài)的,棧式的,和堆式的牲尺,對(duì)應(yīng)的,三種存儲(chǔ)策略使用的內(nèi)存空間主要分別是靜態(tài)存儲(chǔ)區(qū)(也稱方法區(qū))幌蚊、堆區(qū)和棧區(qū)谤碳。

靜態(tài)存儲(chǔ)區(qū)(方法區(qū)):內(nèi)存在程序編譯的時(shí)候就已經(jīng)分配好,這塊內(nèi)存在程序整個(gè)運(yùn)行期間都存在溢豆。它主要存放靜態(tài)數(shù)據(jù)估蹄、全局static數(shù)據(jù)和常量。

棧區(qū):在執(zhí)行函數(shù)時(shí)沫换,函數(shù)內(nèi)局部變量的存儲(chǔ)單元都可以在棧上創(chuàng)建臭蚁,函數(shù)執(zhí)行結(jié)束時(shí)這些存儲(chǔ)單元自動(dòng)被釋放。棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中讯赏,效率很高垮兑,但是分配的內(nèi)存容量有限。

堆區(qū):亦稱動(dòng)態(tài)內(nèi)存分配漱挎。程序在運(yùn)行的時(shí)候用malloc或new申請(qǐng)任意大小的內(nèi)存系枪,程序員自己負(fù)責(zé)在適當(dāng)?shù)臅r(shí)候用free或delete釋放內(nèi)存(Java則依賴?yán)厥掌鳎?dòng)態(tài)內(nèi)存的生存期可以由我們決定磕谅,如果我們不釋放內(nèi)存私爷,程序?qū)⒃谧詈蟛裴尫诺魟?dòng)態(tài)內(nèi)存。 但是膊夹,良好的編程習(xí)慣是:如果某動(dòng)態(tài)內(nèi)存不再使用衬浑,需要將其釋放掉。

堆和棧的區(qū)別:

在函數(shù)中(說明是局部變量)定義的一些基本類型的變量和對(duì)象的引用變量都是在函數(shù)的棧內(nèi)存中分配放刨。當(dāng)在一段代碼塊中定義一個(gè)變量時(shí)工秩,java就在棧中為這個(gè)變量分配內(nèi)存空間,當(dāng)超過變量的作用域后,java會(huì)自動(dòng)釋放掉為該變量分配的內(nèi)存空間助币,該內(nèi)存空間可以立刻被另作他用浪听。

堆內(nèi)存用于存放所有由new創(chuàng)建的對(duì)象(內(nèi)容包括該對(duì)象其中的所有成員變量)和數(shù)組。在堆中分配的內(nèi)存眉菱,由java虛擬機(jī)自動(dòng)垃圾回收器來管理迹栓。在堆中產(chǎn)生了一個(gè)數(shù)組或者對(duì)象后,還可以在棧中定義一個(gè)特殊的變量俭缓,這個(gè)變量的取值等于數(shù)組或者對(duì)象在堆內(nèi)存中的首地址迈螟,在棧中的這個(gè)特殊的變量就變成了數(shù)組或者對(duì)象的引用變量,以后就可以在程序中使用棧內(nèi)存中的引用變量來訪問堆中的數(shù)組或者對(duì)象尔崔,引用變量相當(dāng)于為數(shù)組或者對(duì)象起的一個(gè)別名答毫,或者代號(hào)

堆是不連續(xù)的內(nèi)存區(qū)域(因?yàn)橄到y(tǒng)是用鏈表來存儲(chǔ)空閑內(nèi)存地址,自然不是連續(xù)的)季春,堆大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存(32bit系統(tǒng)理論上是4G)洗搂,所以堆的空間比較靈活,比較大载弄。棧是一塊連續(xù)的內(nèi)存區(qū)域耘拇,大小是操作系統(tǒng)預(yù)定好的,windows下棧大小是2M(也有是1M宇攻,在編譯時(shí)確定惫叛,VC中可設(shè)置)。

對(duì)于堆逞刷,頻繁的new/delete會(huì)造成大量?jī)?nèi)存碎片嘉涌,使程序效率降低。對(duì)于棧夸浅,它是先進(jìn)后出的隊(duì)列仑最,進(jìn)出一一對(duì)應(yīng),不產(chǎn)生碎片帆喇,運(yùn)行效率穩(wěn)定高警医。


image.png

所以我們可以得出結(jié)論:
1.局部變量的基本數(shù)據(jù)類型和引用存儲(chǔ)于棧中,引用的對(duì)象實(shí)體存儲(chǔ)于堆中坯钦。因?yàn)樗鼈儗儆诜椒ㄖ械淖兞吭せ剩芷陔S方法而結(jié)束。

2.成員變量全部存儲(chǔ)與堆中(包括基本數(shù)據(jù)類型婉刀,引用和引用的對(duì)象實(shí)體)吟温,因?yàn)樗鼈儗儆陬悾悓?duì)象終究是要被new出來使用的路星。

3.我們所說的內(nèi)存泄露溯街,只針對(duì)堆內(nèi)存诱桂,他們存放的就是引用指向的對(duì)象實(shí)體洋丐。

內(nèi)存泄露產(chǎn)生的原因

在Java中呈昔,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由垃圾收集器(Garbage Collection友绝,GC)完成的堤尾,程序員不需要通過調(diào)用函數(shù)來釋放內(nèi)存,但它只能回收無(wú)用并且不再被其它對(duì)象引用的那些對(duì)象所占用的空間迁客。

Java的內(nèi)存垃圾回收機(jī)制是從程序的主要運(yùn)行對(duì)象(如靜態(tài)對(duì)象/寄存器/棧上指向的堆內(nèi)存對(duì)象等)開始檢查引用鏈郭宝,當(dāng)遍歷一遍后得到上述這些無(wú)法回收的對(duì)象和他們所引用的對(duì)象鏈,組成無(wú)法回收的對(duì)象集合掷漱,而其他孤立對(duì)象(集)就作為垃圾回收粘室。GC為了能夠正確釋放對(duì)象,必須監(jiān)控每一個(gè)對(duì)象的運(yùn)行狀態(tài)卜范,包括對(duì)象的申請(qǐng)衔统、引用、被引用海雪、賦值等锦爵,GC都需要進(jìn)行監(jiān)控。監(jiān)視對(duì)象狀態(tài)是為了更加準(zhǔn)確地奥裸、及時(shí)地釋放對(duì)象险掀,而釋放對(duì)象的根本原則就是該對(duì)象不再被引用。

在Java中湾宙,這些無(wú)用的對(duì)象都由GC負(fù)責(zé)回收樟氢,因此程序員不需要考慮這部分的內(nèi)存泄露。雖然侠鳄,我們有幾個(gè)函數(shù)可以訪問GC嗡害,例如運(yùn)行GC的函數(shù)System.gc(),但是根據(jù)Java語(yǔ)言規(guī)范定義畦攘,該函數(shù)不保證JVM的垃圾收集器一定會(huì)執(zhí)行霸妹。因?yàn)椴煌腏VM實(shí)現(xiàn)者可能使用不同的算法管理GC。通常GC的線程的優(yōu)先級(jí)別較低知押。JVM調(diào)用GC的策略也有很多種叹螟,有的是內(nèi)存使用到達(dá)一定程度時(shí),GC才開始工作台盯,也有定時(shí)執(zhí)行的罢绽,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC静盅。但通常來說良价,我們不需要關(guān)心這些寝殴。

但是我們?nèi)匀豢梢匀ケO(jiān)聽系統(tǒng)的GC過程,以此來分析我們應(yīng)用程序當(dāng)前的內(nèi)存狀態(tài)明垢。那么怎樣才能去監(jiān)聽系統(tǒng)的GC過程呢蚣常?其實(shí)非常簡(jiǎn)單,系統(tǒng)每進(jìn)行一次GC操作時(shí)痊银,都會(huì)在LogCat中打印一條日志抵蚊,我們只要去分析這條日志就可以了,日志的基本格式如下所示:

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time>  

首先第一部分GC_Reason溯革,這個(gè)是觸發(fā)這次GC操作的原因贞绳,一般情況下一共有以下幾種觸發(fā)GC操作的原因:
GC_CONCURRENT: 當(dāng)我們應(yīng)用程序的堆內(nèi)存快要滿的時(shí)候,系統(tǒng)會(huì)自動(dòng)觸發(fā)GC操作來釋放內(nèi)存致稀。

GC_FOR_MALLOC: 當(dāng)我們的應(yīng)用程序需要分配更多內(nèi)存冈闭,可是現(xiàn)有內(nèi)存已經(jīng)不足的時(shí)候,系統(tǒng)會(huì)進(jìn)行GC操作來釋放內(nèi)存抖单。
GC_HPROF_DUMP_HEAP: 當(dāng)生成HPROF文件的時(shí)候萎攒,系統(tǒng)會(huì)進(jìn)行GC操作,關(guān)于HPROF文件我們下面會(huì)講到臭猜。
GC_EXPLICIT: 這種情況就是我們剛才提到過的躺酒,主動(dòng)通知系統(tǒng)去進(jìn)行GC操作,比如調(diào)用System.gc()方法來通知系統(tǒng)蔑歌「Γ或者在DDMS中,通過工具按鈕也是可以顯式地告訴系統(tǒng)進(jìn)行GC操作的次屠。

接下來第二部分Amount_freed园匹,表示系統(tǒng)通過這次GC操作釋放了多少內(nèi)存。

然后Heap_stats中會(huì)顯示當(dāng)前內(nèi)存的空閑比例以及使用情況(活動(dòng)對(duì)象所占內(nèi)存 / 當(dāng)前程序總內(nèi)存)劫灶。

最后Pause_time表示這次GC操作導(dǎo)致應(yīng)用程序暫停的時(shí)間裸违。關(guān)于這個(gè)暫停的時(shí)間,Android在2.3的版本當(dāng)中進(jìn)行過一次優(yōu)化本昏,在2.3之前GC操作是不能并發(fā)進(jìn)行的供汛,也就是系統(tǒng)正在進(jìn)行GC,那么應(yīng)用程序就只能阻塞住等待GC結(jié)束涌穆。雖說這個(gè)阻塞的過程并不會(huì)很長(zhǎng)怔昨,也就是幾百毫秒,但是用戶在使用我們的程序時(shí)還是有可能會(huì)感覺到略微的卡頓宿稀。而自2.3之后趁舀,GC操作改成了并發(fā)的方式進(jìn)行,就是說GC的過程中不會(huì)影響到應(yīng)用程序的正常運(yùn)行祝沸,但是在GC操作的開始和結(jié)束的時(shí)候會(huì)短暫阻塞一段時(shí)間矮烹,不過優(yōu)化到這種程度越庇,用戶已經(jīng)是完全無(wú)法察覺到了。

我們來看看Java中需要被回收的垃圾:

{
Person p1 = new Person();
……
}

引用句柄p1的作用域是從定義到“}”處奉狈,執(zhí)行完這對(duì)大括號(hào)中的所有代碼后卤唉,產(chǎn)生的Person對(duì)象就會(huì)變成垃圾,因?yàn)橐眠@個(gè)對(duì)象的句柄p1已超過其作用域嘹吨,p1失效搬味,在棧中被銷毀境氢,因此堆上的Person對(duì)象不再被任何句柄引用了蟀拷。 因此person變?yōu)槔瑫?huì)被回收萍聊。

這里我們需要講述一個(gè)關(guān)鍵詞:引用问芬,通過A能調(diào)用并訪問到B,那就說明A持有B的引用寿桨,或A就是B的引用此衅,B的引用計(jì)數(shù)+1.

(1)比如 Person p1 = new Person();通過P1能操作Person對(duì)象,因此P1是Person的引用亭螟;
(2)比如類O中有一個(gè)成員變量是I類對(duì)象挡鞍,因此我們可以使用o.i的方式來訪問I類對(duì)象的成員,因此o持有一個(gè)i對(duì)象的引用预烙。

GC過程與對(duì)象的引用類型是嚴(yán)重相關(guān)的墨微,我們來看看Java對(duì)引用的分類Strong reference, SoftReference, WeakReference, PhatomReference


image.png

在Android應(yīng)用的開發(fā)中,為了防止內(nèi)存溢出扁掸,在處理一些占用內(nèi)存大而且聲明周期較長(zhǎng)的對(duì)象時(shí)候翘县,可以盡量應(yīng)用軟引用和弱引用技術(shù)。

軟/弱引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用谴分,如果軟引用所引用的對(duì)象被垃圾回收器回收锈麸,Java虛擬機(jī)就會(huì)把這個(gè)軟引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。利用這個(gè)隊(duì)列可以得知被回收的軟/弱引用的對(duì)象列表牺蹄,從而為緩沖器清除已失效的軟/弱引用忘伞。

假設(shè)我們的應(yīng)用會(huì)用到大量的默認(rèn)圖片,比如應(yīng)用中有默認(rèn)的頭像沙兰,默認(rèn)游戲圖標(biāo)等等氓奈,這些圖片很多地方會(huì)用到。如果每次都去讀取圖片僧凰,由于讀取文件需要硬件操作探颈,速度較慢,會(huì)導(dǎo)致性能較低训措。所以我們考慮將圖片緩存起來伪节,需要的時(shí)候直接從內(nèi)存中讀取光羞。但是,由于圖片占用內(nèi)存空間比較大怀大,緩存很多圖片需要很多的內(nèi)存纱兑,就可能比較容易發(fā)生OutOfMemory異常。這時(shí)化借,我們可以考慮使用軟/弱引用技術(shù)來避免這個(gè)問題發(fā)生潜慎。以下就是高速緩沖器的雛形:

首先定義一個(gè)HashMap,保存軟引用對(duì)象蓖康。

1.private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

再來定義一個(gè)方法铐炫,保存Bitmap的軟引用到HashMap

public class CacheSoftRef {

    //首先定義一個(gè)HashMap,保存引用對(duì)象
    private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

    //再來定義一個(gè)方法,保存Bitmap的軟引用到HashMap
    public void addBitmapToCache(String path) {
        //強(qiáng)引用的Bitmap對(duì)象
        Bitmap bitmap = BitmapFactory.decodeFile(path);
        //軟引用的Bitmap對(duì)象
        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
        //添加該對(duì)象到Map中使其緩存
        imageCache.put(path, softBitmap);
    }

    //獲取的時(shí)候蒜焊,可以通過SoftReference的get()方法得到Bitmap對(duì)象
    public Bitmap getBitmapByPath(String path) {
        //從緩存中取軟引用的Bitmap對(duì)象
        SoftReference<Bitmap> softBitmap = imageCache.get(path);
        //判斷是否存在軟引用
        if (softBitmap == null) {
            return null;
        }
        //通過軟引用取出Bitmap對(duì)象倒信,如果由于內(nèi)存不足Bitmap被回收,將取得空泳梆,如果未被回收鳖悠,
        //則可重復(fù)使用,提高速度优妙。
        Bitmap bitmap = softBitmap.get();
        return bitmap;
    }
}

使用軟引用以后乘综,在OutOfMemory異常發(fā)生之前,這些緩存的圖片資源的內(nèi)存空間可以被釋放掉的套硼,從而避免內(nèi)存達(dá)到上限卡辰,避免Crash發(fā)生。

如果只是想避免OutOfMemory異常的發(fā)生熟菲,則可以使用軟引用看政。如果對(duì)于應(yīng)用的性能更在意,想盡快回收一些占用內(nèi)存比較大的對(duì)象抄罕,則可以使用弱引用允蚣。

另外可以根據(jù)對(duì)象是否經(jīng)常使用來判斷選擇軟引用還是弱引用。如果該對(duì)象可能會(huì)經(jīng)常使用的呆贿,就盡量用軟引用嚷兔。如果該對(duì)象不被使用的可能性更大些,就可以用弱引用做入。

所以我們得出內(nèi)存泄漏的原因:堆內(nèi)存中的長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的強(qiáng)/軟引用冒晰,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因?yàn)殚L(zhǎng)生命周期對(duì)象持有它的引用而導(dǎo)致不能被回收竟块,這就是Java中內(nèi)存泄露的根本原因壶运。

內(nèi)存泄漏的檢測(cè)

說了那么多關(guān)于內(nèi)存分配的知識(shí),接下來我們就看看Android給我們提供了哪些工具來解決內(nèi)存泄漏的問題

Allocation Tracker(Device Monitor)

Allocation Tracker位于Android Device Monitor中


image.png

Allocation Tracker面板


image.png

各名稱的含義如下:
image.png
Allocation Tracker操作

1.首先進(jìn)入你要追蹤的界面
2.點(diǎn)擊Start Tracking按鈕浪秘,開始跟蹤內(nèi)存分配軌跡
3.操作你的界面蒋情,盡量時(shí)間短點(diǎn)
4.點(diǎn)擊Get Allocations按鈕埠况,抓去內(nèi)存分配軌跡信息,顯示在右邊的面板中棵癣,默認(rèn)以內(nèi)存大小排序辕翰,你可以以分配順序排序或者仍以列排序。
5.logcat中會(huì)顯示出這次的軌跡共抓到內(nèi)存分配軌跡記錄數(shù)狈谊,可以簡(jiǎn)單的理解分配了多少次內(nèi)存喜命,這個(gè)數(shù)值和Alloc order的最大值是相等的
6.如果你不想看那么多亂七八糟的,你可以使用Filter來過濾河劝,輸入包名就可以了壁榕。

跟蹤內(nèi)存軌跡

如果這個(gè)時(shí)候我們想單獨(dú)獲取某次操作的內(nèi)存軌跡,首先一定要記得Stop Tracking再Start Tracking一下丧裁,讓追蹤點(diǎn)初始化一下护桦,然后就進(jìn)行我們需要觀察內(nèi)存變化的操作含衔,然后點(diǎn)擊Get Allocations煎娇,這個(gè)時(shí)候我們從首頁(yè)進(jìn)入一個(gè)詳情頁(yè),看一下我們的內(nèi)存分配軌跡:


image.png

追蹤到的內(nèi)存分配20293次,看著是不是有點(diǎn)無(wú)從下手贪染,沒關(guān)系缓呛,用Filter過濾下:


image.png

過濾后,就剩下了跟我們App源碼有關(guān)系的分配軌跡杭隙,我們隨便選擇一欄哟绊,可以看到其trace信息:
image.png

上圖中,我們可以看出來痰憎,在第635次內(nèi)存分配中票髓,分配的是IntroduceFragment對(duì)象,占用內(nèi)存224字節(jié)铣耘,處理線程Id為3245洽沟,在java.lang.Class的newInstance方法中分配的。從trace信息可以看出來該方法一步一步被調(diào)用的信息蜗细。

DDMS的Heap Viewer

Heap Viewer能做什么裆操?
?實(shí)時(shí)查看App分配的內(nèi)存大小和空閑內(nèi)存大小
?發(fā)現(xiàn)Memory Leaks

在Devices 中,點(diǎn)擊要監(jiān)控的程序炉媒。
點(diǎn)擊Devices視圖界面中最上方一排圖標(biāo)中的“Update Heap”
點(diǎn)擊Heap視圖
點(diǎn)擊Heap視圖中的“Cause GC”按鈕
到此為止需檢測(cè)的進(jìn)程就可以被監(jiān)視踪区。

image.png

按上圖的標(biāo)記順序按下,我們就能看到內(nèi)存的具體數(shù)據(jù)吊骤,右邊面板中數(shù)值會(huì)在每次GC時(shí)發(fā)生改變缎岗,包括App自動(dòng)觸發(fā)或者你來手動(dòng)觸發(fā)。
ok白粉,現(xiàn)在來解釋下面板中的名詞

總覽


image.png
image.png

詳情


image.png
image.png

下面是每一個(gè)對(duì)象都有的列名含義:

image.png

當(dāng)我們點(diǎn)擊某一行時(shí)传泊,可以看到如下的柱狀圖:

image.png

橫坐標(biāo)是對(duì)象的內(nèi)存大小茅郎,這些值隨著不同對(duì)象是不同的,縱坐標(biāo)是在某個(gè)內(nèi)存大小上的對(duì)象的數(shù)量

Heap Viewer的使用

我們說Heap Viewer適合發(fā)現(xiàn)內(nèi)存泄漏的問題或渤,那么如何檢測(cè)呢系冗?
那么如何檢測(cè)呢?
進(jìn)入某應(yīng)用薪鹦,不斷的操作該應(yīng)用掌敬,同時(shí)注意觀察data object的Total Size值,正常情況下Total Size值都會(huì)穩(wěn)定在一個(gè)有限的范圍內(nèi)池磁,也就是說由于程序中的的代碼良好奔害,沒有造成對(duì)象不被垃圾回收的情況。

所以說雖然我們不斷的操作會(huì)不斷的生成很多對(duì)象地熄,而在虛擬機(jī)不斷的進(jìn)行GC的過程中华临,這些對(duì)象都被回收了,內(nèi)存占用量會(huì)會(huì)落到一個(gè)穩(wěn)定的水平端考;反之如果代碼中存在沒有釋放對(duì)象引用的情況雅潭,則data object的Total Size值在每次GC后不會(huì)有明顯的回落。隨著操作次數(shù)的增多Total Size的值會(huì)越來越大却特,直到到達(dá)一個(gè)上限后導(dǎo)致進(jìn)程被殺掉扶供。

MAT工具

那么通過上面DDMS工具,現(xiàn)在我們已經(jīng)可以比較輕松地發(fā)現(xiàn)應(yīng)用程序中是否存在內(nèi)存泄露的現(xiàn)象了裂明。
我們應(yīng)該怎么定位到具體是哪里出的內(nèi)存泄露呢椿浓?這就需要借助一個(gè)內(nèi)存分析工具了,叫做Eclipse Memory Analyzer(MAT)闽晦。
為了使用該工具扳碍,我們需要hprof文件。但是該文件不能直接被MAT使用仙蛉,需要進(jìn)行一步轉(zhuǎn)化笋敞,可以使用hprof-conv命令來轉(zhuǎn)化,但是Android Studio可以直接轉(zhuǎn)化,轉(zhuǎn)化方法如下:

1.選擇一個(gè)hprof文件捅儒,點(diǎn)擊右鍵選擇Export to standard .hprof選項(xiàng)液样。


image.png

2.填寫更改后的文件名和路徑:

image.png

點(diǎn)擊OK按鈕后,MAT工具所需的文件就生成了巧还,下面我們用MAT來打開該工具:
1.打開MAT后選擇File->Open File選擇我們剛才生成的doctorq.hprof文件
2.選擇該文件后鞭莽,MAT會(huì)有幾秒種的時(shí)間解析該文件,有的hprof文件可能過大麸祷,會(huì)有更長(zhǎng)的時(shí)間解析澎怒,解析后,展現(xiàn)在我們的面前的界面如下:

image.png

上圖最中央的那個(gè)餅狀圖展示了最大的幾個(gè)對(duì)象所占內(nèi)存的比例,這張圖中提供的內(nèi)容并不多喷面,我們可以忽略它星瘾。在這個(gè)餅狀圖的下方就有幾個(gè)非常有用的工具了,我們來學(xué)習(xí)一下惧辈。

Histogram可以列出內(nèi)存中每個(gè)對(duì)象的名字琳状、數(shù)量以及大小。
Dominator Tree會(huì)將所有內(nèi)存中的對(duì)象按大小進(jìn)行排序盒齿,并且我們可以分析對(duì)象之間的引用結(jié)構(gòu)念逞。
現(xiàn)在點(diǎn)擊Dominator Tree,結(jié)果如下圖所示:

image.png

首先Retained Heap表示這個(gè)對(duì)象以及它所持有的其它引用(包括直接和間接)所占的總內(nèi)存边翁,因此從上圖中看翎承,前兩行的Retained Heap是最大的,我們分析內(nèi)存泄漏時(shí)符匾,內(nèi)存最大的對(duì)象也是最應(yīng)該去懷疑的叨咖。

在每一行的最左邊都有一個(gè)文件型的圖標(biāo),這些圖標(biāo)有的左下角帶有一個(gè)點(diǎn)啊胶,有的則沒有甸各。帶點(diǎn)的對(duì)象就表示是可以被GC Roots訪問到的,可以被GC Root訪問到的對(duì)象都是無(wú)法被回收的创淡。那么這就說明所有帶紅色的對(duì)象都是泄漏的對(duì)象嗎痴晦?當(dāng)然不是,因?yàn)橛行?duì)象系統(tǒng)需要一直使用琳彩,本來就不應(yīng)該被回收。我們可以注意到部凑,上圖當(dāng)中所有帶點(diǎn)的對(duì)象最右邊都有寫一個(gè)System Class露乏,說明這是一個(gè)由系統(tǒng)管理的對(duì)象,并不是由我們自己創(chuàng)建并導(dǎo)致內(nèi)存泄漏的對(duì)象涂邀。

上圖當(dāng)中瘟仿,除了帶有System Class的行之外,最大的就是第二行的Bitmap對(duì)象了比勉,雖然Bitmap對(duì)象現(xiàn)在不能被GC Roots訪問到劳较,但不代表著Bitmap所持有的其它引用也不會(huì)被GC Roots訪問到。現(xiàn)在我們可以對(duì)著第二行點(diǎn)擊右鍵 -> Path to GC Roots -> exclude weak references浩聋,為什么選擇exclude weak references呢观蜗?因?yàn)槿跻檬遣粫?huì)阻止對(duì)象被垃圾回收器回收的,所以我們這里直接把它排除掉
可以看到衣洁,Bitmap對(duì)象經(jīng)過層層引用之后墓捻,到了MainActivityLeakClass這個(gè)對(duì)象,然后在圖標(biāo)的左下角有個(gè)點(diǎn)坊夫,就說明在這里可以被GCRoots訪問到了砖第,并且這是由我們自己創(chuàng)建的Thread撤卢,并不是SystemClass了,那么由于MainActivityLeakClass這個(gè)對(duì)象梧兼,然后在圖標(biāo)的左下角有個(gè)點(diǎn)放吩,就說明在這里可以被GCRoots訪問到了,并且這是由我們自己創(chuàng)建的Thread羽杰,并不是SystemClass了屎慢,那么由于MainActivityLeakClass能被GC Roots訪問到導(dǎo)致不能被回收,導(dǎo)致它所持有的其它引用也無(wú)法被回收了忽洛,包括MainActivity腻惠,也包括MainActivity中所包含的圖片。

通過這種方式欲虚,我們就成功地將內(nèi)存泄漏的原因找出來了集灌。

Histogram的用法

用的最多的功能是 Histogram,點(diǎn)擊 Actions下的 Histogram項(xiàng)將得到 Histogram結(jié)果:


image.png

它按類名將所有的實(shí)例對(duì)象列出來,可以點(diǎn)擊表頭進(jìn)行排序,在表的第一行可以輸入正則表達(dá)式來匹配結(jié)果 :


image.png

在某一項(xiàng)上右鍵打開菜單選擇 list objects ->with incoming refs 將列出該類的實(shí)例:


image.png

它展示了對(duì)象間的引用關(guān)系复哆,比如展開后的第一個(gè)子項(xiàng)表示這個(gè) HomePage(0x420ca5b0)被HomePageContainer(0x420c9e40)中的 mHomePage屬性所引用.
快速找出某個(gè)實(shí)例沒被釋放的原因欣喧,可以右健 Path to GC Roots–>exclue all phantom/weak/soft etc. reference :


image.png

得到的結(jié)果是:


image.png

從表中可以看出 PreferenceManager -> … ->HomePage這條線路就引用著這個(gè) HomePage實(shí)例。用這個(gè)方法可以快速找到某個(gè)對(duì)象的 GC Root,一個(gè)存在 GC Root的對(duì)象是不會(huì)被 GC回收掉的.

Histogram 對(duì)比

為查找內(nèi)存泄漏梯找,通常需要兩個(gè) Dump結(jié)果作對(duì)比唆阿,打開 Navigator History面板,將兩個(gè)表的 Histogram結(jié)果都添加到 Compare Basket中去 :

image.png

添加好后锈锤,打開 Compare Basket面板掂骏,得到結(jié)果:

image.png

點(diǎn)擊右上角的 ! 按鈕假栓,將得到比對(duì)結(jié)果:


image.png

注意,上面這個(gè)對(duì)比結(jié)果不利于查找差異,可以調(diào)整對(duì)比選項(xiàng):

image.png

再把對(duì)比的結(jié)果排序芒划,就可得到直觀的對(duì)比結(jié)果:

image.png

也可以對(duì)比兩個(gè)對(duì)象集合只锭,方法與此類似闭专,都是將兩個(gè) Dump結(jié)果中的對(duì)象集合添加到Compare Basket中去對(duì)比拨扶。找出差異后用 Histogram查詢的方法找出 GC Root,定位到具體的某個(gè)對(duì)象上呼巴。

LeakCanary

有別于MAT和AndroidStudio中Monitors的實(shí)時(shí)內(nèi)存占用圖泽腮,使用LeakCanary分析內(nèi)存泄露就簡(jiǎn)單多了LeakCanary是Square開源了一個(gè)內(nèi)存泄露自動(dòng)探測(cè)神器 。這是項(xiàng)目的github倉(cāng)庫(kù)地址:https://github.com/square/leakcanary 衣赶。使用非常簡(jiǎn)單诊赊,在build.gradle中引入包依賴

debugCompile 'com.squareup.leakcanary:leakcanary-
android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-
android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-
android-no-op:1.5'
在Application中的onCreate方法中增加初始化代碼:
if (LeakCanary.isInAnalyzerProcess(this)) {
    // This process is dedicated to LeakCanary for
    // heap analysis.
    // You should not init your app in this process.
    return;
}
LeakCanary.install(this);

集成后什么都不用做,按照正常測(cè)試屑埋,當(dāng)有內(nèi)存泄漏發(fā)生后豪筝,應(yīng)用會(huì)通過系統(tǒng)通知欄發(fā)出通知,點(diǎn)擊通知就可以進(jìn)入查看內(nèi)存泄漏的具體信息。在這里舉個(gè)實(shí)踐中的例子续崖。把LeakCanary集成到項(xiàng)目中后敲街,等App啟動(dòng)后一會(huì),系統(tǒng)通知到了严望,點(diǎn)擊通知多艇,跳轉(zhuǎn)到泄漏的詳情頁(yè)面進(jìn)行查看:
image.png

很明顯,WebSiteQueryActivity泄露了像吻。首先峻黍,static 的MaskHeadView.fLayout變量引用了FrameLayout.mContext對(duì)象,這個(gè)對(duì)象的引用就是指向了WebSiteQueryActivity的實(shí)例拨匆,導(dǎo)致了它的泄漏姆涩,在第二節(jié)中我們說過static對(duì)象是內(nèi)部的static對(duì)象是比較容易造成內(nèi)存泄漏的,檢查代碼發(fā)現(xiàn)惭每,MaskHeadView直接在WebSiteQueryActivity的xml文件中使用了骨饿,因此持有WebSiteQueryActivity的實(shí)例,因?yàn)閒Layout對(duì)象是靜態(tài)的台腥,因此它的生命周期與Application同樣長(zhǎng)宏赘,因此WebSiteQueryActivity退出后,它的實(shí)例引用依然被fLayout持有黎侈,導(dǎo)致它無(wú)法被回收從而內(nèi)存泄露了察署。仔細(xì)檢查代碼,發(fā)現(xiàn)fLayout并沒有被外部使用到峻汉,應(yīng)該是之前的開發(fā)者手抖加了個(gè)static字段上去或者是現(xiàn)在不用了贴汪,但是沒有去掉,在這里我直接去掉了這個(gè)修飾符俱济,在此build代碼嘶是,這個(gè)內(nèi)存泄漏的現(xiàn)象消失了。

/去掉static修飾符蛛碌,避免static對(duì)象引起的內(nèi)存泄漏
private static FrameLayout fLayout;


public MaskHeadView(Context context, AttributeSet attrs) {
   super(context, attrs);
   this.context=context;
   initView(context);
}

private void initView(Context context2) {
   view = LayoutInflater.from(context).inflate(R.layout.
   mask_head_view, this);
   fLayout=(FrameLayout) view.findViewById(R.id.
   mask_container);
}

這只是個(gè)極簡(jiǎn)單的例子,但方法是一樣的辖源。順便提一句蔚携,其實(shí)無(wú)論是MAT工具的內(nèi)存分析,還是AndroidStudio中自帶的分析工具亦或是LeakCanary克饶,原理都是一樣的酝蜒,都是dump java heap出來進(jìn)行分析,找到泄漏的問題矾湃,只是LeakCanary幫我們把分析的工作做了亡脑。但值得一提的是,LeakCanary并不是萬(wàn)能的,有些內(nèi)存泄漏霉咨,它也無(wú)法檢測(cè)出來蛙紫。

好了,關(guān)于內(nèi)存泄露的相關(guān)內(nèi)容就介紹到這途戒,后期將持續(xù)更新坑傅,可以關(guān)注一下查看后續(xù)。

本文轉(zhuǎn)自csdn一位博主[伯努力不努力]

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末喷斋,一起剝皮案震驚了整個(gè)濱河市唁毒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌星爪,老刑警劉巖浆西,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異顽腾,居然都是意外死亡近零,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門崔泵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秒赤,“玉大人,你說我怎么就攤上這事憎瘸∪肜海” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵幌甘,是天一觀的道長(zhǎng)潮售。 經(jīng)常有香客問我,道長(zhǎng)锅风,這世上最難降的妖魔是什么酥诽? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮皱埠,結(jié)果婚禮上肮帐,老公的妹妹穿的比我還像新娘。我一直安慰自己边器,他們只是感情好训枢,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著忘巧,像睡著了一般恒界。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上砚嘴,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天十酣,我揣著相機(jī)與錄音涩拙,去河邊找鬼。 笑死耸采,一個(gè)胖子當(dāng)著我的面吹牛兴泥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播洋幻,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼郁轻,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了文留?” 一聲冷哼從身側(cè)響起好唯,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎燥翅,沒想到半個(gè)月后骑篙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡森书,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年靶端,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凛膏。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杨名,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出猖毫,到底是詐尸還是另有隱情台谍,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布吁断,位于F島的核電站趁蕊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏仔役。R本人自食惡果不足惜掷伙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望又兵。 院中可真熱鬧任柜,春花似錦、人聲如沸沛厨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)俄烁。三九已至,卻和暖如春级野,著一層夾襖步出監(jiān)牢的瞬間页屠,已是汗流浹背粹胯。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辰企,地道東北人风纠。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像牢贸,于是被迫代替她去往敵國(guó)和親竹观。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

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

  • 通常情況下我們說的內(nèi)存是指手機(jī)的RAM潜索,它主要包括一下幾個(gè)部分1.寄存器 :速度最快的存儲(chǔ)場(chǎng)所臭增,因?yàn)榧拇嫫魑挥?..
    gogoingmonkey閱讀 1,326評(píng)論 1 10
  • 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏大家都不陌生了竹习,簡(jiǎn)單粗俗的講誊抛,...
    宇宙只有巴掌大閱讀 2,361評(píng)論 0 12
  • 本文出自 Eddy Wiki ,轉(zhuǎn)載請(qǐng)注明出處:http://eddy.wiki/interview-java.h...
    eddy_wiki閱讀 1,156評(píng)論 0 16
  • 昨天才信誓旦旦的想要堅(jiān)持練習(xí)瑜伽整陌,今天就遇到了不可抗力拗窃。好吧,我也不是故意的泌辫。在手術(shù)室外排隊(duì)等候的時(shí)候随夸,我都還沒有...
    杜一一閱讀 185評(píng)論 0 1
  • 韓愈 世有伯樂,然后有千里馬震放。千里馬常有宾毒,而伯樂不常有。故雖有名馬澜搅,祗辱于奴隸人之手伍俘,駢死于槽櫪之間,不以千里稱也...
    小瓶蓋媽媽閱讀 312評(píng)論 0 1