天天看點

Android leakcanary記憶體洩漏檢測和一般的解決方案

檢測記憶體洩漏工具對比: MAT:Java堆記憶體分析工具;和eclipse很像; YourKit:第三方收費軟體,檢測java c#程式性能; LeakCanary:能儲存記憶體鏡像檔案; LeakCanary和MAT的差別:     a.使用簡單;     b.顯示效果友善; 本文主要介紹 leakcanary:

Android leakcanary記憶體洩漏檢測和一般的解決方案
Android leakcanary記憶體洩漏檢測和一般的解決方案

使用: github位址: 點選打開連結

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4'
 }      

在application中:

LeakCanary.install(this);      

1、單例造成的記憶體洩漏 Android的單例模式非常受開發者的喜愛,不過使用的不恰當的話也會造成記憶體洩漏。 因為單例的靜态特性使得單例的生命周期和應用的生命周期一樣長, 這就說明了如果一個對象已經不需要使用了,而單例對象還持有該對象的引用,那麼這個對象将不能被正常回收,這就導緻了記憶體洩漏。 如下這個典例:

public class AppManager {  
    private static AppManager instance;  
    private Context context;  
    private AppManager(Context context) {  
        this.context = context;  
    }  
    public static AppManager getInstance(Context context) {  
        if (instance != null) {  
            instance = new AppManager(context);  
        }  
        return instance;  
    }  
} 
           

這是一個普通的單例模式,當建立這個單例的時候, 由于需要傳入一個Context,是以這個Context的生命周期的長短至關重要: 1)、傳入的是Application的Context:這将沒有任何問題,因為單例的生命周期和Application的一樣長; 2)、傳入的是Activity的Context:當這個Context所對應的Activity退出時,由于該Context和Activity的生命周期一樣長(Activity間接繼承于Context),是以目前Activity退出時它的記憶體并不會被回收,因為單例對象持有該Activity的引用。 是以正确的單例應該修改為下面這種方式:

public class AppManager {  
    private static AppManager instance;  
    private Context context;  
    private AppManager(Context context) {  
        this.context = context.getApplicationContext();  
    }  
    public static AppManager getInstance(Context context) {  
        if (instance != null) {  
            instance = new AppManager(context);  
        }  
        return instance;  
    }  
}
           

不管傳入什麼Context最終将使用Application的Context,而單例的生命周期和應用的一樣長,這樣就防止了記憶體洩漏。 2、Handler造成的記憶體洩漏 Handler的使用造成的記憶體洩漏問題應該說最為常見了, 平時在處理網絡任務或者封裝一些請求回調等api都應該會借助Handler來處理, 對于Handler的使用代碼編寫一不規範即有可能造成記憶體洩漏,如下示例:

Handler mHandler = new Handler() {  
    @Override  
    public void handleMessage(Message msg) {  
        mImageView.setImageBitmap(mBitmap);  
    }  
}
           

