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

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

  • 使用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對象呢?app

  • 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種引用的級別由高到低依次爲: 強引用 > 軟引用 > 弱引用 > 虛引用oop

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

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

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

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

  • 內存泄漏: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內存泄漏的一些總結,若是有不正確的或者須要補充的地方,請指出,一塊學習進步

相關文章
相關標籤/搜索