Android 內存泄漏分析與解決方法


在分析Android內存泄漏之前,先了解一下JAVA的一些知識
1. JAVA中的對象的創建

  • 使用new指令生成對象時,堆內存將會為此開辟一份空間存放該對象
    垃圾回收器回收非存活的對象,並釋放對應的內存空間

2.Java如何判定對象存活或死亡?

  • 引用計數法
    1給對象中添加一個引用計數,假如為count
    2當引用這個對象時:count++
    3當count==0時:對象處於,也就是說沒有其它地方在引用這個對象了,對象就處於“死亡”狀態,回收對象

  • 可達性分析算法
    舉個例子:像找人一樣,A認識B,B認識C,C認識D,那么A就要吧通過這樣的關系認識D,如果能找到D,說明D對象是存活的,不能回收,如果通過所有的關系都找不到D,說明D是“死亡”的,回收D對象。
    可達性分析算法的定義:通過一系列的稱為 GC
    Roots 的對象作為起點,從這些節點開始向下搜索,搜索把走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連(就是從GC Roots 到這個對象不可達)時,則證明此對象是不可用的。如下圖,Object5,Object6,Object7就是不可達對象,是要被回收的對象
    75e26954092541a59a9979876fdfee03_image.png

問:哪些對象可以作為GC Roots對象呢?

  • 1虛擬機棧中引用的對象
  • 2方法區中類靜態屬性引用的對象
  • 3方法區中常量引用的對象
  • 4本地方法棧中JNI引用的對象

3.引用分類

  • 強引用:只要強引用還存在,垃圾回收器永遠不回收強引用的對象.如下
Object obj = new Object()  //強引用
  • 軟引用:在內存溢出異常之前,回收對象
String str=new String("123");   // 強引用  
SoftReference softRef=new SoftReference(str);     // 軟引用
  • 弱引用 : 在下一次 GC 時,無論當前內存是否足夠,都會回收被引用的對象
String str=new String("abc");      
WeakReference abcWeakRef = new WeakReference(str);  
str=null;
  • 虛引用:虛引用 : 沒用,形同虛設,唯一的用處是在對象回收時,會收到一個系統通知

注:JAVA中這4種引用的級別由高到低依次為: 強引用 > 軟引用 > 弱引用 > 虛引用

** 4.JAVA中內存分配 **

  • 靜態儲存區:編譯時就分配好,在程序整個運行期間都存在。它主要存放靜態數據和常量
  • 棧區:當方法執行時,會在棧區內存中創建方法體內部的局部變量,方法結束后自動釋放內存
  • 堆區:通常存放 new 出來的對象。由 Java 垃圾回收器回收。

上面的是JAVA的一些預備知識,下面分析Android內存泄漏相關

** 1 內存泄漏與內存溢出**

  • 內存泄漏:Memory Leak , 無用的對象應該被回收的沒有被回收
  • 內存溢出:常說的OOM,沒有足夠的內存供分配了

** 2 Android內存泄漏分類 **

  • 長期持有(Activity)Context導致的
    (1) 單例類持有Activity引用
    (2) 長生命周期引用短生命周期

  • 由非靜態內部類或者匿名內部類導致的
    (1) Handler泄漏

  • 資源使用完忘記釋放
    (1) Cursor,InputStream/OutputStream 忘記調用close

  • 使用某些系統服務不當
    (1) 在6.0系統,獲取ConnectivityManager服務,如果第一次使用的是Activity對應的Context去獲取這個服務,就會導致內存泄漏

  • 延遲的任務也可能導致內存泄漏
    (1) Handler 的消息未處理完,這時如果Handler是在Activity內存類實現的,消息引用Handler,Handler又引用了Activity,這時如果關閉Activity,就會造成內存泄漏

  • 忘記注銷監聽器或者觀察者
    (1) 比如 EventBus.unregister() 忘記調用

注:非靜態內部類和匿名內部類都會潛在的引用它們所屬的外部類,但是靜態內部類卻不會

