Android內存泄漏定位、分析、解決全方案


原文鏈接

更多教程


爲什麼會發生內存泄漏


內存空間使用完畢之後未回收, 會導致內存泄漏。有人會問:Java不是有垃圾自動回收機制麼?不幸的是,在Java中仍存在很多容易導致內存泄漏的邏輯(logical leak)。雖然垃圾回收器會幫我們幹掉大部分無用的內存空間,但是對於還保持着引用,但邏輯上已經不會再用到的對象,垃圾回收器不會回收它們。

例如

  • 忘記釋放分配的內存的。(Cursor忘記關閉等)。
  • 應用不再需要這個對象,未釋放該對象的所有引用。
  • 強引用持有的對象,垃圾回收器是無法在內存中回收這個對象。
  • 持有對象生命週期過長,導致無法回收。

Java判斷無效對象的原理

Android內存回收管理策略圖:


圖中的每個圓節點代表對象的內存資源,箭頭代表可達路徑。當圓節點與 GC Roots 存在可達路徑時,表示當前資源正被引用,虛擬機是無法對其進行回收的(如圖中的黃色節點)。反過來,如果圓節點與 GC Roots 不存在可達路徑,則意味着這塊對象的內存資源不再被程序引用,系統虛擬機可以在 GC 過程中將其回收掉。

從定義上講,Android(Java)平臺的內存泄漏是指沒有用的對象資源任與GC-Root保持可達路徑,導致系統無法進行回收。

內存泄漏帶來的危害

  • 用戶對單次的內存泄漏並沒有什麼感知,但當泄漏積累到內存都被消耗完,就會導致卡頓,崩潰。
  • 內存泄露是內存溢出OOM的重要原因之一,會導致Crash

Android中常見的可能發生內存泄漏的地方


1.在Android開發中,最容易引發的內存泄漏問題的是Context

比如ActivityContext,就包含大量的內存引用,一旦泄漏了Context,也意味泄漏它指向的所有對象。

造成Activity泄漏的常見原因:

  • Static Activities

在類中定義了靜態Activity變量,把當前運行的Activity實例賦值於這個靜態變量。
如果這個靜態變量在Activity生命週期結束後沒有清空,就導致內存泄漏。
因爲static變量是貫穿這個應用的生命週期的,所以被泄漏的Activity就會一直存在於應用的進程中,不會被垃圾回收器回收。

static Activity activity; //這種代碼要避免
  • 單例中保存Activity

在單例模式中,如果Activity經常被用到,那麼在內存中保存一個Activity實例是很實用的。但是由於單例的生命週期是應用程序的生命週期,這樣會強制延長Activity的生命週期,這是相當危險而且不必要的,無論如何都不能在單例子中保存類似Activity的對象。
舉例:

public class Singleton {
    private static Singleton instance;
    private Context mContext;
    private Singleton(Context context){
        this.mContext = context;
    }

    public static Singleton getInstance(Context context){
        if (instance == null){
            synchronized (Singleton.class){
                if (instance == null){
                    instance = new Singleton(context);
                }
            }
        }
        return instance;
    }
}

在調用Singleton的getInstance()方法時傳入了Activity。那麼當instance沒有釋放時,這個Activity會一直存在。因此造成內存泄露。
解決方法:

可以將new Singleton(context)改爲new Singleton(context.getApplicationContext())即可,這樣便和傳入的Activity沒關係了。
  • Static Views

同理,靜態的View也是不建議的

  • Inner Classes

內部類的優勢可以提高可讀性和封裝性,而且可以訪問外部類,不幸的是,導致內存泄漏的原因,就是內部類持有外部類實例的強引用。 例如在內部類中持有Activity對象

解決方法:

1.將內部類變成靜態內部類;
2.如果有強引用Activity中的屬性,則將該屬性的引用方式改爲弱引用;
3.在業務允許的情況下,當Activity執行onDestory時,結束這些耗時任務;

例如:
發生內存泄漏的代碼:

