先給出一個內(nèi)存泄漏的栗子
我們在項目中經(jīng)常會創(chuàng)建一些工具類,例如獲取屏幕信息的、SharePreference、圖片壓縮等工具類,而且我們往往寫成單利,例如下面的CommonUtils所示。(防止內(nèi)存泄漏就應該使用Application Context)
/**
* 測試內(nèi)存泄漏
*/
public class CommonUtils {
private static CommonUtils sInstance;
private Context mContext;
private CommonUtils(Context context) {
mContext = context;
}
public static CommonUtils getInstance(Context context) {
if (sInstance == null) {
sInstance = new CommonUtils(context);
}
return sInstance;
}
}
在Activity中,我們經(jīng)常這樣使用:
public class LeakActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
CommonUtils.getInstance(this);
}
}
這就是很典型的單例模式導致內(nèi)存對象無法釋放而導致Activity內(nèi)存泄露,因為CommonUtils持有Activity的引用,導致Activity不能被正?;厥?。即Destroy之后還存在。
我們先通過這一個簡單的栗子來演示一遍工具的使用,然后總結一下:在不知道哪里發(fā)生內(nèi)存泄漏的時候,我們應該如何利用工具提供的線索去分析。
Android Studio Android Monitor的使用
首先運行我們的項目,然后打開Android Monitor。如下圖所示:

Android Monitor可以看到手機的一些信息,我們目前最關心的是Memory這一板塊。這里可以看到我們APP進程的空閑內(nèi)存和已經(jīng)分配的內(nèi)存,如圖右邊所示。在圖的前面部分我們可以看到分配內(nèi)存突然減少,這就是GC在回收垃圾了。
內(nèi)存抖動:當內(nèi)存發(fā)生很頻繁地起伏的時候,這就是內(nèi)存抖動,這時候就要注意是不是內(nèi)存泄漏引起的了。
左上角有三個小工具,這里先介紹前面兩個,第一個是手動觸發(fā)GC,第二個是拍堆內(nèi)存快照。

我們手動觸發(fā)GC多幾次,等到內(nèi)存穩(wěn)定下來的時候,然后拍快照,這時候AS會自動拉去hprf文件并且打開。然后我們旋轉屏幕,觸發(fā)內(nèi)存泄漏,然后再重復這樣的操作。

我們可以通過選擇查看APP、Image、Zygote的內(nèi)存信息,也可以選擇按照包名來展示。這里需要解釋的參數(shù)有:單位是byte
Total Count:對象的總個數(shù)。
Heap Count:對象在堆內(nèi)存中的個數(shù)。
Sizeof:對象本身的大小。
Shallow Size:所有該類的對象的大小總和。
Retained Size:所有該類的對象釋放的時候(包括引用了該類的對象的釋放)釋放的總內(nèi)存之和。
Depth:對象被引用的深度,如果沒有被引用,則深度為0;直接引用,深度為1,如此類推。
Dominating Size:對象管轄的內(nèi)存大小。
下面我們進行分析,按照包名分類展示,找到我們的LeakActivity,可以在右邊看到有幾個LeakActivity實例。在下面我們可以看到LeakActivity的引用樹。在藍色的部分就要注意了,可以看大我們的CommonUtils通過mContext直接引用了LeakActivity,藍色高亮部分一般代表內(nèi)存泄漏。我們也可以通過點擊右邊的Analyzer Tasks右上角的綠色三角形來進行自動分析。
其實我們可以直接看Activity的數(shù)量(2)就可以推測出來是否有內(nèi)存泄漏了。

延伸
我們多旋轉幾次,內(nèi)存里面是會有多個Activity實例的,但是一旦內(nèi)存比較緊張的時候,只會保留第0個和最后創(chuàng)建的那個。
中間的幾個Activity會被回收,因為并沒有被CommonUtil引用。
CommonUtil與Activity的生命周期不一致,不一致的時候有可能造成泄漏。
實際項目比較復雜,需要引用樹一個一個分析,引用是否合理,然后看代碼。工具只是提供線索。
MAT的使用
MAT是Memory Analyzer Tools的簡稱,是專門用于Java內(nèi)存分析的工具,是Eclipse的插件,也可以單獨使用,可以從官網(wǎng)下載。
首先我們要把AS生成的hprof文件轉為為標準的文件,當然,如果是ADT的話,可以直接拿來用。如下圖示:

然后在MAT中打開:我們一般選擇內(nèi)存泄漏分析報告。

這里可以看到一些有問題的報告,例如Resources類被系統(tǒng)的類加載器加載進來了,并且占用了很大內(nèi)存空間。但是一般這個界面沒有什么卵用。

下面這個界面比較重要:
Histogram:Lists number of instances per class
內(nèi)存中對象個數(shù)和大小,最主要看這個,其余都是看大概。
Dominator Tree: List the biggest objects and what they keep alive.
列舉大對象,同時它們依賴什么,是否還存在。

這個界面可以看到跟AS差不多的東西,這里不詳細介紹。

