软件编程
位置:首页>> 软件编程>> Android编程>> 详解LeakCanary分析内存泄露如何实现

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

作者:流浪汉kylin  发布时间:2022-12-03 22:04:40 

标签: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分析内存泄露如何实现

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

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

使用

先看看它要监测什么,因为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

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

监听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的

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

最终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的。

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

这边因为只是做浅析,不是源码详细分析,所以我这边就不去一个个分析是如何调用到销毁的这个方法的,我们通过上面的方法得到一个结论,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里面的是写得简单,但是不熟的话可以先别管怎么执行的,只要先知道反正执行到这个地方就行)

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

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

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

写到这里我感觉有点慌了,因为如果不熟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操作了,之后的就是内存分析、弹出通知那堆操作了,去分析内存的泄漏这些,因为内容比较多,这篇先大概就先到这里。

来源:https://juejin.cn/post/7167026728596946980

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com