详解LeakCanary分析内存泄露如何实现

 更新时间:2022年12月06日 11:41:05   作者:流浪汉kylin  
这篇文章主要为大家介绍了详解LeakCanary分析内存泄露如何实现,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

前言

平时我们都有用到LeakCanary来分析内存泄露的情况,这里可以来看看LeakCanary是如何实现的,它的内部又有哪些比较有意思的操作。

LeakCanary的使用

官方文档:square.github.io/leakcanary/…

引用方式

dependencies {
    // debugImplementation because LeakCanary should only run in debug builds. 
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
}

可以看到LeakCanary的新版本中依赖非常简单,甚至不需要你做什么就可以直接使用。

LeakCanary原理

LeakCanary的封装主要是利用ContentProvider,LeakCanary检测内存泄漏主要是监听Activity和Fragment、view的生命周期,配合弱引用和ReferenceQueue。

源码浅析

初始化

首先debugImplementation只是在Debug的包会依赖,在正式包不会把LeakCanary的内容打进包中。

LeakCanary的初始化是使用了ContentProvider,ContentProvider的onCreate会在Application的onCreate之前,它把ContentProvider写在自己的AndroidMainifest中,打包时会进行合并,所以这整个过程都不需要接入端做初始化操作。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.squareup.leakcanary.objectwatcher" >
    <uses-sdk android:minSdkVersion="14" />
    <application>
        <provider
            android:name="leakcanary.internal.MainProcessAppWatcherInstaller"
            android:authorities="${applicationId}.leakcanary-installer"
            android:enabled="@bool/leak_canary_watcher_auto_install"
            android:exported="false" />
    </application>
</manifest>

这是它在AndroidManifest所定义的,打包的时候会合并所有的AndroidManifest

这就是它自动初始化的操作,也比较明显了,不用过多解释。

使用

先看看它要监测什么,因为LeakCanary 2.x的代码都是kotlin写的,所以这里得分析kotlin,如果不熟悉kt的朋友,我只能说尽量讲慢一些,因为我想看旧版本的能不能用java来分析,但是简单看了下源码上是有一定的差别,所以还是要分析2.x。

fun appDefaultWatchers(
  application: Application,
  reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {
  return listOf(
    ActivityWatcher(application, reachabilityWatcher),
    FragmentAndViewModelWatcher(application, reachabilityWatcher),
    RootViewWatcher(reachabilityWatcher),
    ServiceWatcher(reachabilityWatcher)
  )
}

从这里看到他主要分析Activity、Fragment和Fragment的View、RootView、Service。

看Activity的监听ActivityWatcher

监听Activity调用Destroy时会调用reachabilityWatcher的expectWeaklyReachable方法。
这里可以看看旧版本的做法(正好以前有记录)

private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
    new ActivityLifecycleCallbacksAdapter() {
      @Override public void onActivityDestroyed(Activity activity) {
        refWatcher.watch(activity);
      }
    };

旧版本是调用refWatcher的watch,虽然代码不同,但是思想一样,再看看旧版本的Fragment

private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
    new FragmentManager.FragmentLifecycleCallbacks() {
      @Override public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
        View view = fragment.getView();
        if (view != null) {
          refWatcher.watch(view);
        }
      }
      @Override
      public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
        refWatcher.watch(fragment);
      }
    };

这里监听了Fragment和Fragment的View,所以相比于新版本,旧版本只监听Activity、Fragment和Fragment的View

再回到新版本,分析完Activity的监听之后看看Fragment的

最终Destroy之后也是调用到reachabilityWatcher的expectWeaklyReachable。然后看看RootViewWatcher的操作

private val listener = OnRootViewAddedListener { rootView ->
  val trackDetached = when(rootView.windowType) {
    PHONE_WINDOW -> {
      when (rootView.phoneWindow?.callback?.wrappedCallback) {
        is Activity -> false
        is Dialog -> {
          ......
        }
        else -> true
      }
    }
    POPUP_WINDOW -> false
    TOOLTIP, TOAST, UNKNOWN -> true
  }
  if (trackDetached) {
    rootView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
      val watchDetachedView = Runnable {
        reachabilityWatcher.expectWeaklyReachable(
          rootView, "${rootView::class.java.name} received View#onDetachedFromWindow() callback"
        )
      }
        ......
    })
  }
}

最终也是调用到reachabilityWatcher的expectWeaklyReachabl。最后再看看Service的。

这边因为只是做浅析,不是源码详细分析,所以我这边就不去一个个分析是如何调用到销毁的这个方法的,我们通过上面的方法得到一个结论,Activity、Fragment和Fragment的View、RootView、Service,他们几个,在销毁时都会调用到reachabilityWatcher的expectWeaklyReachabl。所以这些地方就是检测对象是否泄漏的入口。

