Android開發中時常會遇到內存泄漏的問題,而Android系統對單個App又有一定的內存限制,此值可以通過一下方式獲取:
ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = am.getMemoryClass();
上述代碼中momeryClass的值可以當做每個App的內存限制。這個值根據不同的設備廠商都是不一樣的,比如我的模擬器的值是32M,如果在我的模擬器上運行的一個App,分配的內存空間超過32M,則會報OOM(內存溢出)!而內存泄漏也是一個導致內存溢出的隱患,因此必須掌握解決內存溢出的方法。
本章主要講解使用Android Studio查看是否有內存泄漏問題,然后使用MAT(Memory Analyzer Tool)來分析並解決內存泄漏問題。
Android Studio分析是否有內存泄漏
打開Android Studio中的Android Monitor中的Memory面板,可以看到有一個實時變化的堆內存曲線圖,如下圖所示
上圖中重點列出了3部分內容:
- 被測試的終端設置,如圖所示我是在模擬器Nexus_S上做測試
- 被測試的進程,點擊可選擇其他Application或者進程
- 當前被測試的進程中內存分配情況
- Allocated代表已分配的空間
- Free代表可用剩余空間
- Allocated + Free不能超App內存限制(32M)
-
內存分析的工具欄,從上向下一共4個按鈕,依次是:
終止檢測的開關,沒什么實質性的作用
就是手動調用GC,我們在抓內存前,一定要手動點擊 Initiate GC按鈕手動觸發GC,這樣抓到的內存使用情況就是不包括Unreachable對象的(Unreachable指的是可以被垃圾回收器回收的對象,但是由於沒有GC發生,所以沒有釋放,這時抓的內存使用中的Unreachable就是這些對象)
獲取hprof文件(hprof文件是我們使用MAT工具分析內存時使用的文件),但這里直接產生的文件MAT還不能直接使用,需用轉換成標准的hprof文件。可以使用AndroidStudio轉換或者用hprof-conv命令轉化,網上可以查到
開始分配追蹤,第一次點擊可以指定追蹤內存的開始位置,第二次點擊可以結束追蹤的位置。這樣我們截取了一段要分析的內存,等待幾秒鍾AndroidStudio會給我們打開一個Allocation視圖(感覺和MAT工具差不多,不過MAT工具更加強大,我們也可以獲取hprof文件,使用MAT來分析)
寫一段代碼動態演示一下:
xml布局文件如下,定義一個Button,並設置onClick屬性
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:text="測試Memory Monitor" android:onClick="click" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
通過上面代碼,可以預見,每次點擊Button時,都會動態生成10000個ImageView並添加到List中保存起來,Memory的效果圖如下:
可以看到,剛開始系統分配了2M左右的內存,當點擊一次Button之后,內存增加到8M,再次點擊內存增加到24M左右。
上述情況下,當我們按下返回退出Activity時,然后點擊Init GC按鈕執行垃圾回收操作,進程中的內存會重新回到2M,如下圖:
這種情況下,代碼是安全穩定的代碼,但是如果Activity中有內存泄漏會是何種情況呢,接下來我們先把之前的代碼修改一下,認為構造一個內存泄漏的場景,如下代碼所示:
public class MainActivity extends AppCompatActivity { private List<ImageView> list = new ArrayList<>(); static MemoryLeak memoryLeak; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (memoryLeak == null) { memoryLeak = new MemoryLeak(); } } public void click(View view) { for (int i = 0; i < 10000; i++) { ImageView imageView = new ImageView(this); list.add(imageView); } } class MemoryLeak { void doSomeThing() { System.out.println("Wheee!!!"); } }
可以看到,在MainActivity中,添加了一個非靜態內存類MemoryLeak,然后聲明了一個靜態MemoryLeak引用。
運行上述代碼,然后再次執行點擊Button的操作,可以看到內存同樣會上升到8M左右,再次點擊上升到16M左右,但是此時按下返回按鈕並執行垃圾回收操作之后,Allocated + Free的總空間並沒有重新回到2M左右,而是一直徘徊於8M左右 說明存在內存泄漏!!! 但是為什么會是8M呢??
Android Studio生成內存字節文件
剛才在介紹Studio的Memory面板時,有提到一個工具欄Dump Java Heap,通過點擊此按鈕就可以導出一個hprof文件,此過程會比較慢,需要耐心等待,當下圖中心的圓圈停止轉動之后hprof文件也就導出成功
導出完成后將自動打開這個文件,如下圖所示:
點擊Analyzer Tasks右邊的綠色運行箭頭,Android Studio會自動的根據此hprof文件分析有哪些類是有內存泄漏的,如下圖所示:
確實有一個MainActivity存在內存泄漏的情況,但是跟我之前預想的有一點出入,本來以為向網上很多人說的那樣,每次打開一個MainActivity時都會造成內存泄漏,但是現在事實就擺在眼前。仔細想了一下也恍然大悟了,MemoryLeak在第一個MainActivity中被聲明是static靜態的,當第二個被打開的MainActivity並不會再重新初始化MemoryLeak對象了,因此static MemoryLeak對象在內存中只是持有了第一個MainActivity的對象的引用,因此當我們調用多次GC操作之后,實際上只有第一個MainActivity不會被GC回收掉!!
如果再將Activity的代碼修改一下
package material.danny_jiang.com.adbmemoryanalyze; import android.app.ActivityManager; import android.content.Context; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.ImageView; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private List<ImageView> list = new ArrayList<>(); //private static MemoryLeak memoryLeak; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //memoryLeak = new MemoryLeak(); } public void click(View view) { for (int i = 0; i < 10000; i++) { ImageView imageView = new ImageView(this); list.add(imageView); } new Thread() { @Override public void run() { super.run(); while (true) { try { System.out.println("Thread running!!"); Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } class MemoryLeak { void doSomeThing() { System.out.println("Wheee!!!"); } } }
可以看到,我講造成內存泄漏的場景由內部類改成了內部線程類,並且在線程中無限循環打印log。
再次執行進入MainActivity–返回鍵–進入MainActivity–返回鍵的操作
然后再生成hprof文件並打開,並執行Analyzer Tasks,可以看到如下圖片的信息:
上圖可以看出打開的每一個MainActivity都會造成內存泄漏。 擦嘞!!為什么這會兒又是這種情況呢???這個問題就牽涉到Java中線程的問題了—Java中的Thread有一個特點就是她們都是直接被GC Root所引用,也就是說Dalvik虛擬機對所有被激活狀態的線程都是持有強引用,導致GC永遠都無法回收掉這些線程對象,除非線程被手動停止並置為null或者用戶直接kill進程操作。看到這相信你應該也是心中有答案了吧 : 我在每一個MainActivity中都創建了一個線程,此線程會持有MainActivity的引用,即使退出Activity當前線程因為是直接被GC Root引用所以不會被回收掉,導致MainActivity也無法被GC回收。所以當使用線程時,一定要考慮在Activity退出時,及時將線程也停止並釋放掉
MAT內存分析工具
正常來講,根據上面我講的使用Studio來分析簡單的內存泄漏已經足夠了,但是在Studio之前有一款更加強大的內存分析工具MAT,谷歌工程師稱它更加的Powerful!!。接下來就看一下如何使用MAT來分析內存問題
1 首先在eclipse官網下載MAT工具
2 下載完MAT並安裝好之后,需要先生成hprof文件。
這兩我還是使用之前線程造成內存泄漏的案例來演示,
- 首先第一次打開MainActivity時,點擊dump heap生成一個hprof文件
- 其次進行一系列的操作, 比如點擊Button,按下返回鍵,再次進入MainActivity等等,這里我重復了4遍如上操作,然后再點擊dump heap生成hprof文件
3 點擊Studio的Captures欄,顯示剛才生成的hprof文件,如下圖所示:
這兩個文件我們需要使用MAT去打開並對比分析,但是MAT不能直接打開這兩個文件,需要將它轉換成MAT能夠識別的文件,Captures欄中,右鍵點擊每一個hprof文件,然后選擇Export to standard .hprof並保存到電腦目錄中,如下圖:
4 使用MAT打開轉換后的hprof文件,顯示如下圖
可以看到有兩個dump的面板,其中每一個都顯示了一個內存的餅狀圖。其中用的最多的功能是左下角的Histogram, 點擊 Actions下的 Histogram項將得到 Histogram結果:
它按類名將所有的實例對象列出來,可以點擊表頭進行排序,在表的第一行可以輸入正則表達式來匹配結果 :
在Histogram中,可以右鍵某一想查看的對象,然后選中List Objects來查看此對象的所有實例,如下圖
選中之后,會跳出所有實例對象面板,在此面板中可以可以繼續某一特定實例在內存中的Path To GC Root(從GC開始的強引用)。在之前的案例操作中,我重復的進入MainActivity4次,並依次點擊Button運行線程,因此正常來說MainActivity應該有4個實例在內存當中,如下圖
exclude all phantom/weak/soft的意思是講所有的虛引用/軟引用/弱引用都排除掉,因為只有強引用才會造成內存泄漏!點擊之后顯示下圖信息:
可以看到,MainActivity最終都是被一個叫做MainActivity1的對象引用,而M1就是在click方法中創建的匿名內部類Thread對象。 最終我們找到了內存泄漏的根本原因 : 當Activity退出之后,Thread因為被GC Root直接引用,所以不會被GC回收掉,而Thread又持有Activity的引用導致Activity也無法被GC正常回收掉,造成了Activity的內存泄漏,大功告成!!!
如何發現內存泄漏
上面分別介紹了使用Android studio和MAT分析內存的方法。Android studio自帶的內存分析工具直觀方便,但其功能卻不如MAT強大,特別是沒有有效的搜索、排序等功能。遇到一些棘手的問題,可能還是要借助MAT來分析內存。
上面的例子是我們人為制造了一個內存泄漏,然后有意用工具檢測他。但實際開發中,我們如何發現內存泄漏呢?我想可以首先使用studio自帶或DDMS中的heap分析工具,觀察在反復執行某個操作時(例如打開某個頁面、點擊某個按鈕、加載某個資源等等)時,內存在執行GC后能始終維持在穩定的值附近。如果內存呈線性增長的趨勢,那一定是發生了內存泄漏。此時,就要dump出內存鏡像,然后使用工具分析了。
在分析內存時,第一是可以使用工具自帶的泄漏檢查器幫助定位。另外,可以在執行操作(懷疑造成內存泄漏的操作)前后,分別dump出一份內存鏡像,然后使用MAT的Compare Basket對比兩個文件的內存情況,這樣可以幫助定位到是哪個對象發生了泄漏。然后再找到這個對象的GC Roots,這樣就可以進一步定位到具體的代碼了。
常見內存泄漏問題總結
請查看鏈接:http://blog.csdn.net/zxm317122667/article/details/52106741