内存泄露和LeakCanary的故事

新鲜文章请关注微信公众号: JueCode

今天我们来聊一聊Android中的内存泄露和内存泄露的检测工具LeakCanary。Java有垃圾回收线程为什么还会发生内存泄露?原因就是外部人为持有对象引用,持有引用者的生命周期大于被引用者的生命周期,导致本该回收的对象不能被回收,从而继续停留在内存中。

常见的内存泄露有:

  • 集合类
  • Static关键字修饰的成员变量
  • 非静态内部类或者匿名类
  • 资源对象使用后未及时关闭等

其中第三点非静态内部类或者匿名类导致的内存泄露在开发中比较容易疏忽,重点来看下这个特点的例子。

首先看下LeakCanary的使用:

1.LeakCanary使用

首先在build.gradle中引入,

dependencies {
        debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
        releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}

在Application中:

public class MyApplication extends Application {

    public static RefWatcher getRefWatcher(Context context){
        MyApplication application = (MyApplication) context.getApplicationContext();
        return application.mRefWatcher;
    }

    private RefWatcher mRefWatcher;

    @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;
        }
        mRefWatcher = LeakCanary.install(this);
    }
}

使用RefWatcher监控哪些本该被回收的对象:

@Override
protected void onDestroy() {
    super.onDestroy();
    Log.d(TAG, "onDestroy");
    MyApplication.getRefWatcher(this).watch(this);
}

OK,LeakCanary的使用就是这么简单。

接下来看看怎么来造内存泄露,这里通过多线程和内部类(Handler)来造内存泄露。

2.内存泄露

没什么好说的,直接上代码, 在onCreate中启动一个线程,这里是通过匿名内部类的方式,然后通过handler发送消息到主线程。

    private Handler mHandler;
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mHandler = new LeakHandler();

        new Thread() {
            @Override
            public void run() {
                try {
                    sleep(10000);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                Message message = Message.obtain();
                message.what = 1;
                message.obj = "test";
                mHandler.sendMessage(message);
            }
        }.start();

        mButton = findViewById(R.id.btn);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);
                startActivity(intent);
                MainActivity.this.finish();
            }
        });
    }


    private class LeakHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    Log.d(TAG, (String) msg.obj);
                    break;
            }

        }
    }

上面的例子会造成内存泄露吗?

leak.png

可以看到MainActivity实例内存泄露了,并且是由于MainActivity中的匿名内部类Thread造成的。

为什么呢?

有个知识点需要补充下:

匿名内部类或者非静态内部类会持有外部类的引用。静态内部类不会持有外部类的引用。

所以在上面例子中虽然在按钮的点击事件中

 MainActivity.this.finish();

但是由于Thread延迟10s发送消息,而Thread引用了MainActivity的实例,导致MainActivity不能被回收,引起内存泄露。

原因既然知道了,我们就把Thread改造成静态内部类看看还会不会泄露。

MainActivity代码改成下面:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mHandler = new LeakHandler();

        new LeakThread(mHandler).start();

        mButton = findViewById(R.id.btn);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);
                startActivity(intent);
                MainActivity.this.finish();
            }
        });
    }
    
    private static class LeakThread extends Thread{
        private LeakHandler mHandler;

        private LeakThread(LeakHandler handler) {
            mHandler = handler;
        }

        @Override
        public void run() {
            try {
                sleep(10000);
            } catch (Exception e) {
                e.printStackTrace();
            }

            Message message = Message.obtain();
            message.what = 1;
            message.obj = "test";
            mHandler.sendMessage(message);
        }
    }


    private class LeakHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    Log.d(TAG, (String) msg.obj);
                    break;
            }
        }
    }
leakHandler.png

可以看出来还是有泄露,为什么?可以看到是LeakHandler导致的内存泄露,这是因为LeakHandler也是非静态内部类的定义方式,引用了外部类MainActivity。

leakHandlerReason.png

那么思路就很自然了,把LeakHandler改造成静态内部类,其他代码不动:

private static class LeakHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    Log.d(TAG, (String) msg.obj);
                    break;
            }

        }
}

经过上面两个步骤就不会发生内存泄露了。

接下来扒一扒LeakCanary的源码。

3.LeakCanary源码分析

mRefWatcher = LeakCanary.install(this);开始着手。

  /**
   * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
   * references (on ICS+).
   */
  public static RefWatcher install(Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        .buildAndInstall();
  }

