(转载)http://chenfuduo.me/2016/01/21/LeakCanary%E7%9A%84%E4%BD%BF%E7%94%A8/#more
LeakCanary是用于Android中内存泄露检测的一个工具。那么什么是内存泄露?内存泄露和内存溢出的区别是什么?简单的说下我的理解-内存溢出(out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory,比如申请了一个integer,但是给它存了long才能存下的数,那就是内存溢出。
内存泄露(memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但是内存泄露堆积后果很严重,无论多少内存,迟早会被占光。memory leak最终会导致out of memory。
本篇主要针对Android中内存泄露的探测工具leakcanary做相关分析。
在build.gradle
中加入引用,不同的编译使用不同的引用:
dependencies {
compile 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
compile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
}
|
接下来在Application中,使用:
package me.jarvischen.leakcanary; import android.app.Application; import android.os.StrictMode; import com.squareup.leakcanary.LeakCanary; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.GINGERBREAD; public class MyApp extends Application { @Override public void onCreate() { super.onCreate(); enabledStrictMode(); LeakCanary.install(this); } private void enabledStrictMode() { if (SDK_INT >= GINGERBREAD) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() // .detectAll() // .penaltyLog() // .penaltyDeath() // .build()); } } } |
这样就可以使用了,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知。
通过一个具体的案例来查看这个工具,在Activity中,添加下面的代码:
package me.jarvischen.leakcanary; import android.support.v7.app.AppCompatActivity; import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.util.Log; import android.view.View; public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); View button = findViewById(R.id.test); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startAsyncTask(); } }); } void startAsyncTask() { // This async task is an anonymous class and therefore has a hidden reference to the outer // class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation), // the activity instance will leak. new AsyncTask |
布局就是两个按钮:
<LinearLayout 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" android:orientation="vertical" android:gravity="center" tools:context="me.jarvischen.leakcanary.MainActivity"> <Button android:id="@+id/test" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="5dp" android:text="测试" /> LinearLayout> |
然后新建一个Application,代码如下:
package me.jarvischen.leakcanary; import android.app.Application; import android.os.StrictMode; import com.squareup.leakcanary.LeakCanary; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.GINGERBREAD; public class MyApp extends Application { @Override public void onCreate() { super.onCreate(); enabledStrictMode(); LeakCanary.install(this); } private void enabledStrictMode() { if (SDK_INT >= GINGERBREAD) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() // .detectAll() // .penaltyLog() // .penaltyDeath() // .build()); } } } |
在清单文件中注册这个Application,这样就可以运行了。
开始,点击按钮,然后立刻退出应用,看到LogCat打印了onDestroy()
的信息,说明,Activity的生命周期结束了。
过一会,看见通知栏出现了下面的通知:
打开查看详情:
他同样会产生日志:
In me.jarvischen.leakcanary:1.0:1.
* me.jarvischen.leakcanary.MainActivity has leaked:
* GC ROOT static android.os.AsyncTask.sDefaultExecutor
* references android.os.AsyncTask$SerialExecutor.mActive
* references android.os.AsyncTask$SerialExecutor$1.val$r (anonymous class implements java.lang.Runnable)
* references android.os.AsyncTask$3.this$0 (anonymous class extends java.util.concurrent.FutureTask)
* references me.jarvischen.leakcanary.MainActivity$2.this$0 (anonymous class extends android.os.AsyncTask)
* leaks me.jarvischen.leakcanary.MainActivity instance
* Retaining: 13 KB.
* Reference Key: 2c8d2e30-1c96-4f71-be32-dbaf23302d48
* Device: Meizu Meizu m2 m2
* Android Version: 5.1 API: 22 LeakCanary: 1.4-beta1 02804f3
* Durations: watch=5054ms, gc=164ms, heap dump=2215ms, analysis=29393ms
* Details:
* Class android.os.AsyncTask
| static MESSAGE_POST_RESULT = 1
| static sThreadFactory = android.os.AsyncTask$1@584378432 (0x22d4e840)
| static sPoolWorkQueue = java.util.concurrent.LinkedBlockingQueue@583012656 (0x22c01130)
| static KEEP_ALIVE = 1
| static sHandler = android.os.AsyncTask$InternalHandler@584385952 (0x22d505a0)
| static LOG_TAG = java.lang.String@1876968600 (0x6fe04498)
| static $staticOverhead = byte[104]@583532769 (0x22c800e1)
| static sDefaultExecutor = android.os.AsyncTask$SerialExecutor@584378416 (0x22d4e830)
| static SERIAL_EXECUTOR = android.os.AsyncTask$SerialExecutor@584378416 (0x22d4e830)
| static MAXIMUM_POOL_SIZE = 9
| static CORE_POOL_SIZE = 5
| static THREAD_POOL_EXECUTOR = java.util.concurrent.ThreadPoolExecutor@583041200 (0x22c080b0)
| static MESSAGE_POST_PROGRESS = 2
| static CPU_COUNT = 4
* Instance of android.os.AsyncTask$SerialExecutor
| mActive = android.os.AsyncTask$SerialExecutor$1@583008640 (0x22c00180)
| mTasks = java.util.ArrayDeque@584385984 (0x22d505c0)
* Instance of android.os.AsyncTask$SerialExecutor$1
| this$0 = android.os.AsyncTask$SerialExecutor@584378416 (0x22d4e830)
| val$r = android.os.AsyncTask$3@583016800 (0x22c02160)
* Instance of android.os.AsyncTask$3
| this$0 = me.jarvischen.leakcanary.MainActivity$2@583016768 (0x22c02140)
| callable = android.os.AsyncTask$2@583008624 (0x22c00170)
| outcome = null
| runner = java.lang.Thread@583038432 (0x22c075e0)
| state = 0
| waiters = null
* Instance of me.jarvischen.leakcanary.MainActivity$2
| this$0 = me.jarvischen.leakcanary.MainActivity@584012960 (0x22cf54a0)
| mCancelled = java.util.concurrent.atomic.AtomicBoolean@584410704 (0x22d56650)
| mFuture = android.os.AsyncTask$3@583016800 (0x22c02160)
| mStatus = android.os.AsyncTask$Status@1886480304 (0x707167b0)
| mTaskInvoked = java.util.concurrent.atomic.AtomicBoolean@584410720 (0x22d56660)
| mWorker = android.os.AsyncTask$2@583008624 (0x22c00170)
* Instance of me.jarvischen.leakcanary.MainActivity
| static $staticOverhead = byte[8]@583299073 (0x22c47001)
| static TAG = java.lang.String@584403360 (0x22d549a0)
| mDelegate = android.support.v7.app.AppCompatDelegateImplV14@583697984 (0x22ca8640)
| mCreated = true
| mFragments = android.support.v4.app.FragmentController@584410736 (0x22d56670)
| mHandler = android.support.v4.app.FragmentActivity$1@584448448 (0x22d5f9c0)
| mMediaController = null
| mOptionsMenuInvalidated = false
| mReallyStopped = true
| mRequestedPermissionsFromFragment = false
| mResumed = false
| mRetaining = false
| mStopped = true
| mAccessControlManager = null
| mActionBar = null
| mActionBarToTop = false
| mActionModeHeaderHidden = false
| mActivityInfo = android.content.pm.ActivityInfo@584382336 (0x22d4f780)
| mActivityTransitionState = android.app.ActivityTransitionState@584450880 (0x22d60340)
| mAllLoaderManagers = android.util.ArrayMap@584448480 (0x22d5f9e0)
| mApplication = me.jarvischen.leakcanary.MyApp@584064480 (0x22d01de0)
| mCalled = true
| mChangeCanvasToTranslucent = false
| mChangingConfigurations = false
| mCheckedForLoaderManager = true
| mComponent = android.content.ComponentName@584410752 (0x22d56680)
| mConfigChangeFlags = 0
| mContainer = android.app.Activity$1@584410768 (0x22d56690)
| mContext = null
| mCurrentConfig = android.content.res.Configuration@584406288 (0x22d55510)
| mDecor = null
| mDefaultKeyMode = 0
| mDefaultKeySsb = null
| mDestroyed = true
| mDisableStatusBarIconTheme = false
| mDoReportFullyDrawn = false
| mEmbeddedID = null
| mEnableDefaultActionBarUp = false
| mEnterTransitionListener = android.app.SharedElementCallback$1@1886474784 (0x70715220)
| mExitTransitionListener = android.app.SharedElementCallback$1@1886474784 (0x70715220)
| mFinished = true
| mFragments = android.app.FragmentManagerImpl@584406400 (0x22d55580)
| mHandler = android.os.Handler@584448512 (0x22d5fa00)
| mIdent = 318223219
| mInstanceTracker = android.os.StrictMode$InstanceTracker@584410784 (0x22d566a0)
| mInstrumentation = android.app.Instrumentation@584454480 (0x22d61150)
| mIntent = android.content.Intent@584450944 (0x22d60380)
| mLastNonConfigurationInstances = null
| mLoaderManager = null
| mLoadersStarted = false
| mMainThread = android.app.ActivityThread@583020912 (0x22c03170)
| mManagedCursors = java.util.ArrayList@584448544 (0x22d5fa20)
| mManagedDialogs = null
| mMenuInflater = null
| mParent = null
| mReferrer = java.lang.String@584448576 (0x22d5fa40)
| mResultCode = 0
| mResultData = null
| mResumed = false
| mSearchManager = null
| mStartedActivity = false
| mStopped = true
| mTemporaryPause = false
| mTitle = java.lang.String@584062432 (0x22d015e0)
| mTitleColor = 0
| mTitleReady = true
| mToken = android.os.BinderProxy@584448608 (0x22d5fa60)
| mTopRegionMainColor = null
| mTranslucentCallback = null
| mUiThread = java.lang.Thread@1969037240 (0x755d1fb8)
| mVisibleBehind = false
| mVisibleFromClient = true
| mVisibleFromServer = true
| mVoiceInteractor = null
| mWindow = com.android.internal.policy.impl.PhoneWindow@583621504 (0x22c95b80)
| mWindowAdded = true
| mWindowManager = android.view.WindowManagerImpl@584448640 (0x22d5fa80)
| mInflater = com.android.internal.policy.impl.PhoneLayoutInflater@584396512 (0x22d52ee0)
| mOverrideConfiguration = null
| mResources = android.content.res.Resources@583967120 (0x22cea190)
| mTheme = android.content.res.Resources$Theme@584448672 (0x22d5faa0)
| mThemeResource = 2131296386
| mBase = android.app.ContextImpl@584382464 (0x22d4f800)
* Excluded Refs:
| Field: android.view.inputmethod.InputMethodManager.mNextServedView
| Field: android.view.inputmethod.InputMethodManager.mServedView
| Field: android.view.inputmethod.InputMethodManager.mServedInputConnection
| Field: android.view.inputmethod.InputMethodManager.mCurRootView
| Field: android.animation.LayoutTransition$1.val$parent
| Field: android.view.textservice.SpellCheckerSession$1.this$0
| Field: android.support.v7.internal.widget.ActivityChooserModel.mActivityChoserModelPolicy
| Field: android.widget.ActivityChooserModel.mActivityChoserModelPolicy
| Field: android.accounts.AccountManager$AmsTask$Response.this$1
| Field: android.media.MediaScannerConnection.mContext
| Field: android.os.UserManager.mContext
| Field: android.media.AudioManager$1.this$0
| Field: android.view.Choreographer$FrameDisplayEventReceiver.mMessageQueue (always)
| Static field: android.text.TextLine.sCached
| Thread:FinalizerWatchdogDaemon (always)
| Thread:main (always)
| Thread:LeakCanary-Heap-Dump (always)
| Class:java.lang.ref.WeakReference (always)
| Class:java.lang.ref.SoftReference (always)
| Class:java.lang.ref.PhantomReference (always)
| Class:java.lang.ref.Finalizer (always)
| Class:java.lang.ref.FinalizerReference (always)
| Root Class:android.os.Binder (always)
|
这个案例可以对这个工具有初步的印象,当然这个案例之所以内存泄露,是因为按钮的点击事件,这个点击事件在内部类AsyncTask中,执行了20秒的任务,这个内部类持有外部类MainActivity的引用,当点击按钮后立刻退出,Activity执行了onDestroy方法,但是任务还是没有结束,于是Activity得不到释放,而Activity又指向一个Window,Window又拥有整个View继承树,算下来,那可是一大段的内存空间。
LeakCanary的作者Pierre-Yvews Ricau
在Droidcon NYC 2015
做了一个关于内存泄露的非技术性解释,一下的文字来自这里
假设我的手代表着我们 App 能用的所有内存。我的手里能放很多东西。比如:钥匙,Android 玩偶等等。设想我的 Android 玩偶需要扬声器才能工作,而我的扬声器也需要依赖 Android 玩偶才能工作,因此他们持有彼此的引用。
我的手里可以持有如此多的东西。扬声器依附到 Android 玩偶上会增加总重量,就像引用会占用内存一样。一旦我放弃了我的玩偶,把他扔到地上,会有垃圾回收器来回收掉它。一旦所有的东西都进了垃圾桶,我的手又轻便了。
不幸的是,有的时候,一些不好的情况会发生。比如:我的钥匙没准和我的 Android 玩偶黏在了一起,阻止我把 Android 玩偶扔到地上。最终的结果就是 Android 玩偶无论如何都不会被回收掉。这就是内存泄露。
有外部的引用(钥匙,扬声器)指向了 本不应该再指向的对象(Andorid 玩偶)。类似这样的小规模的内存泄露堆积以后就会造成大麻烦。
现在我们知道,可被回收的 Android 对象应该及时被销毁。但是还是没法清楚看到这些对象是否已经被回收掉。有了 LeakCanary 以后,可以给可被回收的 Android 对象上打了智能标记。智能标记能知道他们所指向的对象是否被成功释放掉。如果过一小段时间对象依然没有被释放,他就会给内存做个快照。
LeakCanary 随后会把结果发布出来,帮助我们看到内存到底怎么泄露了,清晰的展示无法被释放的对象的引用链。
下面的这行代码:
private static Button buyNowButton;
|
由于某种原因,把这个 button 设置成了 static 的。问题随之而来,这个按钮除非你设置成了null,不然就内存泄露了。
你也许会说:”只是一个按钮而已,没啥大不了”。问题是这个按钮还有一个成员变量:叫 “mContext”,这个东西指向了一个 Acitvity,Acitivty 又指向了一个 Window,Window 有拥有整个 View 继承树。算下来,那可是一大段的内存空间。
静态的变量是 GC root 类型的一种。垃圾回收器会尝试回收所有非 GC root 的对象,或者某些被 GC root 持有的对象。所以如果你创建一个对象,并且移除了这个对象的所有指向,他就会被回收掉。但是一旦你将一个对象设置成 GC root,那他就不会被回收掉。
很显然上面那个按钮持有了一个 Activity 的引用,所以我们必须清理掉它。当你沉浸在你的代码的时候,你肯定很难发现这个问题。你可能只看到了引出的引用。你可以知道 Activity 引用了一个 Window,但是谁引用了 Activity?
你可以用像 IntelliJ 这样的工具做些分析,但是它并不会告诉你所有的东西。通常,你可以把这些 Object 的引用关系组织成图,但是是个单向图。
我用mat工具分析了很久,但是还是没有分析出什么,可能是我用的不熟,总之这个工具比MAT好用多了(个人觉得)。
再看看智能标记(smart pin),我们希望知道的是当生命后期结束后,发生了什么。幸运的时,LearkCanary有一个很简单的 API。
第一步:创建 RefWatcher
。给 RefWatcher 传入一个对象的实例,它会检测这个对象是否被成功释放掉。
public class MyApp extends Application { public static RefWatcher getRefWatcher(Context context) { ExampleApplication application = (MyApp) context.getApplicationContest(); return application.refWatcher; } private RefWatcher refWatcher; @Override public void onCreate () { super.onCreate(); // Using LeakCanary refWatcher = LeakCanary.install(this); } } |
第二步:监听 Activity 生命周期。然后,当 onDestroy 被调用的时候,我们传入 Activity。
@Override public void onActivityDestroyed(Activity activity) { refWatcher.watch(activity); } |
RefWatcher 检查对象是否被回收是在一个 Executor 中执行的, Android 的监控 提供了AndroidWatchExecutor
, 它在主线程执行, 但是有一个delay 时间(默认5000 milisecs), 因为对于application 来说,执行destroy activity只是把必要资源回收,activity 对象不一定会马上被 gc回收。AndroidWatchExecutor
的源码:
/* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.leakcanary; import android.annotation.TargetApi; import android.app.Activity; import android.app.Application; import android.os.Bundle; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH; import static com.squareup.leakcanary.Preconditions.checkNotNull; @TargetApi(ICE_CREAM_SANDWICH) public final class ActivityRefWatcher { public static void installOnIcsPlus(Application application, RefWatcher refWatcher) { if (SDK_INT < ICE_CREAM_SANDWICH) { // If you need to support Android < ICS, override onDestroy() in your base activity. return; } ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher); activityRefWatcher.watchActivities(); } private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = new Application.ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { ActivityRefWatcher.this.onActivityDestroyed(activity); } }; private final Application application; private final RefWatcher refWatcher; /** * Constructs an {@link ActivityRefWatcher} that will make sure the activities are not leaking * after they have been destroyed. */ public ActivityRefWatcher(Application application, final RefWatcher refWatcher) { this.application = checkNotNull(application, "application"); this.refWatcher = checkNotNull(refWatcher, "refWatcher"); } void onActivityDestroyed(Activity activity) { refWatcher.watch(activity); } public void watchActivities() { // Make sure you don't get installed twice. stopWatchingActivities(); application.registerActivityLifecycleCallbacks(lifecycleCallbacks); } public void stopWatchingActivities() { application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks); } } |
刚才提到过静态域的变量会持有Activity的引用。所以一个声明为静态的按钮就会持有 mContext 对象,导致 Activity 无法被释放掉。这个被称作强引用(strong reference)。在垃圾回收过程中,可以对一个对象有很多的强引用。当这些强引用的个数总和为零的时候,垃圾回收器就会释放掉它。
弱引用,就是一种不增加引用总数的持有引用方式
。垃圾回收期是否决定要回收一个对象,只取决于它是否还存在强引用。所以说,如果我们将我们的 Activity 持有为弱引用,一旦我们发现弱引用持有的对象已经被销毁了,那么这个 Activity 就已经被垃圾回收器回收了。否则,那可以大概确定这个 Activity 已经被泄露了。
private static Button buyNowButton; Context mContext; |
WeakReference |
public class Baguette Activity extends Activity { @Override protected void onCreate(Bundle state) { super.onCreate(state); setContentView(R.layout.activity_main); } } |
弱引用的主要目的是为了做 Cache,而且非常有用。主要就是告诉 GC,尽管我持有了这个对象,但是如果一旦没有对象在用这个对象的时候,GC 就可以在需要的时候销毁掉。
在下面的例子中,继承了 WeakReference:
final class KeyedWeakReference extends WeakReference<Object> { public final String key; // (1) Unique identifier public final String name; KeyedWeakReference(Object referent, String key, String name, ReferenceQueue super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue")); this.key = checkNotNull(key, "key"); this.name = checkNotNull(name, "name"); } } |
可以看到,我们给弱引用添加了一个 Key,这个 Key 是一个唯一字符串。想法是这样的:当我们解析一个 heap dump 文件的时候,我们可以询问所有的 KeyedWeakReference 实例,然后找到对应的 Key。
首先,我们创建一个 weakReference,然后我们写入『一会儿,我需要检查弱引用』。(尽管一会儿可能就是几秒后)。当我们调用 watch 函数的时候,其实就是发生了这些事情。
public void watch(Object watchedReference, String referenceName) { checkNotNull(watchedReference, "watchedReference"); checkNotNull(referenceName, "referenceName"); if (debuggerControl.isDebuggerAttached()) { return; } final long watchStartNanoTime = System.nanoTime(); String key = UUID.randomUUID().toString(); retainedKeys.add(key); final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue); watchExecutor.execute(() → { ensureGone(reference, watchStartNanoTime); }); } |
在这一切的背后,我们调用了 System.GC — 免责声明— 我们本不应该去做这件事情。然而,这是一种告诉垃圾回收器:『Hey,垃圾回收器,现在是一个不错的清理垃圾的时机。』,然后我们再检查一遍,如果发现有些对象依然存活着,那么可能就有问题了。我们就要触发 heap dump 操作了。
当 LeakCanary 探测到一个 Activity 已经被销毁掉,而没有被垃圾回收器回收掉的时候,它就会强制导出一份 heap dump 文件存在磁盘上。然后开启另外一个进程去分析这个文件得到内存泄漏的结果。如果在同一进程做这件事的话,可能会在尝试分析堆内存结构的时候而发生内存不足的问题。
最后,你会得到一个通知,点击一下就会展示出详细的内存泄漏链。而且还会展示出内存泄漏的大小,你也会很明确自己解决掉这个内存泄漏后到底能够解救多少内存出来。
LeakCanary 也是支持 API 的,这样你就可以挂载内存泄漏的回调,比方说可以把内存泄漏问题传到服务器上。
Resources resources = context.getResources(); LayoutInflater inflater = LayoutInflater.from(context); File filesDir = context.getFilesDir(); InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); |
可以从上面那几行代码出发写具体的案例,这里写第一个,具体代码如下:
package me.jarvischen.leakcanary; import android.content.res.Resources; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; public class SecondLeakActivity extends AppCompatActivity { private static final String TAG = SecondLeakActivity.class.getSimpleName(); private static Button btn; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second_leak); btn = (Button) findViewById(R.id.test2); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Resources resources = SecondLeakActivity.this.getResources(); } }); } @Override protected void onDestroy() { super.onDestroy(); Log.e(TAG, "onDestroy"); } } |
检测出来的是:
源码
参考:
Droidcon NYC 2015
MAT的使用
Android 内存泄露检测工具 LeakCanary 的监控原理