LeakCanary-幫助你檢測Android所有的內存泄漏

平時我們在寫Android代碼的時候會經常遇到非常多的Out Of Memory異常,可以通過leakcanary這個第三方庫幫助我們定位出現問題的地方

LeakCanary是什么

  • LeakCanary是一個能夠幫助Android和Java開發者檢查內存泄露的第三方庫

LeakCanary的配置

  • 在你的build.gradle文件的dependencies中添加如下代碼 :
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
添加依賴
  • 新建一個MyApplication類繼承Application初始化LeakCanary
package com.android.oz.myleakactiviy;
import android.app.Application;
import com.squareup.leakcanary.LeakCanary;
/** * @author O.z Young 
* @date 16/8/29 
*/
public class MyApplication extends Application {    
    @Override    
    public void onCreate() {        
        super.onCreate();
        // 安裝LeakCanary
        LeakCanary.install(this);
    }}
}
  • 在你的AndroidManifest.xml中配置剛才寫好的MyApplication
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.android.oz.myleakactiviy">    
  <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
配置MyApplication

好了,到此為止你的App已經添加上了LeakCanary下面寫一個例子用來展示一下LeeakCanary的實力


使用例子

我們在使用多線程Handler + Thread進行交互的的時候經常會出現Out of Memory的情況,下面通過一個例子來進行分析,以及修正這個例子

  • 首先確認布局activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:paddingBottom="@dimen/activity_vertical_margin"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin"    tools:context="com.android.oz.myleakactiviy.MainActivity">
    <ImageView
        android:id="@+id/iv_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

這是一個很簡單的布局,在一個相對布局中包含一個ImageView.

  • 編寫onCreate方法
@Overrideprotected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    iv_image = (ImageView) findViewById(R.id.iv_image);
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                Log.v("Oz", "運行-->" + i + "次");
                SystemClock.sleep(2000);
                Message message = mHandler.obtainMessage();
                message.what = 0;
                mHandler.sendMessageDelayed(message, 5000);
            }
        }
    }).start();
}

onCreate方法中,定義了一個Thread,這個子線程要運行20次,每運行一次都要發送一個消息給Handler去處理,這里使用了SystemClock.sleep(time)方法,是為了模擬網絡請求的等待

  • 編寫成員變量Handler
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 1:
                iv_image.setImageResource(R.mipmap.ic_launcher);
                break;
        }
    }
};

這里就是處理剛才接受到消息的地方啦,接收到消息之后給iv_image設置一個圖片

  • 運行這個app

在app被成功安裝到手機上之后,我們看到這里除了我們自己的app還多了一個應用,另外一個應用就是LeakCanary

安裝成功

在運行的時候我們不要等待兩秒鐘的時間,一進入到程序之后就點擊返回鍵,退出程序,發現有LeakCanary的消息推送,點開以后內容如下

LeakCanary中的內容
  • 問題分析

可以很清楚的看到,這里的問題是這個Thread導致的,那么為什么會出現這個問題呢,原因就是我們在Thread中sleep了兩秒,然后在還沒有到兩秒的時間中,我們點擊了返回鍵,此時這個MainActivity就已經被銷毀了,但是在MainActivity中,這個Thread還是存在的,所以導致了最終的內存溢出問題

  • 改造Thread

由于產生問題的原因在于,MainActivity已經銷毀了,但是我們的Thread還沒有銷毀,那么我們可以自定義一個靜態的MyThread類去繼承Thread類,讓MyThread與MainActivity的關系脫離,除此以外我們在MyThread中使用MainActivity的弱引用,這樣當GC掃描到我們的Thread類的時候,發現有弱應用就會自動回收了,這樣進一步提高內存的利用率.

  • 定義一個MyThread繼承Thread并且聲明MainActivity的弱引用

重寫后的MyThread,因為將MyThread聲明成一個靜態的類,所以在里面使用mHandler的時候需要繞一下彎子,通過弱引用weakReference獲取到MainActivity里面的mHandler對象