然后我们来看看expectWeaklyReachable方法

@Synchronized override fun expectWeaklyReachable(
  watchedObject: Any,
  description: String
) {
  // 先从queue中移除一次已回收对象
  removeWeaklyReachableObjects()
  // 生成随机数当成key
  val key = UUID.randomUUID().toString()
  val watchUptimeMillis = clock.uptimeMillis()
  // 创建弱引用关联ReferenceQueue
  val reference =
    KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
  ......
  // 把reference和key 添加到一个Map中
  watchedObjects[key] = reference
  // 下一步
  checkRetainedExecutor.execute {
    moveToRetained(key)
  }
}

你们运气真好,我正好以前也有记录旧版本的refWatcher的watch方法

public void watch(Object watchedReference, String referenceName) {
        ......
        // 生成随机数当成key
        String key = UUID.randomUUID().toString();
        // 把key 添加到一个Set中
        this.retainedKeys.add(key);
        // 创建弱引用关联ReferenceQueue
        KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
        // 下一步
        this.ensureGoneAsync(watchStartNanoTime, reference);
}

通过对比发现,模板的流程是一样的,但是细节不一样,以前是用Set,现在是用Map,这就是我觉得不能拿旧版本代码来分析的原因。

文章写到这里,突然想到一个很有意思的东西,你要是面试时,面试官看过新版本的代码,你看的是旧版本的代码,结果如果问到一些比较深入的细节,你答出来的和他所理解的不同,那就尴尬了,所以面试时得先说清楚你是看过旧版本的代码

看到用一个弱引用生成一个key和对象绑定起来。然后调用ensureGoneAsync方法

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

execute里面会调用到waitForIdle方法。

我们再回到新版本的代码中

checkRetainedExecutor.execute其实是会执行到这里(kt里面的是写得简单,但是不熟的话可以先别管怎么执行的,只要先知道反正执行到这个地方就行)

这里是做了一个延时发送消息的操作,延时5秒,具体代码在这里

写到这里我感觉有点慌了,因为如果不熟kt的朋友可能真会看困,其实如果看不懂这个代码的话没关系,只要我圈出来的地方,我觉是大概能看懂的,然后流程我会说,我的意思是没必要深入去看每一行是什么意思,我们的目的是找出大概的流程(用游戏的说法,我们是走主线任务,不是要全收集)

延迟5秒后会调回到前面的moveToRetained(key)。那不好意思各位,我又要拿旧版本来对比了,因为细节不同。

private void waitForIdle(final Retryable retryable, final int failedAttempts) {
  // 使用IdleHandler来实现在闲时才去执行后面的流程
  Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override public boolean queueIdle() {
      postToBackgroundWithDelay(retryable, failedAttempts);
      return false;
    }
  });
}

使用IdleHandler来完成闲时触发,我不记得很早之前的版本是不是也用的IdleHandler,这里使用IdleHandler只能说有好有坏吧,好处是闲时触发确实是一个很好的操作,不好的地方是如果一直有异步消息,就一直不会触发后面的流程。

private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
  long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
  long delayMillis = initialDelayMillis * exponentialBackoffFactor;
  // 根据上下文去计算,这里是5秒
  backgroundHandler.postDelayed(new Runnable() {
    @Override public void run() {
      Retryable.Result result = retryable.run();
      if (result == RETRY) {
        postWaitForIdle(retryable, failedAttempts + 1);
      }
    }
  }, delayMillis);
}

看到旧版本是先用IdelHanlder,在闲时触发的情况下再去延时5秒,而新版本是直接延时5秒,不使用IdelHandler,我没看过这块具体的文档描述,我猜是为了防止饿死,如果用IdelHanlder的话可能会出现一直不触发的情况。

返回看新版本的moveToRetained

@Synchronized private fun moveToRetained(key: String) {
  // 从ReferenceQueue中拿出对象移除
  removeWeaklyReachableObjects()
  // 经过上一步之后判断Map中还有没有这个key,有的话进入下一步操作
  val retainedRef = watchedObjects[key]
  if (retainedRef != null) {
    retainedRef.retainedUptimeMillis = clock.uptimeMillis()
    onObjectRetainedListeners.forEach { it.onObjectRetained() }
  }
}
private fun removeWeaklyReachableObjects() {
  // 从ReferenceQueue中拿出对象,然后从Map中移除
  var ref: KeyedWeakReference?
  do {
    ref = queue.poll() as KeyedWeakReference?
    if (ref != null) {
      watchedObjects.remove(ref.key)
    }
  } while (ref != null)
}

moveToRetained主要是从ReferenceQueue中找出弱引用对象,然后移除Map中相应的弱引用对象。弱引用+ReferenceQueue的使用,应该不用多说吧,如果弱引用持有的对象被回收,弱引用会添加到ReferenceQueue中。所以watchedObjects代表的是应该将要被回收的对象,queue表示已经被回收的对象,这步操作就是从queue中找出已经回收的对象,然后从watchedObjects移除相应的对象,剩下的的就是应该被回收却没被回收的对象。如果对象被正常回收,那这整个流程就走完了,如果没被回收,会执行到onObjectRetained(),之后就是Dump操作了,之后的就是内存分析、弹出通知那堆操作了,去分析内存的泄漏这些,因为内容比较多,这篇先大概就先到这里。

总结

浅析,就是只做了简单分析LeakCanary的整个工作过程和工作原理。

原理就是用弱引用和ReferenceQueue去判断应该被回收的对象是否已经被回收。大致的工作流程是:监听Activity、Fragment和Fragment的View、RootView、Service对象的销毁,然后将这些对象放入“应该被回收”的容器中,然后5秒后通过弱引用和ReferenceQueue去判断对象是否已被回收,如果被回收则从容器中删除对应的对象,否则进行内存分析。

至于是如何判断不同对象的销毁和如何分析内存情况找出泄漏的引用链,这其中也是细节满满,但是我个人LeakCanary应该是看过两三次源码了,从一开始手动初始化,到旧版本java的实现方式,到现在用kt去实现,能发现它的核心思想其实是一样的,只不过在不断的优化一些细节和不断的扩展可以监测的对象。

以上就是详解LeakCanary分析内存泄露如何实现的详细内容,更多关于LeakCanary分析内存泄露的资料请关注脚本之家其它相关文章!

相关文章

  • Android颜色处理SweepGradient扫描及梯度渲染示例

    Android颜色处理SweepGradient扫描及梯度渲染示例

    这篇文章主要为大家介绍了Android颜色处理SweepGradient扫描渲染及梯度渲染示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06
  • Android中常用的XML生成方法实例分析

    Android中常用的XML生成方法实例分析

    这篇文章主要介绍了Android中常用的XML生成方法,以实例形式较为详细的分析了Android生成XML的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-10-10
  • Android数据库greenDAO配置与使用介绍

    Android数据库greenDAO配置与使用介绍

    这篇文章主要介绍了Android集成GreenDao数据库,使用数据库存储时候,一般都会使用一些第三方ORM框架,比如GreenDao,本文分几步给大家介绍Android集成GreenDao数据库的方法,需要的朋友可以参考下
    2023-03-03
  • Android开发中在TableView上添加悬浮按钮的方法

    Android开发中在TableView上添加悬浮按钮的方法

    如果直接在TableVIewController上贴Button的话会导致这个会随之滚动,下面通过本文给大家分享在TableView上实现位置固定悬浮按钮的两种方法,对tableview 悬浮按钮感兴趣的朋友一起学习吧
    2016-11-11
  • Android TextView多文本折叠展开效果

    Android TextView多文本折叠展开效果

    这篇文章主要为大家详细介绍了Android TextView多文本折叠展开效果的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10
  • 基于Android引入IjkPlayer无法播放mkv格式视频的解决方法

    基于Android引入IjkPlayer无法播放mkv格式视频的解决方法

    下面小编就为大家分享一篇基于Android引入IjkPlayer无法播放mkv格式视频的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-01-01
  • Android自定义View新年烟花、祝福语横幅动画

    Android自定义View新年烟花、祝福语横幅动画

    这篇文章主要为大家详细介绍了Android自定义View新年烟花、祝福语横幅动画,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • Android自定义组合控件之自定义下拉刷新和左滑删除实例代码

    Android自定义组合控件之自定义下拉刷新和左滑删除实例代码

    最近做了个项目,其中有项目需求,用到下拉刷新和左滑删除,网上没有找到比较理想的解决办法。下面小编给大家分享我的一个小demo有关Android自定义组合控件之自定义下拉刷新和左滑删除实例代码,需要的朋友参考下
    2016-04-04
  • Android即时通讯设计(腾讯IM接入和WebSocket接入)

    Android即时通讯设计(腾讯IM接入和WebSocket接入)

    本文主要介绍了Android即时通讯设计(腾讯IM接入和WebSocket接入),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • Android实现简易登陆注册逻辑的实例代码

    Android实现简易登陆注册逻辑的实例代码

    在android的应用中越来越多的包含了网络互动功能,这就带来了注册,登陆账号功能,这篇文章主要给大家介绍了关于Android实现简易登陆注册逻辑的相关资料,需要的朋友可以参考下
    2021-06-06

最新评论