1. 程式人生 > >安卓專案實戰之記憶體洩漏檢測神器LeakCanary

安卓專案實戰之記憶體洩漏檢測神器LeakCanary

為什麼會產生記憶體洩漏?

Java記憶體洩漏指的是程序中某些物件(垃圾物件)已經沒有使用價值了,但有另外一個正在使用的物件持有它的引用,從而導致它不能回收停留在堆記憶體中,這就產生了記憶體洩漏。無用的物件佔據著記憶體空間,使得實際可使用記憶體變小,形象地說法就是記憶體洩漏了。

記憶體洩露對程式產生的影響?

記憶體洩漏是造成應用程式OOM的主要原因之一。Android系統為每個應用程式分配有限的記憶體,當應用中記憶體洩漏較多時,就難免會導致應用所需要的記憶體超出系統分配限額,從而導致OOM應用Crash;

Android常見的記憶體洩露

1,單例造成:由於單例靜態特性使得單例的生命週期和應用的生命週期一樣長,如果一個物件(如Context)已經不使用了,而單例物件還持有物件的引用造成這個物件不能正常被回收;
2,非靜態內部類建立靜態例項造成

:在Acitivity記憶體建立一個非靜態內部類單例,避免每次啟動資源重新建立。但是因為非靜態內部類預設持有外部類(Activity)的引用,並且使用該類建立靜態例項。造成該例項和應用生命週期一樣長,導致靜態例項持有引用的Activity和資源不能正常回收;
3,Handler造成:子執行緒執行網路任務,使用Handler處理子執行緒傳送訊息。由於handler物件是非靜態匿名內部類的物件,持有外部類(Activity)的引用。在Handler-Message中Looper執行緒不斷輪詢處理訊息,當Activity退出還有未處理或者正在處理的訊息時,訊息佇列中的訊息持有handler物件引用,handler又持有Activity,導致Activity的記憶體和資源不能及時回收;
4,執行緒造成
:匿名內部類Runnalbe和AsyncTask物件執行非同步任務,對當前Activity隱式引用。當Activity銷燬之前,任務還沒有執行完,將導致Activity的記憶體和資源不能及時回收;
5,資源未關閉造成的記憶體洩露:對於使用了BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出,否則這些資源將不會被回收,造成記憶體洩露;

記憶體洩漏與記憶體溢位的區別是什麼?

記憶體洩漏: 垃圾物件依舊佔據記憶體,如水龍頭的洩漏,水本來是屬於水源的, 但是水龍頭沒關緊,那麼洩漏到了水池;再來看記憶體,記憶體本來應 該被回收,但是依舊在記憶體堆中;總結一下就是記憶體存在於不該存在的地方(沒用的地方)

記憶體溢位: 記憶體佔用達到最大值,當需要分配記憶體時,已經沒有記憶體可以分配了,就是溢位;依舊以水池為例, 水池的水如果滿了,那麼如果繼 續需要從水龍頭流水的話,水就會溢位。總結一下就是,記憶體的分配超出最大閥值,導致了一種異常

明白了兩者的概念,那麼兩者有什麼關係呢?

記憶體的溢位是記憶體分配達到了最大值,而記憶體洩漏是無用記憶體充斥了記憶體堆;因此記憶體洩漏是導致記憶體溢位的元凶之一,而且是很大的元凶;因為記憶體分配完後,哪怕佔用再大,也會回收,而洩漏的記憶體則不然;當清理掉無用記憶體後,記憶體溢位的閥值也會相應降低。

LeakCanary簡介

LeakCanary是Square公司開源的一個檢測記憶體的洩露的函式庫,可以方便地和你的專案進行整合,在Debug版本中監控Activity、Fragment等的記憶體洩露;
LeakCanary整合到專案中之後,在檢測到記憶體洩露時,會發送訊息到系統通知欄。點選後開啟名稱DisplayLeakActivity的頁面,並顯示洩露的跟蹤資訊,Logcat上面也會有對應的日誌輸出。同時如果跟蹤資訊不足以定位時,DisplayLeakActivity還為開發者預設儲存了最近7個dump檔案到App的目錄中,可以使用MAT等工具對dump檔案進行進一步的分析;

如何使用leakcanary檢測記憶體洩露

1,在android studio的build.gradle中引用leakcanary

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'

2,自定義Application,並在onCreate方法中執行以下程式碼:

public class QAplication extends Application{
    @Override
    public void onCreate() {
        super.onCreate();
        ... ...
        //初始化LeakCanary
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return;
        }
        LeakCanary.install(this);
    }
}

3,AndroidManifest.xml中新增許可權

<!--SDCard中建立與刪除檔案許可權--> 
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> 
<!--向SDCard寫入資料許可權--> 
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

已經結束了!通過以上配置,你就可以輕鬆使用LeakCanary檢測記憶體洩漏了,當然這種簡單的配置僅限於在Activity中進行檢測,當然還存在其他類的記憶體洩漏,如Fragment等,這時我們就需要使用RefWatcher來進行監控了,具體見下面在非Activity的其它類中使用RefWatcher來進行記憶體洩漏的監控的講解。

單例記憶體洩露模擬

1.單例如下:

public class TestManager {
    //單例靜態特性使得單例的生命週期和應用的生命週期一樣長
    private static TestManager instance;
    private Context context;
 
    /**
     * 傳入的Context的生命週期很重要:
     *   如果傳入的是Application的Context,則生命週期和單例生命週期一樣長;
     *   如果傳入的是Activity的Context,由於該Context和Activity的生命週期一樣長,當Activity退出的時候它的記憶體不會被回收,因為單例物件持有它的引用;
     */
    private TestManager(Context context) {
        this.context = context;
    }
 
    public static TestManager getInstance(Context context) {
        if (instance == null) {
            instance = new TestManager(context);
        }
        return instance;
    }
}

2,在activity中建立該單例物件:

public class LeakCanaryActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leakcanary);
        //獲取單例物件,退出Activity即可模擬出記憶體洩露
        TestManager testManager = TestManager.getInstance(this);
    }
}

執行App到LeakCanaryActivit頁面並退出,在檢測到記憶體洩露的時候,會發送訊息到系統通知欄;
在這裡插入圖片描述 在這裡插入圖片描述
點選通知訊息,開啟名為DisplayLeakActivity的頁面,並顯示洩漏的跟蹤資訊;
在這裡插入圖片描述

在非Activity的其它類中使用RefWatcher來進行記憶體洩漏的監控

第二節的例子程式碼只能夠檢測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; 
     } 
     
}

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); 
     } 
 }

MainActivity存在記憶體洩漏,原因就是非靜態內部類LeakThread持有外部類MainActivity的引用,LeakThread中做了耗時操作,導致MainActivity無法被釋放。
在註釋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,this$0的含義就是內部類自動保留的一個指向所在外部類的引用,而這個外部類就是詳情最後一行所給出的MainActiviy的例項,這將會導致MainActivity無法被GC,從而產生記憶體洩漏。