
被文同時(shí)發(fā)布在CSDN上,歡迎查看。
APP內(nèi)存的使用,是評價(jià)一款應(yīng)用性能高低的一個(gè)重要指標(biāo)。雖然現(xiàn)在智能手機(jī)的內(nèi)存越來越大,但是一個(gè)好的應(yīng)用應(yīng)該將效率發(fā)揮到極致,精益求精。
本文是【Android 性能優(yōu)化】系列的第二篇文章,我們在第一篇【Android 性能優(yōu)化】—— UI篇中主要介紹了Android界面的優(yōu)化的原理以及方法,這一篇中我們將著重介紹Android的內(nèi)存優(yōu)化。本文的篇幅很長,但是請不要嫌煩,因?yàn)槊靠匆还?jié),你就多了一份在面試官面前裝X的資本。
1. 內(nèi)存與內(nèi)存分配策略概述
1.1 什么是內(nèi)存
通常情況下我們說的內(nèi)存是指手機(jī)的RAM,它主要包括一下幾個(gè)部分:
- 寄存器(Registers
讀音:[?r?d??st?])
速度最快的存儲場所,因?yàn)榧拇嫫魑挥谔幚砥鲀?nèi)部,所以在程序中我們無法控制。 - 棧(Stack)
存放基本類型的對象和引用,但是對象本身不存放在棧中,而是存放在堆中。
變量其實(shí)是分為兩部分的:一部分叫變量名,另外一部分叫變量值,對于局部變量(基本類型的變量和對象的引用變量)而言,統(tǒng)一都存放在棧中,但是變量值中存儲的內(nèi)容就有在一定差異了:Java中存在8大基本類型,他們的變量值中存放的就是具體的數(shù)值,而其他的類型都叫做引用類型(對象也是引用類型,你只要記住除了基本類型,都是引用類型)他們的變量值中存放的是他們在堆中的引用(內(nèi)存地址)。
在函數(shù)執(zhí)行的時(shí)候,函數(shù)內(nèi)部的局部變量就會在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束的時(shí)候這些存儲單元會被自動釋放。棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中是一塊連續(xù)的內(nèi)存區(qū)域,效率很高,速度快,但是大小是操作系統(tǒng)預(yù)定好的所以分配的內(nèi)存容量有限。
堆(Heap)
在堆上分配內(nèi)存的過程稱作 內(nèi)存動態(tài)分配過程。在java中堆用于存放由new創(chuàng)建的對象和數(shù)組。堆中分配的內(nèi)存,由java虛擬機(jī)自動垃圾回收器(GC)來管理(可見我們要進(jìn)行的內(nèi)存優(yōu)化主要就是對堆內(nèi)存進(jìn)行優(yōu)化)。堆是不連續(xù)的內(nèi)存區(qū)域(因?yàn)橄到y(tǒng)是用鏈表來存儲空閑內(nèi)存地址,自然不是連續(xù)的),堆大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存(32bit系統(tǒng)理論上是4G)靜態(tài)存儲區(qū)/方法區(qū)(Static Field)
是指在固定的位置上存放應(yīng)用程序運(yùn)行時(shí)一直存在的數(shù)據(jù),java在內(nèi)存中專門劃分了一個(gè)靜態(tài)存儲區(qū)域來管理一些特殊的數(shù)據(jù)變量如靜態(tài)的數(shù)據(jù)變量。常量池(Constant Pool)
顧名思義專門存放常量的。注意String s = "java"中的“java”也是常量。JVM虛擬機(jī)為每個(gè)已經(jīng)被轉(zhuǎn)載的類型維護(hù)一個(gè)常量池。常量池就是該類型所有用到地常量的一個(gè)有序集合包括直接常量(基本類型,String)和對其他類型、字段和方法的符號引用。
總結(jié):
- 定義一個(gè)局部變量的時(shí)候,java虛擬機(jī)就會在棧中為其分配內(nèi)存空間,局部變量的基本數(shù)據(jù)類型和引用存儲于棧中,引用的對象實(shí)體存儲于堆中。因?yàn)樗鼈儗儆诜椒ㄖ械淖兞?,生命周期隨方法而結(jié)束。
- 成員變量全部存儲與堆中(包括基本數(shù)據(jù)類型,引用和引用的對象實(shí)體),因?yàn)樗鼈儗儆陬?,類對象終究是要被new出來使用的。當(dāng)堆中對象的作用域結(jié)束的時(shí)候,這部分內(nèi)存也不會立刻被回收,而是等待系統(tǒng)GC進(jìn)行回收。
- 所謂的內(nèi)存分析,就是分析Heap中的內(nèi)存狀態(tài)。
1.2 Android中的沙盒機(jī)制
大家可能都聽說過IOS中有沙盒機(jī)制(sandbox),但是我們的Android系統(tǒng)中也存在沙盒機(jī)制,只不過沒有IOS中的嚴(yán)格,所以常常被人忽略。
由于Android是建立在Linux系統(tǒng)之上的,所以Android系統(tǒng)繼承了Linux的 類Unix繼承進(jìn)程隔離機(jī)制與最小權(quán)限原則,并且在原有Linux的進(jìn)程管理基礎(chǔ)上對UID的使用做了改進(jìn),形成了Android應(yīng)用的”沙箱“機(jī)制。
普通的Linux中啟動的應(yīng)用通常和登陸用戶相關(guān)聯(lián),同一用戶的UID相同。但是Android中給不同的應(yīng)用都賦予了不同的UID,這樣不同的應(yīng)用將不能相互訪問資源。對應(yīng)用而言,這樣會更加封閉,安全。
引文來自Android的SandBox(沙箱)
在Android系統(tǒng)中,應(yīng)用(通常)都在一個(gè)獨(dú)立的沙箱中運(yùn)行,即每一個(gè)Android應(yīng)用程序都在它自己的進(jìn)程中運(yùn)行,都擁有一個(gè)獨(dú)立的Dalvik虛擬機(jī)實(shí)例。Dalvik經(jīng)過優(yōu)化,允許在有限的內(nèi)存中同時(shí)高效地運(yùn)行多個(gè)虛擬機(jī)的實(shí)例,并且每一個(gè)Dalvik應(yīng)用作為一個(gè)獨(dú)立的Linux進(jìn)程執(zhí)行。Android這種基于Linux的進(jìn)程“沙箱”機(jī)制,是整個(gè)安全設(shè)計(jì)的基礎(chǔ)之一。
引文來自淺析Android沙箱模型
簡單點(diǎn)說就是在Android的世界中每一個(gè)應(yīng)用相當(dāng)與一個(gè)Linux中的用戶,他們相互獨(dú)立,不能相互共享與訪問,(這也就解釋了Android系統(tǒng)中為什么需要進(jìn)程間通信),正是由于沙盒機(jī)制的存在最大程度的保護(hù)了應(yīng)用之間的安全,但是也帶來了每一個(gè)應(yīng)用所分配的內(nèi)存大小是有限制的問題。
2. Generational Heap Memory內(nèi)存模型的概述
在Android和Java中都存在著一個(gè)Generational(讀音:[?d?en??re???nl]) Heap Memory模型,系統(tǒng)會根據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型分別執(zhí)行不同的GC操作。Generational Heap Memory模型主要由:Young Generation(新生代)、Old Generation(舊生代)、Permanent(讀音:[?p?:rm?n?nt]) Generation三個(gè)區(qū)域組成,而且這三個(gè)區(qū)域存在明顯的層級關(guān)系。所以此模型也可以成為三級Generation的內(nèi)存模型。