** 3 Android內存泄漏分析工具 **

  • MAT
  • LeakCanary
  • Strictmode
  • Android Memory Monitors

推薦使用LeakCanary,•LeakCanary是一個檢測Java和Android內存泄漏的庫,集成LeakCanary之后,只需要等待內存泄漏出現就可以了無需認為進行主動檢測

** 4 LeakCanary的添加 **

  • 第一步:在build.gradle中添加依賴
    compile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
  • 第二步:在Application的onCreate()方法中添加 LeakCanary.install(this);

完成以上兩步,就添加了LeakCanary,接下來就正常開發測試就行了,如果有內存泄漏,就會在通知欄中會有相應的通知,點開看就可以了,找到對應的內存泄漏的地方,解決

下面是演示的內存泄漏的幾張圖,可以看一下:
2b1eb131874b43298dcbe15fd3cdfaf2_image.png

569db1221ca94d6f83c7a6c9f1c857c4_image.png

3932f31ca9ad483182d47073321e567a_image.png

5 Android內存泄漏的案例

  • 案例一:單例造成的內存泄漏
    典型比如context的使用不當造成內存泄漏
public class ToastUtils {
    private static String oldMsg;
    protected static Toast toast = null;
    private static long oneTime = 0;
    private static long twoTime = 0;
    private static long gapTime = 3 * 1000;//3s只顯示一次

    public static void show(Context context, String s) {
        if (context != null && !TextUtils.isEmpty(s)) {
            if (toast == null) {
                toast = Toast.makeText(context, s, Toast.LENGTH_SHORT);
                toast.show();
                oneTime = System.currentTimeMillis();
            } else {
                twoTime = System.currentTimeMillis();
                if (s.equals(oldMsg)) {
                    if (twoTime - oneTime > gapTime) {
                        toast.show();
                    }
                } else {
                    oldMsg = s;
                    toast.setText(s);
                    toast.show();
                }
            }
            oneTime = twoTime;
        }
    }
}

在Activity 中使用:

ToastUtils.show(this, "登錄成功");

上面的代碼就會出現內存泄漏,因為在activity中使用ToastUtils.show(this, "登錄成功")的時候,傳的第一個參數 this 代表當時的activity,而ToashTuils中的toast變量是一個靜態變量,
代碼如下

 protected static Toast toast = null;

創建toast對象如下代碼

toast = Toast.makeText(context, s, Toast.LENGTH_SHORT);

Toast.makeText的第一參數就是上面傳的activity,Toast類中有一個變量mContext會保存這個activity,就是強引用,但是toast又是一個靜態的變量,靜態變量的生命同期是和當前的APP的進程一樣長的,所以這時我們如果關閉這個Activity,就會導致Activity被靜態變量強引用,垃圾回收永遠不會回收這個Activity,所以就會出現內存泄漏。
我們看一下Toast.makeText的源碼
2e50f3e3cd964177ab6e20667f6df027_image.png

上面圖中,new一個Toast,把context傳給了Toast的構造方法。

0f331513d8b741fa97d02dfaca34075e_image.png

所以調用 ToastUtils.show(this, "登錄成功");就會導致 activity 被靜態的toast變量強引用了,導致內存泄漏。

解決方法

