1. 程式人生 > >Android Studio +MAT 分析記憶體洩漏實戰

Android Studio +MAT 分析記憶體洩漏實戰

對於記憶體洩漏,在Android中如果不注意的話,還是很容易出現的,尤其是在Activity中,比較容易出現,下面我就說下自己是如何查詢記憶體洩露的。

首先什麼是記憶體洩漏?

記憶體洩漏就是一些已經不使用的物件還存在於記憶體之中且垃圾回收機制無法回收它們,導致它們常駐記憶體,會使記憶體消耗越來越大,最終導致程式效能變差。
其中在Android虛擬機器中採用的是根節點搜尋演算法列舉根節點判斷是否是垃圾,虛擬機器會從GC Roots開始遍歷,如果一個節點找不到一條到達GC Roots的路線,也就是沒和GC Roots 相連,那麼就證明該引用無效,可以被回收,記憶體洩漏就是存在一些不好的呼叫導致一些無用物件和GC Roots相連,無法被回收。

既然知道了什麼是記憶體洩漏,自然就知道如何去避免了,就是我們在寫程式碼的時候儘量注意產生對無用物件長時間的引用,說起來簡單,但是需要足夠的經驗才能達到,所以記憶體洩漏還是比較容易出現的,既然不容易完全避免,那麼我們就要能發現程式中出現的記憶體洩漏並修復它,
下面我就說說如何發現記憶體洩漏的吧。

查詢記憶體洩漏:

比如說下面這個程式碼:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super
.onCreate(savedInstanceState); setContentView(R.layout.activity_main); String string = new String(); } public void click(View view){ Intent intent = new Intent(); intent.setClass(getApplicationContext(),SecondActivity.class); startActivity(intent); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(8000000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(runnable).start();
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

每次跳轉到這個Activity中時都會呼叫一個執行緒,然後這個執行緒會執行runnable的run方法 由於Runnable是一個匿名內部物件 所以握有SecondActivity的引用,因此
很簡單的兩個Activity,可由MainActivity跳轉到SecondActivity中,
下面我們從MainActivity跳到SecondActivity 然後從SecondActivity返回MainActivity,連續這樣5次 ,最終返回MainActivity,按照常理來說,我們從SecondActivity返回MainActivity之後 SecondActivity就該被銷燬回收,可實際可能並不是這樣。

這時候要判斷髮沒發生記憶體溢位就要使用工具了!下面有兩種方式

1.利用MAT工具查詢

首先開啟AS中的Android Device Monitor工具 具體位置如下圖:
AS Android Device Monitor位置
開啟後會出現如下的介面
ADM介面
先選中你要檢測的應用的包名,然後點選下圖畫圈的地方,會在程式包名後標記一個圖示

接下來要做的就是操作我們的app 來回跳轉5次。
之後點選下圖的圖示 就可匯出hprof檔案進行分析了

匯出檔案如下圖所示:
hprof檔案
得到了hprof檔案 我們就可以利用MAT工具進行分析了,
開啟MAT工具
如果沒有 可以在下面網址下載
MAT工具下載地址

介面如下圖所示:

開啟我們先前匯出的hprof檔案 ,不出意外會報下面的錯誤

這是因為MAT是用來分析java程式的hprof檔案的 與Android匯出的hprof有一定的格式區別,因此我們需要把匯出的hprof檔案轉換一下,sdk中提供給我們轉換的工具 hprof-conv.exe 在下圖的位置
hprof-conv位置
接下來我們cd到這個路徑下執行這個命令轉換我們的hprof檔案即可,如下圖
轉換hprof檔案
其中 hprof-conv 命令 這樣使用
hprof-conv 原始檔 輸出檔案
比如 hprof-conv E:\aaa.hprof E:\output.hprof
就是 把aaa.hprof 轉換為output.hprof輸出 output.hprof就是我們轉換之後的檔案,圖中 mat2.hprof就是我們轉換完的檔案。

接下來 我們用MAT工具開啟轉換之後的mat2.hprof檔案即可 ,開啟後不報錯 如下圖所示:
MAT開啟hprof檔案
之後我們就可以檢視當前記憶體中存在的物件了,由於我們記憶體洩漏一般發生在Activity中,因此只需要查詢Activity即可。
點選下圖中標記的QQL圖示 輸入 select * from instanceof android.app.Activity
類似於 SQL語句 查詢 Activity相關的資訊 點選 紅色歎號執行後 如下圖所示:
QQL

接下來 我們就可以看到下面過濾到的Activity資訊了
如上圖所示, 其中記憶體中還存在 6個SecondActivity例項,但是我們是想要全部退出的,這表明出現了記憶體洩漏

其中 有 Shallow size 和 Retained Size兩個屬性

Shallow Size
物件自身佔用的記憶體大小,不包括它引用的物件。針對非陣列型別的物件,它的大小就是物件與它所有的成員變數大小的總和。
當然這裡面還會包括一些java語言特性的資料儲存單元。針對陣列型別的物件,它的大小是陣列元素物件的大小總和。
Retained Size
Retained Size=當前物件大小+當前物件可直接或間接引用到的物件的大小總和。(間接引用的含義:A->B->C, C就是間接引用)
不過,釋放的時候還要排除被GC Roots直接或間接引用的物件。他們暫時不會被被當做Garbage。

  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

接下來 右擊一個SecondActivity

選擇 with all references
開啟如下圖所示的頁面

檢視下圖的頁面
看到 this0Activitythis0引用了這個Activity而this0是表示 內部類的意思,也就是一個內部類引用了Activity 而 this$0又被 target引用 target是一個執行緒,原因找到了,記憶體洩漏的原因 就是 Activity被 內部類引用 而內部類又被執行緒使用 因此無法釋放,我們轉到這個類的程式碼處檢視

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(8000000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(runnable).start();
    }
}
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

確實 在 SecondActivity中 存在Runnable 內部類物件,然後又被執行緒 使用,而執行緒要執行8000秒 因此 SecondActivity物件被引用 無法釋放,導致了記憶體溢位。
要解決這種的記憶體溢位,要及時在Activity退出時結束執行緒(不過不大好結束。。),或者良好的控制執行緒執行的時間即可。

這樣我們就找出了這個程式中的記憶體溢位。

2.直接利用Android Studio的 Monitor Memory 查詢記憶體溢位
還是利用上面那個程式,我就簡單點說了。

首先 在手機上執行程式,開啟AS的 Minotor 介面 檢視Memory 影象

點選 小卡車圖示(圖中1位置圖示) 可以觸發一次 GC

點選 圖中2位置圖示可以檢視hprof檔案

左邊是 記憶體中的物件,在裡面找 Activity 看存不存在我們希望已經回收的Activity 如果 出現我們期望已經回收的Activity,單擊 就會在右邊顯示它的總的個數,點選右邊的某個,可以顯示 它的GC Roots的樹關係圖 ,檢視關係圖就可以找出發生記憶體洩漏的位置(類似於第一種方式)

這樣就完成了記憶體洩漏的查詢。

其中記憶體洩漏產生的原因在Android中大致分為以下幾種:

1.static變數引起的記憶體洩漏
因為static變數的生命週期是在類載入時開始 類解除安裝時結束,也就是說static變數是在程式程序死亡時才釋放,如果在static變數中 引用了Activity 那麼 這個Activity由於被引用,便會隨static變數的生命週期一樣,一直無法被釋放,造成記憶體洩漏。

解決辦法:
在Activity被靜態變數引用時,使用 getApplicationContext 因為Application生命週期從程式開始到結束,和static變數的一樣。

2.執行緒造成的記憶體洩漏
類似於上述例子中的情況,執行緒執行時間很長,及時Activity跳出還會執行,因為執行緒或者Runnable是Acticvity內部類,因此握有Activity的例項(因為建立內部類必須依靠外部類),因此造成Activity無法釋放。
AsyncTask 有執行緒池,問題更嚴重

解決辦法:
1.合理安排執行緒執行的時間,控制執行緒在Activity結束前結束。
2.將內部類改為靜態內部類,並使用弱引用WeakReference來儲存Activity例項 因為弱引用 只要GC發現了 就會回收它 ,因此可儘快回收

3.BitMap佔用過多記憶體
bitmap的解析需要佔用記憶體,但是記憶體只提供8M的空間給BitMap,如果圖片過多,並且沒有及時 recycle bitmap 那麼就會造成記憶體溢位。

解決辦法:
及時recycle 壓縮圖片之後載入圖片

4.資源未被及時關閉造成的記憶體洩漏
比如一些Cursor 沒有及時close 會儲存有Activity的引用,導致記憶體洩漏

解決辦法:
在onDestory方法中及時 close即可

5.Handler的使用造成的記憶體洩漏
由於在Handler的使用中,handler會發送message物件到 MessageQueue中 然後 Looper會輪詢MessageQueue 然後取出Message執行,但是如果一個Message長時間沒被取出執行,那麼由於 Message中有 Handler的引用,而 Handler 一般來說也是內部類物件,Message引用 Handler ,Handler引用 Activity 這樣 使得 Activity無法回收。

解決辦法:
依舊使用 靜態內部類+弱引用的方式 可解決

其中還有一些關於 集合物件沒移除,註冊的物件沒反註冊,程式碼壓力的問題也可能產生記憶體洩漏,但是使用上述的幾種解決辦法一般都是可以解決的。

參考資料:
使用Android studio分析記憶體洩露

Android記憶體洩漏分析及除錯

java物件的強引用,軟引用,弱引用和虛引用

Android記憶體洩漏終極解決篇(下)

				<script>
					(function(){
						function setArticleH(btnReadmore,posi){
							var winH = $(window).height();
							var articleBox = $("div.article_content");
							var artH = articleBox.height();
							if(artH > winH*posi){
								articleBox.css({
									'height':winH*posi+'px',
									'overflow':'hidden'
								})
								btnReadmore.click(function(){
									articleBox.removeAttr("style");
									$(this).parent().remove();
								})
							}else{
								btnReadmore.parent().remove();
							}
						}
						var btnReadmore = $("#btn-readmore");
						if(btnReadmore.length>0){
							if(currentUserName){
								setArticleH(btnReadmore,3);
							}else{
								setArticleH(btnReadmore,1.2);
							}
						}
					})()
				</script>
				</article>