上面是一段簡單的Handler的使用。當使用内部類(包括匿名類) 來建立Handler的時候,Handler對象會隐式地持有一個外部類對象 (通常是一個Activity)的引用(不然你怎麼可能通過Handler來操作Activity中的View?)。 而Handler通常會伴随着一個耗時的背景線程(例如從網絡拉取圖檔)一起出現,這個背景線程在任務執行完畢(例如圖檔下載下傳完畢)之後,通過消息機制通知Handler,然後Handler把圖檔更新到界面。然而,如果使用者在網絡請求過程中關閉了Activity,正常情況下,Activity不再被使用,它就有可能在GC檢查時被回收掉,但由于這時線程尚未執行完,而該線程持有Handler的引用(不然它怎麼發消息給Handler?),這個Handler又持有Activity的引用,就導緻該Activity無法被回收(即記憶體洩露),直到網絡請求結束(例如圖檔下載下傳完畢)。另外,如果你執行了Handler的postDelayed()方法: //要做的事情,這裡再次調用此Runnable對象,以實作每兩秒實作一次的定時器操作 handler.postDelayed(this, 2000); 該方法會将你的Handler裝入一個Message,并把這條Message推到MessageQueue中,那麼在你設定的delay到達之前,會有一條MessageQueue -> Message -> Handler -> Activity的鍊,導緻你的Activity被持有引用而無法被回收。 這種建立Handler的方式會造成記憶體洩漏,由于mHandler是Handler的非靜态匿名内部類的執行個體,是以它持有外部類Activity的引用,我們知道消息隊列是在Looper中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler執行個體的引用,mHandler又持有Activity的引用,是以導緻該Activity的記憶體資源無法及時回收,引發記憶體洩漏。 使用Handler導緻記憶體洩露的解決方法 方法一:通過程式邏輯來進行保護。 1).在關閉Activity的時候停掉你的背景線程。線程停掉了,就相當于切斷了Handler和外部連接配接的線,Activity自然會在合适的時候被回收。 2).如果你的Handler是被delay的Message持有了引用,那麼使用相應的Handler的removeCallbacks()方法,把消息對象從消息隊列移除就行了。 方法二:将Handler聲明為靜态類。 靜态類不持有外部類的對象,是以你的Activity可以随意被回收。代碼如下:

static class MyHandler extends Handler {  
    @Override  
    public void handleMessage(Message msg) {  
        mImageView.setImageBitmap(mBitmap);  
    }  
}
           

但其實沒這麼簡單。使用了以上代碼之後,你會發現,由于Handler不再持有外部類對象的引用,導緻程式不允許你在Handler中操作Activity中的對象了。是以你需要在Handler中增加一個對Activity的弱引用(WeakReference):

static class MyHandler extends Handler {  
    WeakReference<Activity > mActivityReference;  
    MyHandler(Activity activity) {  
        mActivityReference= new WeakReference<Activity>(activity);  
    }  
    @Override  
    public void handleMessage(Message msg) {  
        final Activity activity = mActivityReference.get();  
        if (activity != null) {  
            mImageView.setImageBitmap(mBitmap);  
        }  
    }  
}
           

将代碼改為以上形式之後,就算完成了。 延伸:什麼是WeakReference? WeakReference弱引用,與強引用(即我們常說的引用)相對,它的特點是,GC在回收時會忽略掉弱引用,即就算有弱引用指向某對象,但隻要該對象沒有被強引用指向(實際上多數時候還要求沒有軟引用,但此處軟引用的概念可以忽略),該對象就會在被GC檢查到時回收掉。對于上面的代碼,使用者在關閉Activity之後,就算背景線程還沒結束,但由于僅有一條來自Handler的弱引用指向Activity,是以GC仍然會在檢查的時候把Activity回收掉。 這樣,記憶體洩露的問題就不會出現了。 4、線程造成的記憶體洩漏 對于線程造成的記憶體洩漏,也是平時比較常見的,如下這兩個示例可能每個人都這樣寫過:

//——————test1  
        new AsyncTask<Void, Void, Void>() {  
            @Override  
            protected Void doInBackground(Void... params) {  
                SystemClock.sleep(10000);  
                return null;  
            }  
        }.execute();  
//——————test2  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                SystemClock.sleep(10000);  
            }  
        }).start();
           

上面的異步任務和Runnable都是一個匿名内部類,是以它們對目前Activity都有一個隐式引用。如果Activity在銷毀之前,任務還未完成, 那麼将導緻Activity的記憶體資源無法回收,造成記憶體洩漏。正确的做法還是使用靜态内部類的方式,如下:

static class MyAsyncTask extends AsyncTask<Void, Void, Void> {  
        private WeakReference<Context> weakReference;  

        public MyAsyncTask(Context context) {  
            weakReference = new WeakReference<>(context);  
        }  

        @Override  
        protected Void doInBackground(Void... params) {  
            SystemClock.sleep(10000);  
            return null;  
        }  