其中Young Generation區(qū)域存放的是最近被創(chuàng)建對象,此區(qū)域最大的特點(diǎn)就是創(chuàng)建的快,被銷毀的也很快。當(dāng)對象在Young Generation區(qū)域停留的時(shí)間到達(dá)一定程度的時(shí)候,它就會被移動到Old Generation區(qū)域中,同理,最后他將會被移動到Permanent Generation區(qū)域中。

在三級Generation內(nèi)存模型中,每一個(gè)區(qū)域的大小都是有固定值的,當(dāng)進(jìn)入的對象總大小到達(dá)某一級內(nèi)存區(qū)域閥值的時(shí)候就會觸發(fā)GC機(jī)制,進(jìn)行垃圾回收,騰出空間以便其他對象進(jìn)入。

不僅如此,不同級別的Generation區(qū)域GC是需要的時(shí)間也是不同的。同等對象數(shù)目下,Young Generation GC所需時(shí)間最短,Old Generation次之,Permanent Generation 需要的時(shí)間最長。當(dāng)然GC執(zhí)行的長短也和當(dāng)前Generation區(qū)域中的對象數(shù)目有關(guān)。遍歷查找20000個(gè)對象比起遍歷50個(gè)對象自然是要慢很多的。
3. GC機(jī)制概述
與C++不用,在Java中,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程序員不需要通過調(diào)用函數(shù)來釋放內(nèi)存,但也隨之帶來了內(nèi)存泄漏的可能。簡單點(diǎn)說:對于 C++ 來說,內(nèi)存泄漏就是new出來的對象沒有 delete,俗稱野指針;而對于 java 來說,就是 new 出來的 Object 放在 Heap 上無法被GC回收。
Android使用的主要開發(fā)語言是Java所以二者的GC機(jī)制原理也大同小異,所以我們只對于常見的JVM GC機(jī)制的分析,就能達(dá)到我們的目的。我還是先看看那二者的不同之處吧。
3.1 Dalvik 和標(biāo)準(zhǔn)Java虛擬機(jī)的區(qū)別
3.1.1 Dalvik 和標(biāo)準(zhǔn)Java虛擬機(jī)的主要區(qū)別
Dalvik虛擬機(jī)(DVM)是Android系統(tǒng)在java虛擬機(jī)(JVM)基礎(chǔ)上優(yōu)化得到的,DVM是基于寄存器的,而JVM是基于棧的,由于寄存器高效快速的特性,DVM的性能相比JVM更好。
3.1.2 Dalvik 和 java 字節(jié)碼的區(qū)別
Dalvik執(zhí)行.dex格式的字節(jié)碼文件,JVM執(zhí)行的是.class格式的字節(jié)碼文件,Android程序在編譯之后產(chǎn)生的.class 文件會被aapt工具處理生成R.class等文件,然后dx工具會把.class文件處理成.dex文件,最終資源文件和.dex文件等打包成.apk文件。
3.2 分別對Young Generation(新生代)和Old Generation(舊生代)采用的兩種垃圾回收機(jī)制?
3.2.1 對于Young Generation(新生代)的GC
由于Young Generation通常存活的時(shí)間比較短,所以Young Generation采用了Copying算法進(jìn)行回收,Copying算法就是掃描出存活的對象,并復(fù)制到一塊新的空間中,這個(gè)過程就是下圖Eden與Survivor Space之間的復(fù)制過程。Young Generation采用空閑指針的方式來控制GC觸發(fā),指針保存最后一個(gè)分配在Young Generation中分配空間地對象的位置。當(dāng)有新的對象要分配內(nèi)存空間的時(shí)候,就會主動檢測空間是否足夠,不夠的情況下就出觸發(fā)GC,當(dāng)連續(xù)分配對象時(shí),對象會逐漸從Eden移動到Survivor,最后移動到Old Generation。