public class LeakAct extends Activity {  
     @Override
     protected void onCreate(Bundle savedInstanceState) {    
         super.onCreate(savedInstanceState);
         setContentView(R.layout.aty_leak);
         test();
     } 
     //這兒發生泄漏    
     public void test() {    
         new Thread(new Runnable() {      
             @Override
             public void run() {        
                 while (true) {          
                     try {
                         Thread.sleep(1000);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
             }
         }).start();
     }
 }

解決方法:

public class LeakAct extends Activity {  
     @Override
     protected void onCreate(Bundle savedInstanceState) {    
         super.onCreate(savedInstanceState);
         setContentView(R.layout.aty_leak);
         test();
     }  
     //加上static,變成靜態匿名內部類
     public static void test() {    
         new Thread(new Runnable() {     
             @Override
             public void run() {        
                 while (true) {          
                     try {
                         Thread.sleep(1000);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
             }
         }).start();
     }
 }
  • Anonymous Classes

匿名類也維護了外部類的引用。當你在匿名類中執行耗時任務,如果用戶退出,會導致匿名類持有的Activity實例就不會被垃圾回收器回收,直到異步任務結束。


原文鏈接

更多教程


  • Handler

handler中,Runnable內部類會持有外部類的隱式引用,被傳遞到Handler的消息隊列MessageQueue中,在Message消息沒有被處理之前,Activity實例不會被銷燬了,於是導致內存泄漏。
解決方法:

1.可以把Handler類放在單獨的類文件中,或者使用靜態內部類便可以避免泄露;
2.如果想在Handler內部去調用所在的Activity,那麼可以在handler內部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式來達到斷開Handler與Activity之間存在引用關係的目的.
3.在界面銷燬是,釋放handler資源
@Override
 protected void doOnDestroy() {        
     super.doOnDestroy();        
     if (mHandler != null) {
         mHandler.removeCallbacksAndMessages(null);
     }
     mHandler = null;
     mRenderCallback = null;
 }
  • 同樣還有其他匿名類實例,如TimerTask、Threads等,執行耗時任務持有Activity的引用,都可能導致內存泄漏。

線程產生內存泄露的主要原因在於線程生命週期的不可控。如果我們的線程是Activity的內部類,所以MyThread中保存了Activity的一個引用,當MyThread的run函數沒有結束時,MyThread是不會被銷燬的,因此它所引用的老的Activity也不會被銷燬,因此就出現了內存泄露的問題。

要解決Activity的長期持有造成的內存泄漏,可以通過以下方法:

  • 傳入Application 的 Context,因爲 Application 的生命週期就是整個應用的生命週期,所以這將沒有任何問題。
  • 如果此時傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出時,主動結束執行的任務,並釋放Activity資源。
  • 將線程的內部類,改爲靜態內部類。

因爲非靜態內部類會自動持有一個所屬類的實例,如果所屬類的實例已經結束生命週期,但內部類的方法仍在執行,就會hold其主體(引用)。也就使主體不能被釋放,亦即內存泄露。靜態類編譯後和非內部類是一樣的,有自己獨立的類名。不會悄悄引用所屬類的實例,所以就不容易泄露。

  • 如果需要引用Acitivity,使用弱引用。
  • 謹慎對context使用static關鍵字。

2.Bitmap沒調用recycle()

Bitmap對象在不使用時,我們應該先調用recycle()釋放內存,然後才設置爲null.

3.集合中對象沒清理造成的內存泄露

我們通常把一些對象的引用加入到了集合中,當我們不需要該對象時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。
解決方案:

在Activity退出之前,將集合裏的東西clear,然後置爲null,再退出程序。

4.註冊沒取消造成的內存泄露

這種Android的內存泄露比純Java的內存泄漏還要嚴重,因爲其他一些Android程序可能引用系統的Android程序的對象(比如註冊機制)。即使Android程序已經結束了,但是別的應用程序仍然還有對Android程序的某個對象的引用,泄漏的內存依然不能被垃圾回收。
解決方案:

1.使用ApplicationContext代替ActivityContext;
2.在Activity執行onDestory時,調用反註冊;

5.資源對象沒關閉造成的內存泄露

資源性對象比如(Cursor,File文件等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收內存。而不是等待GC來處理。

6.佔用內存較多的對象(圖片過大)造成內存溢出

因爲Bitmap佔用的內存實在是太多了,特別是分辨率大的圖片,如果要顯示多張那問題就更顯著了。Android分配給Bitmap的大小隻有8M.
解決方法:

  • 1.等比例縮小圖片
BitmapFactory.Options options = new BitmapFactory.Options(); 
options.inSampleSize = 2;//圖片寬高都爲原來的二分之一,即圖片爲原來的四分之一 
  • 2.對圖片採用軟引用,及時地進行recycle()操作
//軟引用
 SoftReference<Bitmap> bitmap = new SoftReference<Bitmap>(pBitmap); 
//回收操作
 if(bitmap != null) {  
       if(bitmap.get() != null && !bitmap.get().isRecycled()){ 
           bitmap.get().recycle(); 
           bitmap = null;  
       } 
} 

7.WebView內存泄露(影響較大)

解決方案:

用新的進程起含有WebView的Activity,並且在該Activity 的onDestory() 最後加上 System.exit(0); 殺死當前進程。

檢測內存泄漏的方法


  • 1.使用 靜態代碼分析工具-Lint 檢查內存泄漏

Lint 是 Android Studio 自帶的工具,使用姿勢很簡單 Analyze -> Inspect Code 然後選擇想要掃面的區域即可

對可能引起泄漏的編碼,Lint 都會進行溫馨提示:

  • 2.LeakCanary 工具

Square 公司出品的內存分析工具,官方地址如下:https://github.com/square/lea...
LeakCanary 需要在項目代碼中集成,不過代碼也非常簡單,如下的官方示例:

在你的 build.gradle:

dependencies {
 debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
 releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'
 // Optional, if you use support library fragments:
 debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3'
}

在 Application 類:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

當內存泄漏發生時,LeakCanary 會彈窗提示並生成對應的堆存儲信息記錄

-3.Android Monitor

開Android Studio,編譯代碼,在模擬器或者真機上運行App,然後點擊

,在Android Monitor下點擊Monitor對應的Tab,進入如下界面

在Memory一欄中,可以觀察不同時間App內存的動態使用情況,點擊

可以手動觸發GC,點擊

可以進入HPROF Viewer界面,查看Java的Heap,如下圖


Reference Tree代表指向該實例的引用,可以從這裏面查看內存泄漏的原因,Shallow Size指的是該對象本身佔用內存的大小,Retained Size代表該對象被釋放後,垃圾回收器能回收的內存總和。

擴展知識


四種引用類型的介紹

  • 強引用(StrongReference):JVM 寧可拋出 OOM ,也不會讓 GC 回收具有強引用的對象;
  • 軟引用(SoftReference):只有在內存空間不足時,纔會被回的對象;
  • 弱引用(WeakReference):在 GC 時,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存;
  • 虛引用(PhantomReference):任何時候都可以被GC回收,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。可以用來作爲GC回收Object的標誌。

原文鏈接

更多教程


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