Leakcanary原理解析以及换肤框架skin的原理分析

一、错误现场
 java.lang.ClassCastException: androidx.appcompat.widget.TintContextWrapper cannot be cast to android.app.Activity

 	at leakcanary.internal.navigation.ViewsKt.getActivity(Views.kt:16)

 	at leakcanary.internal.activity.screen.LeaksScreen.onGroupsRetrieved(LeaksScreen.kt:40)

 	at leakcanary.internal.activity.screen.LeaksScreen.access$onGroupsRetrieved(LeaksScreen.kt:23)

 	at leakcanary.internal.activity.screen.LeaksScreen$createView$$inlined$apply$lambda$1$1.invoke(LeaksScreen.kt:35)

 	at leakcanary.internal.activity.screen.LeaksScreen$createView$$inlined$apply$lambda$1$1.invoke(LeaksScreen.kt:23)

 	at leakcanary.internal.activity.db.Io$execute$2$1.invoke(Io.kt:52)

 	at leakcanary.internal.activity.db.Io$execute$2$1.run(Io.kt:12)

发生错误的代码:

// LeakCanary中Views.kt文件中的扩展属性
internal val View.activity
  get() = context as Activity

突如其来的Bug,让我措手不及。好好滴,leakcanary怎么会crash了?事出蹊跷必有妖啊,虽然crash发生在Leakcanary,
作为一个正义的程序猿,也不能把锅甩给Leakcanary。所以,为了分析这个问题,我趁机把leakcanary源码撸了一遍。原来发现是换肤框架利用

二、Leakcanary原理抽丝剥茧

作为一个Android程序猿,想必一定使用过Leakcanary这个三方库,他可以很方便的帮我们发现内存泄漏的问题。那么为了方便我们快速的抓住核心内容,我们从问题出发来剖析:

  • 问题一:Leakcanary如何检测和获取堆转储文件?

我们知道新版本的LC(为方便打字,以下用LC代替Leakcanary的拼写)是不需要初始化的,是通过ContentProvider来初始化的,当我们的应用启动的时候再Application的onCreate被调用之前,ContentProvider的onCreate会先调用,LeakCanary正好利用的这个机制,下面我们先看下LeakCanary监控内存泄漏以及如何获得堆转储文件
在这里插入图片描述

上面的流程是LeakCanary的整个方法调用的细节,我们不需要记忆的,这里写出来只是供大家参考的,其中关键的几个步骤我用绿色的背景标识了。

第一个LeakCanary初始化的机制,这个上面介绍过;

第二个LeakCanary如何判断去评判一个页面等是否有内存泄漏,通过注册一个生命周期的回调,当页面调用到onDestroy时,也就是页面应该可以被销毁时,这个时机去做接下来的一些列判断;

第三个利用了当一个弱引用的对象编程弱可达对象,就会被放到队列里,这个时候我们通过将队列中的数据取出来,如果有值,说明对象已经是弱可达了,也就不会存在内存泄漏了,那我们就将其从被监测的对象列表中删除,否则就认为对象可能存在内存泄漏;

第四个关键的是通过调用GC,来强制GC一遍,但是我们知道**System.gc()**这个API是不能保证垃圾回收马上开始的,所以可以使用这个办法确保进行垃圾回收:

    override fun runGc() {
    
    
      // Code taken from AOSP FinalizationTest:
      // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
      // java/lang/ref/FinalizationTester.java
      // System.gc() does not garbage collect every time. Runtime.gc() is
      // more likely to perform a gc.
      Runtime.getRuntime()
        .gc()
      enqueueReferences()
      System.runFinalization()
    }

    private fun enqueueReferences() {
    
    
      // Hack. We don't have a programmatic way to wait for the reference queue daemon to move
      // references to the appropriate queues.
      try {
    
    
        Thread.sleep(100)
      } catch (e: InterruptedException) {
    
    
        throw AssertionError()
      }
    }

第五个我们关心的是如何获取到堆信息并转储成文件呢(最关键的一行代码),那么我们可以使用Android系统的API如下:

Debug.dumpHprofData(heapDumpFile.absolutePath)

到这里拿到堆转储文件就是完成了我们的监控和收集的工作,接下来就是对文件的处理,以及怎样将处理后的信息通过页面清晰的展示出内存泄漏的原因。

早期的LeakCanary使用的是haha的库进行分析的,新版的LeakCanary使用的是shark库,进行分析的,那么这里就不再介绍shark是如何进行分析堆转储文件的。

三、换肤框架skin原理抽丝剥茧

换肤框架的一般思路是借助于Factory2来完成自定义inflate的。
为什么可以这么做?我们得先分析下当我们在Activity中通过setContentView()设置布局时,系统是如何创建对应的View的,下面是简化的时序图:
在这里插入图片描述

图中画的是系统会优先使用Factory2来创建View,但是默认情况下Factory2是空的,并没有实现。所以这个实现逻辑就提供给开发者一个可以自定义这个创建过程的逻辑,而换肤框架skin也是利用这一点来实现的。

在这里插入图片描述

skin换肤框架通过注入一个Factory2实例,从而改变系统默认的创建View的过程,再来看下skin框架实现Factory2接口的类:SkinCompatDelegate
在这里插入图片描述

所以使用换肤框架时,一些类型的View创建时传入的Context对象是使用TintContextWrapper包装后的实例,所以这时候View的getContext返回的对象是context的一个包装类实例,这个对象当然是不能强制转换成Activity的。所以才会出现文章开头出现的这个问题。

到这里,问题的根源就找到了,那如何解决呢,一种思路是,skin框架提供通过注解来声明哪些页面需要实现换肤的逻辑,默认是所有的Activity,所以我们只要在我们的基类里加注解(注解要稍微修改一下,改成可继承的),这个时候LeakCanary的页面就不会走换肤框架注入的逻辑,就能恢复正常了。

文章推荐:
史上最有趣的HTTPS漫画趣谈
通俗易懂的HashMap源码分析

Guess you like

Origin blog.csdn.net/codeyanbao/article/details/121407039