3.2.2 對于Old Generation(舊生代)的GC
Old Generation與Young Generation不同,對象存活的時(shí)間比較長,比較穩(wěn)固,因此采用標(biāo)記(Mark)算法來進(jìn)行回收。所謂標(biāo)記就是掃描出存活的對象,然后在回收未必標(biāo)記的對象。回收后的剩余空間要么進(jìn)行合并,要么標(biāo)記出來便于下次進(jìn)行分配,總之就是要減少內(nèi)存碎片帶來的效率損耗。
3.4 如何判斷對象是否可以被回收
從上面的一小節(jié)中我們知道了不同的區(qū)域GC機(jī)制是有所不同的,那么這些垃圾是如何被發(fā)現(xiàn)的呢?下面我們就看一下兩種常見的判斷方法:引用計(jì)數(shù)、對象引用遍歷。
3.4.1引用計(jì)數(shù)器
引用計(jì)數(shù)器是垃圾收集器中的早起策略。這種方法中,每個(gè)對象實(shí)體(不是它的引用)都有一個(gè)引用計(jì)數(shù)器。當(dāng)一個(gè)對象創(chuàng)建的時(shí)候,且將該對象分配給一個(gè)每分配給一個(gè)變量,計(jì)數(shù)器就+1,當(dāng)一個(gè)對象的某個(gè)引用超過了生命周期或者被設(shè)置一個(gè)新值時(shí),對象計(jì)數(shù)器就-1,任何引用計(jì)數(shù)器為 0 的對象可以被當(dāng)作垃圾收集。當(dāng)一個(gè)對象被垃圾收集時(shí),引用的任何對象技術(shù) - 1。
優(yōu)點(diǎn):執(zhí)行快,交織在程序運(yùn)行中,對程序不被長時(shí)間打斷的實(shí)時(shí)環(huán)境比較有利。
缺點(diǎn):無法檢測出循環(huán)引用。比如:對象A中有對象B的引用,而B中同時(shí)也有A的引用。
3.4.2 跟蹤收集器
現(xiàn)在的垃圾回收機(jī)制已經(jīng)不太使用引用計(jì)數(shù)器的方法判斷是否可回收,而是使用跟蹤收集器方法。
現(xiàn)在大多數(shù)JVM采用對象引用遍歷機(jī)制從程序的主要運(yùn)行對象(如靜態(tài)對象/寄存器/棧上指向的堆內(nèi)存對象等)開始檢查引用鏈,去遞歸判斷對象收否可達(dá),如果不可達(dá),則作為垃圾回收,當(dāng)然在便利階段,GC必須記住那些對象是可達(dá)的,以便刪除不可到達(dá)的對象,這稱為標(biāo)記(marking)對象。
下一步,GC就要?jiǎng)h除這些不可達(dá)的對象,在刪除時(shí)未必標(biāo)記的對象,釋放它們的內(nèi)存的過程叫做清除(sweeping),而這樣會造成內(nèi)存碎片化,布局已分配給新的對象,但是他們集合起來還很大。所以很多GC機(jī)制還要重新組織內(nèi)存中的對象,并進(jìn)行壓縮,形成大塊、可利用的空間。
為了達(dá)到這個(gè)目的,GC需要停止程序的其他活動,阻塞進(jìn)程。這里我們要注意的是:**不要頻繁的引發(fā)GC,執(zhí)行GC操作的時(shí)候,任何線程的任何操作都會需要暫停,等待GC操作完成之后,其他操作才能夠繼續(xù)運(yùn)行, 故而如果程序頻繁GC, 自然會導(dǎo)致界面卡頓. **通常來說,單個(gè)的GC并不會占用太多時(shí)間,但是大量不停的GC操作則會顯著占用幀間隔時(shí)間(16ms??蓞⒁?a href="http://www.itdecent.cn/p/906cd1af2ce7" target="_blank">《【Android 性能優(yōu)化】—— UI篇》)。如果在幀間隔時(shí)間里面做了過多的GC操作,那么自然其他類似計(jì)算,渲染等操作的可用時(shí)間就變得少了。
4. Android內(nèi)存泄漏分析
4.1 什么內(nèi)存泄漏
對于 C++ 來說,內(nèi)存泄漏就是new出來的對象沒有 delete,俗稱野指針;而對于 java 來說,就是 new 出來的 Object 放在 Heap 上無法被GC回收
4.2 為什么不能被回收
GC過程與對象的引用類型是嚴(yán)重相關(guān)的,下面我們就看看Java中(Android中存在差異)對于引用的四種分類:
- 強(qiáng)引用(Strong Reference):JVM寧愿拋出OOM,也不會讓GC回收的對象
- 軟引用(Soft Reference) :只有內(nèi)存不足時(shí),才會被GC回收。
- 弱引用(weak Reference):在GC時(shí),一旦發(fā)現(xiàn)弱引用,立即回收
-
虛引用(Phantom Reference):任何時(shí)候都可以被GC回收,當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對象時(shí),如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前,把這個(gè)虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。程序可以通過判斷引用隊(duì)列中是否存在該對象的虛引用,來了解這個(gè)對象是否將要被回收。可以用來作為GC回收Object的標(biāo)志。
注意Android中存在的差異
但是在2.3以后版本中,系統(tǒng)會優(yōu)先將SoftReference的對象提前回收掉, 即使內(nèi)存夠用,其他和Java中是一樣的。所以谷歌官方建議用LruCache(least recentlly use 最少最近使用算法)。會將內(nèi)存控制在一定的大小內(nèi), 超出最大值時(shí)會自動回收, 這個(gè)最大值開發(fā)者自己定。其實(shí)LruCache就是用了很多的HashMap,三百多行的代碼
在開發(fā)過程中,保存對象,這時(shí)我很可以直接使用LruCache來代替,Bitmap對象:
在Android開發(fā)過程中,我們常常使用HasMap保存對象,但是為了防止內(nèi)存泄漏,在保存內(nèi)存占用較大、生命周期較長的對象的時(shí)候,盡量使用LruCache代替HasMap用于保存對象。
//指定最大緩存空間
private static final int MAX_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8);
LruCache<String,Bitmap> mBitmapLruCache = new LruCache<>(MAX_SIZE);
而造成不能回收的根本原因就是:堆內(nèi)存中長生命周期的對象持有短生命周期對象的強(qiáng)/軟引用,盡管短生命周期對象已經(jīng)不再需要,但是因?yàn)殚L生命周期對象持有它的引用而導(dǎo)致不能被回收。
4.3 如何的監(jiān)聽系統(tǒng)發(fā)生GC
那么怎樣才能去監(jiān)聽系統(tǒng)的GC過程呢?其實(shí)非常簡單,系統(tǒng)每進(jìn)行一次GC操作時(shí),都會在LogCat中打印一條日志,我們只要去分析這條日志就可以了,日志的基本格式如下所示:
DVM中
D/dalvikvm(30615): GC FOR ALLOC freed 4442K, 25% free 20183K/26856K, paused 24ms , total 24ms
ART中
I/art(198): Explicit concurrent mark sweep GC freed 700(30KB) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 186us total 12.763ms
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <Pause_time>
原因,一般情況下一共有以下幾種觸發(fā)GC操作的原因:
- GC_CONCURRENT: 當(dāng)我們應(yīng)用程序的堆內(nèi)存快要滿的時(shí)候,系統(tǒng)會自動觸發(fā)GC操作來釋放內(nèi)存。
- GC_FOR_MALLOC: 當(dāng)我們的應(yīng)用程序需要分配更多內(nèi)存,可是現(xiàn)有內(nèi)存已經(jīng)不足的時(shí)候,系統(tǒng)會進(jìn)行GC操作來釋放內(nèi)存。
- GC_HPROF_DUMP_HEAP: 當(dāng)生成HPROF文件的時(shí)候,系統(tǒng)會進(jìn)行GC操作,關(guān)于HPROF文件我們下面會講到。
- GC_EXPLICIT: 這種情況就是我們剛才提到過的,主動通知系統(tǒng)去進(jìn)行GC操作,比如調(diào)用System.gc()方法來通知系統(tǒng)?;蛘咴贒DMS中,通過工具按鈕也是可以顯式地告訴系統(tǒng)進(jìn)行GC操作的。
接下來第二部分Amount_freed,表示系統(tǒng)通過這次GC操作釋放了多少內(nèi)存。
然后Heap_stats中會顯示當(dāng)前內(nèi)存的空閑比例以及使用情況(活動對象所占內(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è)阻塞的過程并不會很長,也就是幾百毫秒,但是用戶在使用我們的程序時(shí)還是有可能會感覺到略微的卡頓。
而自2.3之后,GC操作改成了并發(fā)的方式進(jìn)行,就是說GC的過程中不會影響到應(yīng)用程序的正常運(yùn)行,但是在GC操作的開始和結(jié)束的時(shí)候會短暫阻塞一段時(shí)間,不過優(yōu)化到這種程度,用戶已經(jīng)是完全無法察覺到了。
4.4 導(dǎo)致GC頻繁執(zhí)行有兩個(gè)原因:
由于GC會阻塞進(jìn)程,所以我們不避免頻繁的GC。
- Memory Churn(內(nèi)存抖動),內(nèi)存抖動是因?yàn)榇罅康膶ο蟊粍?chuàng)建又在短時(shí)間內(nèi)馬上被釋放。
- 瞬間產(chǎn)生大量的對象會嚴(yán)重占用Young Generation的內(nèi)存區(qū)域,當(dāng)達(dá)到閥值,剩余空間不夠的時(shí)候,也會觸發(fā)GC。即使每次分配的對象占用了很少的內(nèi)存,但是他們疊加在一起會增加 Heap的壓力,從而觸發(fā)更多其他類型的GC。這個(gè)操作有可能會影響到幀率,并使得用戶感知到性能問題。