用ApplicationContext替代Activity,如下代碼

 public static void show(Context context, String s) {
	 //在這里獲取applicationContext,applicationContext的生命周期是和進程一樣長
	 //這樣就不會出現內存泄漏了
	context = context.getApplicationContext();
	
        if (context != null && !TextUtils.isEmpty(s)) {
            if (toast == null) {
                toast = Toast.makeText(context, s, Toast.LENGTH_SHORT);
                toast.show();
	  ......
	 }
}
  • 案例二 內部類或者匿名內部類造成的內存泄漏
    比如在Activity中使用Handler不當造成的內存泄漏
    如下圖
    2c7182b25b9d404caa4b0b1bb3d53113_image.png

上圖:在MainActivity中有一個匿名內部類Handler,並且有一個此類的對象 uiHandler。
這時我們如果在MainAcitity 中調用下面代碼,就會出現內存泄漏

uiHandle.sendMessageDelayed(uiHandle.obtainMessage(),60 * 1000);

uiHandler.obtainMessage獲取的msg 中有一個成員變量 target,target保存的就是uiHandle,而uiHandler又是內部類創建的對象,所以uiHandler隱式的會對當前的外部類,也就是MainActivity會有一個強引用,如下
msg -> uiHandler -> MainActivity
msg 引用了uiHandler,uiHandler引用了MainActivity,然后這個msg需要60s后才被處理完,在處理過程中,如果退出MainActivity,這時候就會導致內存泄漏,MainActivity回收不了。應該被回收的對象沒有被回收掉,就是內存泄漏。

注:handler機制不明白的可以先看下handler機制,message,handler,loop的關系

解決方法

  • 可以在MainActivity的onDestroy()方法調用下面代碼:
uiHandle.removeCallbacksAndMessages(null);
  • Handler不要用內部類,用靜態的內部類,因為靜態的內部類不會引用外部類,需要外部類的地方,用弱引用,代碼如下:
    68a1a0c678504ee3819e0745edd67000_image.png

使用弱引用的時候,需要作一下判斷是否為null。

  • 案例三:Activity context的不正確使用
    上面的兩個案例中其實也是context的使用場景不當造成的內存泄漏,這里不再舉例,我們通常使用的兩種context是 Acitivty和 Application,只需要注意對context的使用不要超過它的生命同期。部分情況下可以使用applicationContext代替activity的context,因為applicatoinContext會隨着應用程序的存在而存在,而不依賴於activity的生命周期。還有要慎重對context使用static關鍵字。

  • 案例四:一些資源使用完后沒有關閉
    如數據庫的游標 Cursor,輸入輸出流 InputStream/OutputStream沒有close

  • 案例五:注冊的監聽器沒有反注冊
    如EventBus.register,ButterKnife等沒有在activity的onDestroy中反注冊或者其它地方反注冊

  • 案例六:系統服務的泄漏
    在實際項目中發現的,在6.0系統上在activity中第一次如果用的是activity對應的context獲取ConnectivityManager服務會造成內存泄漏。
    代碼對下:

     /**
     * 判斷是否有網絡連接
     * @param context
     * @return
     */
    public static boolean isNetworkConnected(Context context) {
        if (context != null) {
            ConnectivityManager cm = (ConnectivityManager)
                    context.getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo mNetworkInfo = cm.getActiveNetworkInfo();
            if (mNetworkInfo != null) {
                return mNetworkInfo.isAvailable();
            }
        }
        return false;
    }

如果是第一次在activity中調用如下代碼,會發現內存泄漏

  • 注:是第一次在activity中調用,如果第一次是在application中調用不會出現內存泄漏,原因請參考下面這篇文章: http://www.jianshu.com/p/7d4b55f7ed9f
//注意,這個this 是代表的是當前的activity
isNetworkConnected(this)

簡單介紹下:先從Context的getSystemService方法開始,我們知道Activity是從ContextWrapper繼承而來的,ContextWrapper中持有一個mBase實例,這個實例指向一個ContextImpl對象,同時ContextImpl對象持有一個OuterContext對象,對於Activity來說,這個OuterContext就是Activity對象。所以調用getSystemService最終會調用到ContextImpl的getSystemService方法。
在6.0上,在6.0上,ConnectivityManager實現為單例,創建這個單例對象的時候,把相應的OuterContext就是Activity對象,保存到了ConnectivityManager中,就造成了一個單例對象強引用了activity對象,從而造成了內存泄漏,如果是第一次用的是application,則保存的不是activity而是application,反而不會出現內存泄漏了。

使用LeakCanary檢測 ConnectivityManager 內存泄漏圖如下:
3e97cb0dd4434abab3c431c8b8aa0e53_image.png

解決方法

使用applicationContext去獲取服務,不要使用activityContext去獲取服務

上面的就是對Android內存泄漏的一些總結,如果有不正確的或者需要補充的地方,請指出,一塊學習進步


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM