内存泄露和LeakCanary的故事

698 阅读8分钟

新鲜文章请关注微信公众号: 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的原理:

  • 弱引用WeakReference

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

  • 引用队列ReferenceQueue

我们经常用 WeakReference 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发送通知。

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

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