LeakCanary是Square公司開源的一個檢測內存的泄露的函數庫,可以方便地和你的項目進行集成,在Debug版本中監控Activity、Fragment等的內存泄露;
LeakCanary集成到項目中之後,在檢測到內存泄露時,會發送消息到系統通知欄。點擊後打開名稱DisplayLeakActivity的頁面,並顯示泄露的跟蹤信息,Logcat上面也會有對應的日誌輸出。同時如果跟蹤信息不足以定位時,DisplayLeakActivity還爲開發者默認保存了最近7個dump文件到App的目錄中,可以使用MAT等工具對dump文件進行進一步的分析;
二、內存泄漏簡介
在瞭解了LeakCanary的接入方式後,我們肯定着急想見識見識LeakCanary的威力。在跟大家演示LeakCanary檢測和處理構成之前,大家應該明應該對內存泄露有基本的瞭解和認識;
1.爲什麼會產生內存泄漏?
當一個對象不需要使用本該回收時,有另外一個正在使用的對象持有它的引用,從而導致它不能回收停留在堆內存中,這就產生了內存泄漏;
2.內存泄露對程序產生的影響?
內存泄漏是造成應用程序OOM的主要原因之一。Android系統爲每個應用程序分配有限的內存,當應用中內存泄漏較多時,就難免會導致應用所需要的內存超出系統分配限額,從而導致OOM應用Crash;
3.Android常見的內存泄露?
相信內存泄露對大家都早有耳聞,但是它不像一些Java異常情況,會立即造成程序的Crash,卻有讓大家比較“陌生”。下面我們就列舉出日常開發中常見的內存泄露類型,讓大家對內存泄露的認識不僅僅停留在“有所耳聞 ”的層面;
單例造成:由於單例靜態特性使得單例的生命週期和應用的生命週期一樣長,如果一個對象(如Context)已經不使用了,而單例對象還持有對象的引用造成這個對象不能正常被回收;
非靜態內部類創建靜態實例造成:在Acitivity內存創建一個非靜態內部類單例,避免每次啓動資源重新創建。但是因爲非靜態內部類默認持有外部類(Activity)的引用,並且使用該類創建靜態實例。造成該實例和應用生命週期一樣長,導致靜態實例持有引用的Activity和資源不能正常回收;
Handler造成:子線程執行網絡任務,使用Handler處理子線程發送消息。由於handler對象是非靜態匿名內部類的對象,持有外部類(Activity)的引用。在Handler-Message中Looper線程不斷輪詢處理消息,當Activity退出還有未處理或者正在處理的消息時,消息隊列中的消息持有handler對象引用,handler又持有Activity,導致Activity的內存和資源不能及時回收;
線程造成:匿名內部類Runnalbe和AsyncTask對象執行異步任務,對當前Activity隱式引用。當Activity銷燬之前,任務還沒有執行完,將導致Activity的內存和資源不能及時回收;
資源未關閉造成的內存泄露:對於使用了BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄露;
三、LeakCanary接入
下面我們還是以QProject項目進行演示如何在項目中接入LeakCanary,項目目錄如下:
1.添加LeakCanary依賴
在主項目main模塊的build.gradle文件中添加LeakCanary相關依賴;
/main/build.gradle文件
apply plugin: 'com.android.application'
android {
... ...
}
dependencies {
... ...
//添加leakcanary相關的依賴
//在release和test版本中,使用的是LeakCanary的no-op版本,也就是沒有實際代碼和操作的Wrapper版本,只包含LeakCanary和RefWatcher類的空實現,這樣不會對生成的APK包體積和應用性能造成影響
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
... ...
compile project(':test')
}
2.初始化LeakCanary在主項目main模塊的QApplication的onCreate()方法中初始化LeakCanary;
/main/src/main/java/com/qproject/main/QApplication.java文件
public class QAplication extends Application{
@Override
public void onCreate() {
super.onCreate();
... ...
//初始化LeakCanary
if (LeakCanary.isInAnalyzerProcess(this)) {
return;
}
LeakCanary.install(this);
}
}
OK,到這裏我們就完成了一個項目的LeankCanary的簡單接入;提示1:集成LeakCanary後,構建和安裝apk的時候報錯如下:
Error:Error converting bytecode to dex:
Cause: com.android.dex.DexException: Multiple dex files define Lcom/squareup/leakcanary/LeakCanary;
Error:Execution failed for task ':main:transformClassesWithDexForDebug'.
> com.android.build.api.transform.TransformException: com.android.ide.common.process.ProcessException: java.util.concurrent.ExecutionException: java.lang.UnsupportedOperationException
處理1:添加依賴debugCompile 'com.squareup.haha:haha:2.0.3',修改依賴的版本爲1.4-beta2;
四、LeakCanary檢測
通過對一些常見的內存泄露的學習,我們已經對內存泄露有所見聞了。那麼下面我們通過LeakCanary工具,讓大家感受下它在你的日常開發中的真實存在。我們以常見內存—單例造成的內存泄露爲例進行實踐;
1.單例內存泄露模擬
test/src/main/com/qproject/test/TestManager.java
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;
}
}
test/src/main/com/qproject/test/leakcanary/LeakCanaryActivity.javapublic class LeakCanaryActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leakcanary);
//獲取單例對象,退出Activity即可模擬出內存泄露
TestManager testManager = TestManager.getInstance(this);
}
}
2.檢測消息通知運行App到LeakCanaryActivit頁面並退出,在檢測到內存泄露的時候,會發送消息到系統通知欄;
3.查看檢測詳情
點擊通知消息,打開名爲DisplayLeakActivity的頁面,並顯示泄漏的跟蹤信息;
4.查看LogCat日誌
除了以上泄漏信息的顯示,Logcat上面也會有對應的日誌輸出;
//內存泄露對象com.qproject.test.leakcanary.LeakCanaryActivity
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * com.qproject.test.leakcanary.LeakCanaryActivity has leaked:
//static com.qproject.test.TestManager.instance的com.qproject.test.TestManager.context引用了回收的內存對象
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * GC ROOT static com.qproject.test.TestManager.instance
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * references com.qproject.test.TestManager.context
//內存泄露對象大小,Reference Key,Device和Android Version等信息
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * leaks com.qproject.test.leakcanary.LeakCanaryActivity instance
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Retaining: 46 KB.
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Reference Key: 3d74d294-70dc-4447-a9a2-64e656ea86b8
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Device: Genymotion Android PREVIEW - Google Nexus 5X - 7.0.0 - API 24 - 1080x1920 vbox86p
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Android Version: 7.0 API: 24 LeakCanary: 1.5 00f37f5
12-25 07:50:51.710 4941-5795/com.qproject.main D/LeakCanary: * Durations: watch=5038ms, gc=137ms, heap dump=2390ms, analysis=27325ms
//內存泄露對象詳細信息
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Details:
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Class com.qproject.test.TestManager
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | static instance = com.qproject.test.TestManager@316184816 (0x12d898f0)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | static $classOverhead = byte[308]@316175745 (0x12d87581)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Instance of com.qproject.test.TestManager
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | static instance = com.qproject.test.TestManager@316184816 (0x12d898f0)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | static $classOverhead = byte[308]@316175745 (0x12d87581)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | context = com.qproject.test.leakcanary.LeakCanaryActivity@315059712 (0x12c76e00)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | shadow$_klass_ = com.qproject.test.TestManager
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | shadow$_monitor_ = 0
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: * Instance of com.qproject.test.leakcanary.LeakCanaryActivity
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | static $classOverhead = byte[2228]@316203009 (0x12d8e001)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mDelegate = android.support.v7.app.AppCompatDelegateImplN@315842128 (0x12d35e50)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mEatKeyUpEvent = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mResources = null
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mThemeId = 2131230884
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mCreated = true
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mFragments = android.support.v4.app.FragmentController@316183584 (0x12d89420)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mHandler = android.support.v4.app.FragmentActivity$1@316163360 (0x12d84520)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mNextCandidateRequestIndex = 0
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mOptionsMenuInvalidated = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mPendingFragmentActivityResults = android.support.v4.util.SparseArrayCompat@316172368 (0x12d86850)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mReallyStopped = true
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mRequestedPermissionsFromFragment = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mResumed = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mRetaining = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mStopped = true
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mStartedActivityFromFragment = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mStartedIntentSenderFromFragment = false
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mExtraDataMap = android.support.v4.util.SimpleArrayMap@316171864 (0x12d86658)
12-25 07:50:51.711 4941-5795/com.qproject.main D/LeakCanary: | mActionBar = null
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: | mActionModeTypeStarting = 0
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: | mActivityInfo = android.content.pm.ActivityInfo@315841984 (0x12d35dc0)
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: | mActivityTransitionState = android.app.ActivityTransitionState@316207336 (0x12d8f0e8)
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: | mApplication = com.qproject.main.QAplication@314916416 (0x12c53e40)
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: | mCalled = true
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: | mChangeCanvasToTranslucent = false
12-25 07:50:51.712 4941-5795/com.qproject.main D/LeakCanary: | mChangingConfigurations = false
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mComponent = android.content.ComponentName@315998320 (0x12d5c070)
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mConfigChangeFlags = 0
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mCurrentConfig = android.content.res.Configuration@316178888 (0x12d881c8)
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mDecor = null
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mDefaultKeyMode = 0
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mDefaultKeySsb = null
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mDestroyed = true
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mDoReportFullyDrawn = false
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mEatKeyUpEvent = false
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mEmbeddedID = null
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mEnableDefaultActionBarUp = false
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mEnterTransitionListener = android.app.SharedElementCallback$1@1887062680 (0x707a4a98)
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mExitTransitionListener = android.app.SharedElementCallback$1@1887062680 (0x707a4a98)
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mFinished = true
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mFragments = android.app.FragmentController@316183536 (0x12d893f0)
12-25 07:50:51.713 4941-5795/com.qproject.main D/LeakCanary: | mHandler = android.os.Handler@316163296 (0x12d844e0)
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: | mHasCurrentPermissionsRequest = false
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: | mIdent = 20356640
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: | mInstanceTracker = android.os.StrictMode$InstanceTracker@316183552 (0x12d89400)
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: | mInstrumentation = android.app.Instrumentation@314950632 (0x12c5c3e8)
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: | mIntent = android.content.Intent@316215416 (0x12d91078)
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: | mLastNonConfigurationInstances = null
12-25 07:50:51.714 4941-5795/com.qproject.main D/LeakCanary: | mMainThread = android.app.ActivityThread@314966272 (0x12c60100)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mManagedCursors = java.util.ArrayList@316171816 (0x12d86628)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mManagedDialogs = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mMenuInflater = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mParent = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mReferrer = java.lang.String@316215864 (0x12d91238)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mResultCode = 0
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mResultData = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mResumed = false
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mSearchEvent = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mSearchManager = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mStartedActivity = false
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mStopped = true
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mTaskDescription = android.app.ActivityManager$TaskDescription@316163328 (0x12d84500)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mTemporaryPause = false
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mTitle = java.lang.String@315129824 (0x12c87fe0)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mTitleColor = 0
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mTitleReady = true
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mToken = android.os.BinderProxy@315867328 (0x12d3c0c0)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mTranslucentCallback = null
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mUiThread = java.lang.Thread@1959751680 (0x74cf7000)
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mVisibleBehind = false
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mVisibleFromClient = true
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mVisibleFromServer = true
12-25 07:50:51.715 4941-5795/com.qproject.main D/LeakCanary: | mVoiceInteractor = null
12-25 07:50:51.716 4941-5795/com.qproject.main D/LeakCanary: | mWindow = com.android.internal.policy.PhoneWindow@315116864 (0x12c84d40)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | mWindowAdded = true
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | mWindowManager = android.view.WindowManagerImpl@316172152 (0x12d86778)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | mInflater = com.android.internal.policy.PhoneLayoutInflater@316010352 (0x12d5ef70)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | mOverrideConfiguration = null
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | mResources = android.content.res.Resources@316235992 (0x12d960d8)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | mTheme = android.content.res.Resources$Theme@316183728 (0x12d894b0)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | mThemeResource = 2131230884
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | mBase = android.app.ContextImpl@316155392 (0x12d82600)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | shadow$_klass_ = com.qproject.test.leakcanary.LeakCanaryActivity
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | shadow$_monitor_ = 1316364430
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: * Excluded Refs:
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Field: android.view.Choreographer$FrameDisplayEventReceiver.mMessageQueue (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Thread:FinalizerWatchdogDaemon (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Thread:main (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Thread:LeakCanary-Heap-Dump (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.WeakReference (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.SoftReference (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.PhantomReference (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.Finalizer (always)
12-25 07:50:51.717 4941-5795/com.qproject.main D/LeakCanary: | Class:java.lang.ref.FinalizerReference (always)
5.獲取dump日誌文件如果覺得跟蹤信息不足以定位時,DisplayLeakActivity還爲開發者默認保存了最近7個dump文件到APP的目錄中,可以使用MAT等工具對dump文件進行進一步的分析;
vbox86p:/data/data/com.qproject.main/files/leakcanary # ls
2016-12-25_07-50-51_718.hprof 2016-12-25_07-50-51_718.hprof.result
D:\>adb pull ./data/data/com.qproject.main/files/leakcanary/2016-12-25_07-50-51_ 718.hprof
[100%] ./data/data/com.qproject.main/f...akcanary/2016-12-25_07-50-51_718.hprof
Android Studio->View->Tool Windows->Captures,打開Captures窗口,將pull獲取的hprof文件剪切到Capture中文件的目錄下,雙擊打開即可;具體的分析過程,這裏就不重點講述,大家去查詢MAT相關的資料;
五、檢測其他對象
如果想監聽其他的對象(例如Fragment ),可以通過RefWatcher的實例來實現;
六、LeakCanary原理
1.RefWatcher.watch()函數會爲被監控的對象創建一個KeyedWeakReference弱引用對象,是WeakReference對的子類,增加了鍵值對信息,後面會根據指定的鍵key找到弱引用對象;
2.在後臺線程AndroidWatchExecutor中,檢查KeyedWeakReference弱引用是否已經被清楚。如果還存在,則觸發一次垃圾回收之後。垃圾回收之後,如果弱引用對象依然存在,說明發生了內存泄露;