解決上面的問題有簡潔直觀方法,如果你在Memory Monitor里面查看到短時(shí)間發(fā)生了多次內(nèi)存的漲跌,這意味著很有可能發(fā)生了內(nèi)存抖動。

內(nèi)存泄漏的檢測與處理
干說不練假把式,說這么多的內(nèi)存知識,下面就讓我們看看Android給我們提供了那些工具來解決內(nèi)存泄漏的問題。例如
熟悉Android Studio界面
工欲善其事,必先利其器。我們接下來先來熟悉下Android Studio的界面
一般分析內(nèi)存泄露, 首先運(yùn)行程序,打開日志控制臺,有一個(gè)標(biāo)簽Memory ,我們可以在這個(gè)界面分析當(dāng)前程序使用的內(nèi)存情況, 一目了然, 我們再也不需要苦苦的在logcat中尋找內(nèi)存的日志了。
圖中藍(lán)色區(qū)域,就是程序使用的內(nèi)存, 灰色區(qū)域就是空閑內(nèi)存, 當(dāng)然,Android內(nèi)存分配機(jī)制是對每個(gè)應(yīng)用程序逐步增加, 比如你程序當(dāng)前使用30M內(nèi)存, 系統(tǒng)可能會給你分配40M, 當(dāng)前就有10M空閑, 如果程序使用了50M了,系統(tǒng)會緊接著給當(dāng)前程序增加一部分,比如達(dá)到了80M, 當(dāng)前你的空閑內(nèi)存就是30M了。 當(dāng)然,系統(tǒng)如果不能再給你分配額外的內(nèi)存,程序自然就會OOM(內(nèi)存溢出)了。 每個(gè)應(yīng)用程序最高可以申請的內(nèi)存和手機(jī)密切相關(guān),比如我當(dāng)前使用的華為Mate7,極限大概是200M,算比較高的了, 一般128M 就是極限了, 甚至有的手機(jī)只有可憐的16M或者32M,這樣的手機(jī)相對于內(nèi)存溢出的概率非常大了。
我們怎么檢測內(nèi)存泄露呢
首先需要明白一個(gè)概念, 內(nèi)存泄露就是指,本應(yīng)該回收的內(nèi)存,還駐留在內(nèi)存中。 一般情況下,高密度的手機(jī),一個(gè)頁面大概就會消耗20M內(nèi)存,如果發(fā)現(xiàn)退出界面,程序內(nèi)存遲遲不降低的話,可能就發(fā)生了嚴(yán)重的內(nèi)存泄露。 我們可以反復(fù)進(jìn)入該界面,然后點(diǎn)擊dump Java heap 這個(gè)按鈕,然后Android Studio就開始干活了,下面的圖就是正在dump
dump成功后會自動打開 hprof文件,文件以Snapshot+時(shí)間來命名
MAT
通過Android Studio自帶的界面,查看內(nèi)存泄露還不是很智能,我們可以借助第三方工具,常見的工具就是MAT了,下載地址 http://eclipse.org/mat/downloads.php ,這里我們需要下載獨(dú)立版的MAT. 下圖是MAT一開始打開的界面, 這里需要提醒大家的是,MAT并不會準(zhǔn)確地告訴我們哪里發(fā)生了內(nèi)存泄漏,而是會提供一大堆的數(shù)據(jù)和線索,我們需要自己去分析這些數(shù)據(jù)來去判斷到底是不是真的發(fā)生了內(nèi)存泄漏。
接下來我們需要用MAT打開內(nèi)存分析的文件, 上文給大家介紹了使用Android Studio生成了 hprof文件, 這個(gè)文件在呢, 在Android Studio中的Captrues這個(gè)目錄中,可以找到
注意,這個(gè)文件不能直接交給MAT, MAT是不識別的, 我們需要右鍵點(diǎn)擊這個(gè)文件,轉(zhuǎn)換成MAT識別的。
然后用MAT打開導(dǎo)出的hprof(File->Open heap dump) MAT會幫我們分析內(nèi)存泄露的原因
LeakCanary
上面介紹了MAT檢測內(nèi)存泄露, 再給大家介紹LeakCanary。 項(xiàng)目地址:https://github.com/square/leakcanary
LeakCanary會檢測應(yīng)用的內(nèi)存回收情況,如果發(fā)現(xiàn)有垃圾對象沒有被回收,就會去分析當(dāng)前的內(nèi)存快照,也就是上邊MAT用到的.hprof文件,找到對象的引用鏈,并顯示在頁面上。這款插件的好處就是,可以在手機(jī)端直接查看內(nèi)存泄露的地方,可以輔助我們檢測內(nèi)存泄露
使用: 在build.gradle文件中添加,不同的編譯使用不同的引用:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}
在應(yīng)用的Application onCreate方法中添加LeakCanary.install(this),如下
public class ExampleApplication extends Application
@Override
public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}
應(yīng)用運(yùn)行起來后,LeakCanary會自動去分析當(dāng)前的內(nèi)存狀態(tài),如果檢測到泄漏會發(fā)送到通知欄,點(diǎn)擊通知欄就可以跳轉(zhuǎn)到具體的泄漏分析頁面。 Tips:就目前使用的結(jié)果來看,絕大部分泄漏是由于使用單例模式hold住了Activity的引用,比如傳入了context或者將Activity作為listener設(shè)置了進(jìn)去,所以在使用單例模式的時(shí)候要特別注意,還有在Activity生命周期結(jié)束的時(shí)候?qū)⒁恍┳远x監(jiān)聽器的Activity引用置空。 關(guān)于LeakCanary的更多分析可以看項(xiàng)目主頁的介紹,還有這里http://www.liaohuqiu.net/cn/posts/leak-canary-read-me/
追蹤內(nèi)存分配
如果我們想了解內(nèi)存分配更詳細(xì)的情況,可以使用Allocation Traker來查看內(nèi)存到底被什么占用了。 用法很簡單:
點(diǎn)一下是追蹤, 再點(diǎn)一下是停止追蹤, 停止追蹤后 .alloc文件會自動打開,打開后界面如下:
查詢方法執(zhí)行的時(shí)間
Android Studio 功能越來越強(qiáng)大了, 我們可以借助AS觀測各種性能,如下圖:
如果我們要觀測方法執(zhí)行的時(shí)間,就需要來到CPU界面
點(diǎn)擊Start Method Tracking, 一段時(shí)間后再點(diǎn)擊一次, trace文件被自動打開,
非獨(dú)占時(shí)間: 某函數(shù)占用的CPU時(shí)間,包含內(nèi)部調(diào)用其它函數(shù)的CPU時(shí)間。 獨(dú)占時(shí)間: 某函數(shù)占用CPU時(shí)間,但不含內(nèi)部調(diào)用其它函數(shù)所占用的CPU時(shí)間。
我們?nèi)绾闻袛嗫赡苡袉栴}的方法?
通過方法的調(diào)用次數(shù)和獨(dú)占時(shí)間來查看,通常判斷方法是:
如果方法調(diào)用次數(shù)不多,但每次調(diào)用卻需要花費(fèi)很長的時(shí)間的函數(shù),可能會有問題。
如果自身占用時(shí)間不長,但調(diào)用卻非常頻繁的函數(shù)也可能會有問題。
6. 常見的內(nèi)存泄漏
6.1 永遠(yuǎn)的單例(Singleton)
為了完美解決我們在程序中反復(fù)創(chuàng)建同一對象的問題,我們選用了單例模式,單例在我們的程序中隨處可見,但是由于單例模式的靜態(tài)特性,使得它的生命周期和我們的應(yīng)用一樣長,一不小心讓單例無限制的持有Activity的強(qiáng)引用就會導(dǎo)致內(nèi)存泄漏。例如:
public class SingleTon{
private Context context;
private static SingleTon singleTon;
public static final SingleTon getInstance(Context context){
this.context = context;
return SingleHolder.INSTANCE;
}
private static class SingleHolder{
private static final SingleTon INSTANCE = new SingleTon();
}
}
解決辦法:
這個(gè)錯(cuò)誤很普遍,這個(gè)是一個(gè)很正常的單利模式,但是由于傳入了一個(gè)Context,而這個(gè)Context的生命周期就的長短就尤為重要了。如果我們傳入的是某個(gè)Activity的Context,而當(dāng)這個(gè)Activity推出的時(shí)候,由于該Context的強(qiáng)引用被單例持有,那么這個(gè)Activity就等同于擁有了整個(gè)程序的生命周期。這種情況下,當(dāng)Activity退出的時(shí)候內(nèi)存并沒有被回收,這就造成了內(nèi)存泄漏。
正確的做法就是應(yīng)該把傳入的Context改為同應(yīng)用生命周期一樣長的Application中的Context。
public static final SingleTon getInstance(Context context){
this.context = context.getApplicationContext;
return SingleHolder.INSTANCE;
}
當(dāng)然我們可以直接重寫Application,提供getContext方法,不必在依靠傳入的參數(shù):
public class BaseApplication extends Application{
private static BaseApplication baseApplication;
@Override
public void onCreate(){
super.onCreate();
baseApplication = this;
}
public static Context getContext{
baseApplication.getApplicationContext();
}
}
6.2 Handler引起的內(nèi)存泄漏
Handler引起的內(nèi)存泄漏在我們開發(fā)中最為常見的。我們知道Handler、Message、MessageQueue都是相互關(guān)聯(lián)在一起的,萬一Handler發(fā)送的Message尚未被處理,那么該Message以及發(fā)送它的Handler對象都會被線程MessageQueue一直持有。
由于Handler屬于TLS(Thread Local Storage)變量,生命周期和Activity是不一致的,因此這種實(shí)現(xiàn)方式很難保證跟Activity的生命周期一直,所以很容易無法釋放內(nèi)存。比如:
public class HandlerBadActivity extends AppCompatActivity {
private final Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_bad);
// 延遲5min發(fā)送一個(gè)消息
handler.postDelayed(new Runnable() {
@Override
public void run() {
// write something
}
},1000*60*5);
this.finish();
}
}
我們在例子中生命了一個(gè)延時(shí)5分鐘執(zhí)行的Message,當(dāng)該Activity退出的時(shí)候,延時(shí)任務(wù)(Message)還在主線成的MessageQueue中等待,此時(shí)的Message持有Handler的強(qiáng)引用,并且由于Handler是HandlerBadActivity的非靜態(tài)內(nèi)部類,所以Handler會持有HandlerBadActivity的強(qiáng)引用,此時(shí)HandlerBadActivity退出時(shí)無法進(jìn)行內(nèi)存回收,造成內(nèi)存泄漏。
解決辦法:
將Handler生命為靜態(tài)內(nèi)部類,這樣它就不會持有外部來的引用了。這樣以來Handler的的生命周期就與Activity無關(guān)了。不過倘若用到Context等外部類的非static對象,還是應(yīng)該通過使用Application中與應(yīng)用同生命周期的Context比較合適。比如:
public class HandlerGoodActivity extends AppCompatActivity {
private static final class MyHandler extends Handler {
private Context mActivity;
public MyHandler(HandlerGoodActivity activity) {
//使用生命周期與應(yīng)用同長的getApplicationContext
this.mActivity = activity.getApplicationContext();
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (mActivity != null) {
// write something
}
}
}
private final MyHandler myHandler = new MyHandler(this);
// 匿名內(nèi)部類在static的時(shí)候絕對不會持有外部類的引用
private static final Runnable RUNNABLE = new Runnable() {
@Override
public void run() {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_good);
myHandler.postDelayed(RUNNABLE, 1000 * 60 * 5);
}
雖然我們結(jié)局了Activity的內(nèi)存泄漏問題,但是經(jīng)過Handler發(fā)送的延時(shí)消息還在MessageQueue中,Looper也在等待處理消息,所以我們要在Activity銷毀的時(shí)候處理掉隊(duì)列中的消息。
@Override
protected void onDestroy() {
super.onDestroy();
//傳入null,就表示移除所有Message和Runnable
myHandler.removeCallbacksAndMessages(null);
}
6.3 匿名內(nèi)部類在一步線程中的使用
它們方便卻暗藏殺機(jī)。Android開發(fā)經(jīng)常會繼承實(shí)現(xiàn) Activity 或者 Fragment 或者 View。如果你使用了匿名類,而又被異步線程所引用,那得小心,如果沒有任何措施同樣會導(dǎo)致內(nèi)存泄漏的:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_inner_bad);
Runnable runnable1 = new MyRunnable();
Runnable runnable2 = new Runnable() {
@Override
public void run() {
}
};
}
private static class MyRunnable implements Runnable{
@Override
public void run() {
}
}
}
runnable1 和 runnable2的區(qū)別就是,runnable2使用了匿名內(nèi)部類,我們看看引用時(shí)的引用內(nèi)存

