先給出一個內(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實例造成的谒主。
怎么解決朝扼?罪魁禍首找到了,怎么解決應該不難了霎肯,不同情況解決辦法不一樣擎颖,要靠你的智慧了。)