我們先回顧一下GC回收的算法:
以”GC Roots”的對象作為起始點向下搜索,搜索形成的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(即不可達的),則該對象被判定為可以被回收的對象,反之不能被回收。也寫出了內(nèi)存泄露的原因:對象無用了,但仍然可達(未釋放),垃圾回收器無法回收。那些相互引用而不能被回收的就是內(nèi)存泄漏的對象。
我們搜索LeakActivity,然后點擊右鍵,選擇Merge Shortest Path to GC Roots,選擇exclude all phantom/weak/soft etc.references, 意思是查看排除虛引用/弱引用/軟引用等的引用鏈 (這些引用最終都能夠被GC干掉,所以排除)。最后可以看到還有兩個對象引用了LeakActivity,其中輸入法是系統(tǒng)的BUG。
這里也可以看到該對象被誰引用了,或者引用了誰,incmming outcomming

快照的對比
當我們懷疑執(zhí)行了某一個動作導致APP卡慢的時候,可以在這個動作之前先拍一個快照,執(zhí)行動作之后再拍一個,然后利用MAT進行對比。
在Navigation History界面中,選中 History add to compare basket,點擊右上角的紅色嘆號執(zhí)行對比分析,通過對比分析,看看執(zhí)行動作前后對象的內(nèi)存分配、數(shù)量差異也可以發(fā)現(xiàn)問題。

最后,其實我們也可以直接通過Android Monitor的內(nèi)存報告,看View以及Activity的數(shù)量來進行預判:
當app退出的時候,這個進程里面所有的對象應該就都被回收了,尤其是很容易被泄露的(View,Activity)是否還內(nèi)存當中。
可以讓app退出以后,查看系統(tǒng)該進程里面的所有的View、Activity對象是否為0.
工具:使用AndroidStudio--AndroidMonitor--System Information--Memory Usage查看Objects里面的views和Activity的數(shù)量是否為0.

總結
往往做項目的時候情況非常復雜,或者項目做得差不多了想起來要性能優(yōu)化檢查下內(nèi)存泄露。
如何找到項目中存在的內(nèi)存泄露的這些地方呢?
1.確定是否存在內(nèi)存泄露
1)Android Monitors的內(nèi)存分析
最直觀的看內(nèi)存增長情況,知道該動作是否發(fā)生內(nèi)存泄露。(內(nèi)存抖動)
動作發(fā)生之前:GC完后內(nèi)存1.4M; 動作發(fā)生之后:GC完后內(nèi)存1.6M
2)使用MAT內(nèi)存分析工具
MAT分析heap的總內(nèi)存占用大小來初步判斷是否存在泄露
Heap視圖中有一個Type叫做data object,即數(shù)據(jù)對象,也就是我們的程序中大量存在的類類型的對象。
在data object一行中有一列是“Total Size”,其值就是當前進程中所有Java數(shù)據(jù)對象的內(nèi)存總量,
一般情況下,這個值的大小決定了是否會有內(nèi)存泄漏。
我們反復執(zhí)行某一個操作并同時執(zhí)行GC排除可以回收掉的內(nèi)存,注意觀察data object的Total Size值,
正常情況下Total Size值都會穩(wěn)定在一個有限的范圍內(nèi),也就是說由于程序中的的代碼良好,沒有造成對象不被垃圾回收的情況。
反之如果代碼中存在沒有釋放對象引用的情況,隨著操作次數(shù)的增多Total Size的值會越來越大。
那么這里就已經(jīng)初步判斷這個操作導致了內(nèi)存泄露的情況。
2.先找懷疑對象(哪些對象屬于泄露的)
MAT對比操作前后的hprof來定位內(nèi)存泄露是泄露了什么數(shù)據(jù)對象。(這樣做可以排除一些對象,不用后面去查看所有被引用的對象是否是嫌疑)
快速定位到操作前后所持有的對象哪些是增加了(GC后還是比之前多出來的對象就可能是泄露對象嫌疑犯)
技巧:Histogram中還可以對對象進行Group,比如選擇Group By Package更方便查看自己Package中的對象信息。
3.MAT分析hprof來定位內(nèi)存泄露的原因所在。(哪個對象持有了上面懷疑出來的發(fā)生泄露的對象)
1)Dump出內(nèi)存泄露“當時”的內(nèi)存鏡像hprof,分析懷疑泄露的類;
2)把上面2得出的這些嫌疑犯一個一個排查個遍。步驟:
(1)進入Histogram,過濾出某一個嫌疑對象類
(2)然后分析持有此類對象引用的外部對象(在該類上面點擊右鍵List Objects--->with incoming references)
(3)再過濾掉一些弱引用、軟引用、虛引用,因為它們遲早可以被GC干掉不屬于內(nèi)存泄露
(在類上面點擊右鍵Merge Shortest Paths to GC Roots--->exclude all phantom/weak/soft etc.references)
(4)逐個分析每個對象的GC路徑是否正常
此時就要進入代碼分析此時這個對象的引用持有是否合理,這就要考經(jīng)驗和體力了!
(比如上課的例子中:旋轉屏幕后MainActivity有兩個,肯定MainActivity發(fā)生泄露了,
那誰導致他泄露的呢?原來是我們的CommonUtils類持有了旋轉之前的那個MainActivity他,
那是否合理?結合邏輯判斷當然不合理,由此找到內(nèi)存泄露根源是CommonUtils類持有了該MainActivity實例造成的。
怎么解決?罪魁禍首找到了,怎么解決應該不難了,不同情況解決辦法不一樣,要靠你的智慧了。)