用的建造者的模式,通过AndroidRefWatcherBuilder进行构造:

  /** Builder to create a customized {@link RefWatcher} with appropriate Android defaults. */
  public static AndroidRefWatcherBuilder refWatcher(Context context) {
    return new AndroidRefWatcherBuilder(context);
  }

  /**
   * Creates a {@link RefWatcher} instance and starts watching activity references (on ICS+).
   */
  public RefWatcher buildAndInstall() {
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      LeakCanary.enableDisplayLeakActivity(context);
      ActivityRefWatcher.install((Application) context, refWatcher);
    }
    return refWatcher;
  }

最终调用到ActivityRefWatcher, 可以看到install中调用watchActivities,里面会通过application注册回调接口Application.ActivityLifecycleCallbacks:

public final class ActivityRefWatcher {

  /** @deprecated Use {@link #install(Application, RefWatcher)}. */
  @Deprecated
  public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
    install(application, refWatcher);
  }

  public static void install(Application application, RefWatcher refWatcher) {
    new ActivityRefWatcher(application, refWatcher).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, 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);
  }
}

注册监听ActivityLifecycleCallbacks, 但是只监听了所有ActivityonActivityDestroyed事件,当 ActivityDestory 时,调用 ActivityRefWatcher.this.onActivityDestroyed(activity); 函数。

继续看, 最终是把目标activity对象传给了RefWatcher,让它监控这个activity是否被正常回收,如果没有被回收就意味着发生了内存泄露。

  void onActivityDestroyed(Activity activity) {
    refWatcher.watch(activity);
  }

  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, RefWatcher refWatcher) {
    this.application = checkNotNull(application, "application");
    this.refWatcher = checkNotNull(refWatcher, "refWatcher");
  }

RefWatcher怎么监控activity是否被正常回收?

RefWatcher可以看到前面例子中用到的watch方法:

  /**
   * Watches the provided references and checks if it can be GCed. This method is non blocking,
   * the check is done on the {@link WatchExecutor} this {@link RefWatcher} has been constructed
   * with.
   *
   * @param referenceName An logical identifier for the watched object.
   */
  public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    ensureGoneAsync(watchStartNanoTime, reference);
  }

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }

final class KeyedWeakReference extends WeakReference<Object> {
  public final String key;
  public final String name;

  KeyedWeakReference(Object referent, String key, String name,
      ReferenceQueue<Object> referenceQueue) {
    super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
    this.key = checkNotNull(key, "key");
    this.name = checkNotNull(name, "name");
  }
}

可以看到,它把传入的activity包装成一个KeyedWeakReference, 然后watchExecutor会去执行Runnable,调用ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference)

问题来了,watch函数怎么检查一个 activity是否被正常回收?看到ensureGone方法中

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    removeWeaklyReachableReferences();

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    if (gone(reference)) {
      return DONE;
    }
    gcTrigger.runGc();
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
      heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));
    }
    return DONE;
  }

首先先回顾一下WeakReferenceReferenceQueue的原理:

1.弱引用WeakReference

只要被GC发现就会被回收,被强引用的对象就算发生OOM也不会被GC回收。

2.引用队列ReferenceQueue

我们经常用 WeakReference<Activity> reference = new WeakReference(activity);,创建一个 reference 来弱引用到某个 activity,当这个 activity 被垃圾回收器回收后,这个 reference 会被放入内部的 ReferenceQueue 中。也就是说,从队列 ReferenceQueue 取出来的所有 reference,它们指向的真实对象都已经成功被回收了。

继续看上面的watch函数,一个 activity 传给 RefWatcher 时会创建一个唯一的 key 对应这个 activity,该 key 存入一个集合 retainedKeys 中。也就是说,所有我们想要观测的 activity 对应的唯一 key 都会被放入 retainedKeys 集合中。

String key = UUID.randomUUID().toString();
retainedKeys.add(key);

接下来重点分析下ensureGone函数的执行流程:

  1. 刚进来首先removeWeaklyReachableReferences, 就是循环从引用队列中取出引用,如果不为null就说明该引用的对象已经被GC回收, 那么就需要从 retainedKeys 集合中删除这个activity对应的key。剩下的key都是未被回收的对象。