private static class MyThread extends Thread {
    private WeakReference<MainActivity> weakReference;
    public MyThread(MainActivity mainActivity) {
        weakReference = new WeakReference<MainActivity>(mainActivity);
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            Log.v("Oz", "運行-->" + i + "次");
            SystemClock.sleep(2000);
            if (weakReference != null && weakReference.get() != null) {
                //Message message = mHandler.obtainMessage();
                //message.what = 0;
                //mHandler.sendMessageDelayed(message, 5000);
                Message message =
 weakReference.get().mHandler.obtainMessage();
                message.what = 0;
                weakReference.get().mHandler.sendMessageDelayed(message, 5000);
            }
        }
    }
}
  • 再次運行

還是跟剛才一樣,進入程序之后,不到兩秒的時間點擊返回鍵,過一會以后,LeakCanary又報錯了

第二次報錯

如果你有看過Handler和Message的源碼,Message.target實際上指的就是Handler.這里的Handler報錯的原因跟剛才Thread報錯的原因是一樣的,這里就不再重復說明了,接下來我們就要對Handler進行改造.

  • Handler進行改造

改造后的Handler

private static class MyHandler extends Handler {
    private WeakReference<MainActivity> weakReference;
    public MyHandler(MainActivity mainActivity) {
        weakReference = new WeakReference<MainActivity>(mainActivity);    }
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 1:
                if (weakReference != null && weakReference.get() != null) {
                    weakReference.get().iv_image.setImageResource(R.mipmap.ic_launcher);
                }
                break;
        }
    }
}

這個時候再去運行app已經不提示錯誤了,但是此時我們又發現了一個新的問題,當我們退出app的時候,我們MyThread卻還在一直運行,后臺的日志里也在不斷的輸出內容

后臺打印的內容

如何處理這個問題呢?

  • 后臺一直在輸出Log日志的原因分析

我們在退出app之后并沒有將MyThread的停止,所以導致了后臺一直在輸出打印語句,那么我們該如何解決這個問題呢?

  • 停止MyThread線程

首先我們需要明確,停止掉一個線程不能使用Thread.stop()方法,查看了源碼發現已經是過時了的方法,并且在注釋里面也明確說明,調用這個方法是不安全的,如果調用了這個方法會導致一個不可預知的狀態.

原因分析

其實,我們可以使用一個標識位來判斷一下是否要繼續執行MyThread里面的循環方法,如果標識位的判斷結果為false我們直接跳出循環就可以了.在MainActivity銷毀的時候將這個標識位設置成false即可.

新增標識位

/**
 * 定義的標識位,判斷MyThread中的循環是否執行
 **/
private static boolean mIsRunning = false;
/**
 * 將標識位設置成false跳出循環
 **/
public void setmIsRunning() {
    mIsRunning = false;
}

修改MyThread

private static class MyThread extends Thread {
    private WeakReference<MainActivity> weakReference;
    public MyThread(MainActivity mainActivity) {
        weakReference = new WeakReference<MainActivity>(mainActivity);
    }
    @Override
    public void run() {
        mIsRunning = true;
        for (int i = 0; i < 20; i++) {
            if (!mIsRunning) {
                break;
            }
            Log.v("Oz", "運行-->" + i + "次");
            SystemClock.sleep(2000);
            if (weakReference != null && weakReference.get() != null) {
                //Message message = mHandler.obtainMessage();
                //message.what = 0;
                //mHandler.sendMessageDelayed(message, 5000);
                Message message = weakReference.get().myHandler.obtainMessage();
                message.what = 0; 
               weakReference.get().myHandler.sendMessageDelayed(message, 5000);
            }
        }
    }
}

新增onDestory方法

@Override
protected void onDestroy() {
    super.onDestroy();
    // 將標識位設置成false
    setmIsRunning();
    // 清空message中的消息
    myHandler.removeCallbacksAndMessages(null);
}

到這里問題就已經解決啦~

最后本次Demo的源碼已經上傳到github上了,便于大家查看

MyLeakActivity

轉載請注明出處


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容