        @Override  
        protected void onPostExecute(Void aVoid) {  
            super.onPostExecute(aVoid);  
            MainActivity activity = (MainActivity) weakReference.get();  
            if (activity != null) {  
                //...  
            }  
        }  
    }  
    static class MyRunnable implements Runnable{  
        @Override  
        public void run() {  
            SystemClock.sleep(10000);  
        }  
    }  
//——————  
    new Thread(new MyRunnable()).start();  
    new MyAsyncTask(this).execute(); 
           

通過上面的代碼,新線程再也不會持有一個外部Activity 的隐式引用,而且該Activity也會在配置改變後被回收。這樣就避免了Activity的記憶體資源洩漏,當然在Activity銷毀時候也應該取消相應的任務AsyncTask::cancel(),避免任務在背景執行浪費資源。 如果我們線程做的是一個無線循環更新UI的操作,如下代碼:

private static class MyThread extends Thread {  
        @Override  
        public void run() {  
          while (true) {  
            SystemClock.sleep(1000);  
          }  
        }  
      } 
           

這樣雖然避免了Activity無法銷毀導緻的記憶體洩露,但是這個線程卻發生了記憶體洩露。在Java中線程是垃圾回收機制的根源,也就是說,在運作系統中DVM虛拟機總會使硬體持有所有運作狀态的程序的引用,結果導緻處于運作狀态的線程将永遠不會被回收。是以,你必須為你的背景線程實作銷毀邏輯!下面是一種解決辦法:

private static class MyThread extends Thread {  
        private boolean mRunning = false;  

        @Override  
        public void run() {  
          mRunning = true;  
          while (mRunning) {  
            SystemClock.sleep(1000);  
          }  
        }  

        public void close() {  
          mRunning = false;  
        }  
      } 
           

在Activity退出時,可以在 onDestroy()方法中顯示調用mThread.close();以此來結束該線程,這就避免了線程的記憶體洩漏問題。 5、資源對象沒關閉造成的記憶體洩漏 資源性對象比如(Cursor,File檔案等)往往都用了一些緩沖,我們在不使用的時候,應該及時關閉它們,以便它們的緩沖及時回收記憶體。它們的緩沖不僅存在于java虛拟機内,還存在于java虛拟機外。如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體洩漏。因為有些資源性對象,比如SQLiteCursor(在析構函數finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。是以對于資源性對象在不使用的時候,應該調用它的close()函數,将其關閉掉,然後才置為null.在我們的程式退出時一定要確定我們的資源性對象已經關閉。 程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,隻有在常時間大量操作的情況下才會複現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險。 示例代碼:

Cursor cursor = getContentResolver().query(uri...);    
if (cursor.moveToNext()) {    
  ... ...      
}

修正示例代碼:  

Cursor cursor = null;    
try {    
  cursor = getContentResolver().query(uri...);    
  if (cursor != null &&cursor.moveToNext()) {    
      ... ...      
  }    
} finally {    
  if (cursor != null) {    
      try {      
          cursor.close();    
      } catch (Exception e) {    
          //ignore this     
      }    
   }    
}
           

6、Bitmap沒有回收導緻的記憶體溢出 Bitmap的不當處理極可能造成OOM,絕大多數情況都是因這個原因出現的。Bitamp位圖是Android中當之無愧的胖小子,是以在操作的時候當然是十分的小心了。由于Dalivk并不會主動的去回收,需要開發者在Bitmap不被使用的時候recycle掉。使用的過程中,及時釋放是非常重要的。同時如果需求允許,也可以去BItmap進行一定的縮放,通過BitmapFactory.Options的inSampleSize屬性進行控制。如果僅僅隻想獲得Bitmap的屬性,其實并不需要根據BItmap的像素去配置設定記憶體,隻需在解析讀取Bmp的時候使用BitmapFactory.Options的inJustDecodeBounds屬性。最後建議大家在加載網絡圖檔的時候,使用軟引用或者弱引用并進行本地緩存,推薦使用android-universal-imageloader或者xUtils等;

繼續閱讀