private void removeWeaklyReachableReferences() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
      retainedKeys.remove(ref.key);
    }
  }
  1. 接下来调用gone方法, 判断某个reference对应的key是否仍在集合里,不在表示已回收,否则继续。
private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
}
  1. 接着, 出发GC回收所有 WeakReference 引用的对象
gcTrigger.runGc();
  1. 接着继续removeWeaklyReachableReferences,再次清理retainedKeys 集合,如果该reference 还在 retainedKeys(if (!gone(reference))),表示泄漏;

  2. 接着通过heapDumper把内存情况dump成文件,并调用 heapdumpListener 进行内存分析,进一步确认是否发生内存泄漏

File heapDumpFile = heapDumper.dumpHeap();
heapdumpListener.analyze(
          new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
              gcDurationMs, heapDumpDurationMs));

其中heapdumpListener:

private final HeapDump.Listener heapdumpListener;

实现类是:

public final class ServiceHeapDumpListener implements HeapDump.Listener {

  private final Context context;
  private final Class<? extends AbstractAnalysisResultService> listenerServiceClass;

  public ServiceHeapDumpListener(Context context,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    setEnabled(context, listenerServiceClass, true);
    setEnabled(context, HeapAnalyzerService.class, true);
    this.listenerServiceClass = checkNotNull(listenerServiceClass, "listenerServiceClass");
    this.context = checkNotNull(context, "context").getApplicationContext();
  }

  @Override public void analyze(HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }
}

其中HeapAnalyzerServiceIntentService,在onHandleIntent中通过HeapAnalyzer分析dump文件,然后把分析结果发送DisplayLeakService显示通知。

/**
 * This service runs in a separate process to avoid slowing down the app process or making it run
 * out of memory.
 */
public final class HeapAnalyzerService extends IntentService {

  private static final String LISTENER_CLASS_EXTRA = "listener_class_extra";
  private static final String HEAPDUMP_EXTRA = "heapdump_extra";

  public static void runAnalysis(Context context, HeapDump heapDump,
      Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
    Intent intent = new Intent(context, HeapAnalyzerService.class);
    intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName());
    intent.putExtra(HEAPDUMP_EXTRA, heapDump);
    context.startService(intent);
  }

  public HeapAnalyzerService() {
    super(HeapAnalyzerService.class.getSimpleName());
  }

  @Override protected void onHandleIntent(Intent intent) {
    if (intent == null) {
      CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
      return;
    }
    String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
    HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

    HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);

    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }
}

到这里,核心的内存泄漏检测机制便看完了。

4.总结

从上面可以看到,不能因为Java有GC机制就不注意内存泄露的问题,除了写代码中要注意,配合工具能更好的预防和检测泄露的问题。上面主要介绍了LeakCanary的使用也简单分析了下源码。总结就是下面几个步骤:

  1. 利用application.registerActivityLifecycleCallbacks监听整个生命周期内的Activity onDestoryed事件

  2. 通过RefWatcher.watch做观测;

  3. RefWatcherActivity使用KeyedWeakReference包装,并且使用ReferenceQueue记录指向的对象activity是否被回收;

  4. AndroidWatchExecutor在一段时间后开始检查弱引用的activity是否被正常回收,判断依据就是通过引用队列ReferenceQueue,如果在队列里就表明已被GC回收;

  5. 如果没有在引用队里里,那么会调用gcTrigger.runGc(), 再进行第四步的判断,如果还没在队列里就表明发生内存泄露;

  6. 利用HeapAnalyzer对dump文件进行分析,分析结果利用DisplayLeakService发送通知。

今天的内容就到这了,欢迎点赞和关注哈!

感谢@右倾倾理解和支持!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,360评论 0 12
  • Android 内存管理的目的 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。简单粗...
    晨光光阅读 1,286评论 1 4
  • "Read the fucking source code" -- linus一句名言体现出了阅读源码的重要性,学...
    呆萌狗和求疵喵阅读 22,023评论 5 56
  • 什么是内存泄露 一些对象有着有限的生命周期。当这些对象所要做的事情完成了,我们希望他们会被回收掉。但是如果有一系列...
    皇马船长阅读 1,120评论 0 3
  • 生活中不期而遇的事情,不期而遇的人,旧时的朋友,似乎变得让自己讶异,于是沉默不再争辩,不想看到曾经的朋友在...
    多琳的色彩空间阅读 154评论 0 0