android 如何查找內存泄漏

轉載自https://blog.csdn.net/itachi85/article/details/77826112?utm_source=gold_browser_extension
https://blog.csdn.net/u012760183/article/details/52068490

對於內存泄漏,在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
  • 7

接下來 右擊一個SecondActivity

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

查看下圖的頁面
看到 this0引用了这个Activity而this” role=”presentation” style=”position: relative;”>0Activitythis0引用了這個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無法回收。

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

其中還有一些關於 集合對象沒移除,註冊的對象沒反註冊,代碼壓力的問題也可能產生內存泄漏,但是使用上述的幾種解決辦法一般都是可以解決的。

使用LeakCanary進行內存泄漏分析

如果使用MAT來分析內存問題,會有一些難度,並且效率也不是很高,對於一個內存泄漏問題,可能要進行多次排查和對比。
爲了能夠簡單迅速的發現內存泄漏,Square公司基於MAT開源了LeakCanary

2.使用LeakCanary

首先配置build.gradle:

 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.2'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.2'
 }
  • 1
  • 2
  • 3
  • 4

接下來在Application加入如下代碼。

public class LeakApplication extends Application {
    @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {//1
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

註釋1處的代碼用來進行過濾操作,如果當前的進程是用來給LeakCanary 進行堆分析的則return,否則會執行LeakCanary的install方法。這樣我們就可以使用LeakCanary了,如果檢測到某個Activity 有內存泄露,LeakCanary 就會給出提示。

3.LeakCanary應用舉例

第二節的例子代碼只能夠檢測Activity的內存泄漏,當然還存在其他類的內存泄漏,這時我們就需要使用RefWatcher來進行監控。改寫Application,如下所示。

public class LeakApplication extends Application {
    private RefWatcher refWatcher;
    @Override
    public void onCreate() {
        super.onCreate();
        refWatcher= setupLeakCanary();
    }
    private RefWatcher setupLeakCanary() {
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return RefWatcher.DISABLED;
        }
        return LeakCanary.install(this);
    }

    public static RefWatcher getRefWatcher(Context context) {
        LeakApplication leakApplication = (LeakApplication) context.getApplicationContext();
        return leakApplication.refWatcher;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

install方法會返回RefWatcher用來監控對象,LeakApplication中還要提供getRefWatcher靜態方法來返回全局RefWatcher。
最後爲了舉例,我們在一段存在內存泄漏的代碼中引入LeakCanary監控,如下所示。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LeakThread leakThread = new LeakThread();
        leakThread.start();
    }
    class LeakThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(6 * 60 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = LeakApplication.getRefWatcher(this);//1
        refWatcher.watch(this);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

MainActivity存在內存泄漏,原因就是非靜態內部類LeakThread持有外部類MainActivity的引用,LeakThread中做了耗時操作,導致MainActivity無法被釋放。關於內存泄漏可以查看Android內存優化(三)避免可控的內存泄漏這篇文章。
在註釋1處得到RefWatcher,並調用它的watch方法,watch方法的參數就是要監控的對象。當然,在這個例子中onDestroy方法是多餘的,因爲LeakCanary在調用install方法時會啓動一個ActivityRefWatcher類,它用於自動監控Activity執行onDestroy方法之後是否發生內存泄露。這裏只是爲了方便舉例,如果想要監控Fragment,在Fragment中添加如上的onDestroy方法是有用的。
運行程序,這時會在界面生成一個名爲Leaks的應用圖標。接下來不斷的切換橫豎屏,這時會閃出一個提示框,提示內容爲:“Dumping memory app will freeze.Brrrr.”。再稍等片刻,內存泄漏信息就會通過Notification展示出來,比如三星S8的通知欄如下所示。

Notification中提示了MainActivity發生了內存泄漏, 泄漏的內存爲787B。點擊Notification就可以進入內存泄漏詳細頁,除此之外也可以通過Leaks應用的列表界面進入,列表界面如下圖所示。

內存泄漏詳細頁如下圖所示。

點擊加號就可以查看具體類所在的包名稱。整個詳情就是一個引用鏈:MainActiviy的內部類LeakThread引用了LeakThread的this$0</code>,<code>this$0的含義就是內部類自動保留的一個指向所在外部類的引用,而這個外部類就是詳情最後一行所給出的MainActiviy的實例,這將會導致MainActivity無法被GC,從而產生內存泄漏。

除此之外,我們還可以將 heap dump(hprof文件)和info信息分享出去,如下圖所示。
device-2017-09-02-181759_副本.png

需要注意的是分享出去的hprof文件並不是標準的hprof文件,還需要將它轉換爲標準的hprof文件,這樣纔會被MAT識別從而進行分析,關於MAT可以查看Android內存優化(五)詳解內存分析工具MAT這篇文章。

解決方法就是將LeakThread改爲靜態內部類。

public class MainActivity extends AppCompatActivity {
  ...
    static class LeakThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(6 * 60 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

再次運行程序LeakThread就不會給出內存泄漏的提示了。

參考資料
《高性能Android應用開發》
使用LeakCanary檢測安卓中的內存泄漏(實戰)
https://github.com/square/leakcanary
LeakCanary 中文使用說明
Android 源碼系列之<十三>從源碼的角度深入理解LeakCanary的內存泄露檢測機制(中)

使用Android studio分析內存泄露

Android內存泄漏分析及調試

java對象的強引用,軟引用,弱引用和虛引用

Android內存泄漏終極解決篇(下)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章