可以看到,runnable1是沒有什么特別的。但runnable2多出了一個(gè)MainActivity的引用,若是這個(gè)引用再傳入到一個(gè)異步線程,此線程在和Activity生命周期不一致的時(shí)候,也就造成了Activity的泄露。
6.4 善用static成員變量
從前面的介紹我們知道,static修飾的變量位于內(nèi)存的靜態(tài)存儲區(qū),此變量與App的生命周期一致
這必然會導(dǎo)致一系列問題,如果你的app進(jìn)程設(shè)計(jì)上是長駐內(nèi)存的,那即使app切到后臺,這部分內(nèi)存也不會被釋放。按照現(xiàn)在手機(jī)app內(nèi)存管理機(jī)制,占內(nèi)存較大的后臺進(jìn)程將優(yōu)先回收,因?yàn)槿绻薬pp做過進(jìn)程互保?;睿菚斐蒩pp在后臺頻繁重啟。當(dāng)手機(jī)安裝了你參與開發(fā)的app以后一夜時(shí)間手機(jī)被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。
這里修復(fù)的方法是:
不要在類初始時(shí)初始化靜態(tài)成員??梢钥紤]lazy初始化(延遲加載)。架構(gòu)設(shè)計(jì)上要思考是否真的有必要這樣做,盡量避免。如果架構(gòu)需要這么設(shè)計(jì),那么此對象的生命周期你有責(zé)任管理起來。
6.5 避免使用非靜態(tài)內(nèi)部類和匿名類,多用private static class。
在我們的日常代碼中,這樣的情況似乎很常見,及直接寫一個(gè)class就這么光禿禿的情況

這樣就在Activity內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動Activity時(shí)都會使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復(fù)創(chuàng)建,不過這種寫法卻會造成內(nèi)存泄漏,因?yàn)榉庆o態(tài)內(nèi)部類默認(rèn)會持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個(gè)靜態(tài)的實(shí)例,該實(shí)例的生命周期和應(yīng)用的一樣長,這就導(dǎo)致了該靜態(tài)實(shí)例一直會持有該Activity的引用,導(dǎo)致Activity的內(nèi)存資源不能正常回收。正確的做法為:
將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個(gè)單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。當(dāng)然,Application 的 context 不是萬能的,所以也不能隨便亂用,對于有些地方則必須使用 Activity 的 Context,對于Application,Service,Activity三者的Context的應(yīng)用場景如下:

其中: NO1表示 Application 和 Service 可以啟動一個(gè) Activity,不過需要?jiǎng)?chuàng)建一個(gè)新的 task 任務(wù)隊(duì)列。而對于 Dialog 而言,只有在 Activity 中才能創(chuàng)建
6.6 集合引發(fā)的內(nèi)存泄漏
我們通常會把一些對象的引用加入到集合容器(比如ArrayList)中,當(dāng)我們不再需要該對象時(shí),并沒有把它的引用從集合中清理掉,當(dāng)集合中的內(nèi)容過于大的時(shí)候,并且是static的時(shí)候就造成了內(nèi)存泄漏,所有我們最好在onDestory情況并讓其不可達(dá)
private List<String> nameList;
private List<Fragment> list;
@Override
public void onDestroy() {
super.onDestroy();
if (nameList != null){
nameList.clear();
nameList = null;
}
if (list != null){
list.clear();
list = null;
}
}
6.7 webView引發(fā)的內(nèi)存泄漏
WebView解析網(wǎng)頁時(shí)會申請Native堆內(nèi)存用于保存頁面元素,當(dāng)頁面較復(fù)雜時(shí)會有很大的內(nèi)存占用。如果頁面包含圖片,內(nèi)存占用會更嚴(yán)重。并且打開新頁面時(shí),為了能快速回退,之前頁面占用的內(nèi)存也不會釋放。有時(shí)瀏覽十幾個(gè)網(wǎng)頁,都會占用幾百兆的內(nèi)存。這樣加載網(wǎng)頁較多時(shí),會導(dǎo)致系統(tǒng)不堪重負(fù),最終強(qiáng)制關(guān)閉應(yīng)用,也就是出現(xiàn)應(yīng)用閃退或重啟。
由于占用的都是Native堆內(nèi)存,所以實(shí)際占用的內(nèi)存大小不會顯示在常用的DDMS Heap工具中(這里看到的只是Java虛擬機(jī)分配的內(nèi)存,一般即使Native堆內(nèi)存已經(jīng)占用了幾百兆,這里顯示的還只是幾兆或十幾兆)。只有使用adb shell中的一些命令比如dumpsys meminfo 包名,或者在程序中使用Debug.getNativeHeapSize()才能看到。
據(jù)說由于WebView的一個(gè)BUG,即使它所在的Activity(或者Service)結(jié)束也就是onDestroy()之后,或者直接調(diào)用WebView.destroy()之后,它所占用這些內(nèi)存也不會被釋放。
解決這個(gè)問題最直接的方法是:把使用了WebView的Activity(或者Service)放在單獨(dú)的進(jìn)程里。然后在檢測到應(yīng)用占用內(nèi)存過大有可能被系統(tǒng)干掉或者它所在的Activity(或者Service)結(jié)束后,調(diào)用System.exit(0),主動Kill掉進(jìn)程。由于系統(tǒng)的內(nèi)存分配是以進(jìn)程為準(zhǔn)的,進(jìn)程關(guān)閉后,系統(tǒng)會自動回收所有內(nèi)存。
關(guān)于WebView的跟多內(nèi)容請參見 : Android WebView Memory Leak WebView內(nèi)存泄漏
6.8其他常見的引起內(nèi)存泄漏原因
- 構(gòu)造Adapter時(shí),沒有使用緩存的 convertView
- Bitmap在不使用的時(shí)候沒有使用recycle()釋放內(nèi)存
- 非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例容易造成內(nèi)存泄漏:即一個(gè)類中如果你不能夠控制它其中內(nèi)部類的生命周期(譬如Activity中的一些特殊Handler等),則盡量使用靜態(tài)類和弱引用來處理(譬如ViewRoot的實(shí)現(xiàn))。
- 警惕線程未終止造成的內(nèi)存泄露;譬如在Activity中關(guān)聯(lián)了一個(gè)生命周期超過Activity的Thread,在退出Activity時(shí)切記結(jié)束線程。一個(gè)典型的例子就是HandlerThread的run方法是一個(gè)死循環(huán),它不會自己結(jié)束,線程的生命周期超過了Activity生命周期,我們必須手動在Activity的銷毀方法中中調(diào)運(yùn)thread.getLooper().quit();才不會泄露。
- 對象的注冊與反注冊沒有成對出現(xiàn)造成的內(nèi)存泄露;譬如注冊廣播接收器、注冊觀察者(典型的譬如數(shù)據(jù)庫的監(jiān)聽)等。
- 創(chuàng)建與關(guān)閉沒有成對出現(xiàn)造成的泄露;譬如Cursor資源必須手動關(guān)閉,WebView必須手動銷毀,流等對象必須手動關(guān)閉等。
- 不要在執(zhí)行頻率很高的方法或者循環(huán)中創(chuàng)建對象(比如onMeasure),可以使用HashTable等創(chuàng)建一組對象容器從容器中取那些對象,而不用每次new與釋放。
- 避免代碼設(shè)計(jì)模式的錯(cuò)誤造成內(nèi)存泄露;譬如循環(huán)引用,A持有B,B持有C,C持有A,這樣的設(shè)計(jì)誰都得不到釋放。
總結(jié):
- Android內(nèi)存優(yōu)化主要是針對堆(Heap)而言的,當(dāng)堆中對象的作用域結(jié)束的時(shí)候,這部分內(nèi)存也不會立刻被回收,而是等待系統(tǒng)GC進(jìn)行回收。
- Java中造成內(nèi)存泄漏的根本原因是:堆內(nèi)存中長生命周期的對象持有短生命周期對象的強(qiáng)/軟引用,盡管短生命周期對象已經(jīng)不再需要,但是因?yàn)殚L生命周期對象持有它的引用而導(dǎo)致不能被回收。
參考:
