美文网首页
LeakCanary原理解析

LeakCanary原理解析

作者: 艾瑞败类 | 来源:发表于2023-04-02 14:50 被阅读0次

    作者:左大侠

    LeakCanary,由Square开源的一款轻量第三方内存泄露检测工具。能够在不影响程序正常运行的情况下,动态收集程序存在的内存泄露问题。小的内存泄露可能不会直接导致程序崩溃,但随着数量增多,量变引起质变,造成内存溢出,程序崩溃。由于LeakCanary功能强大且部署简单的特点,深受大家喜爱。

    简单使用

    新版本2.x相比1.x的区别不仅仅是开发语言改为kotlin,也不需要手动进行初始化了,只需要在主模块中的build.gradle中添加如下依赖,即可完成初始化。

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

    通过这里的引用方式可见,LeakCanary只对debug版本apk起作用,考虑到其对内存的检测就很消耗系统资源,默认只允许debug包使用也很正常,当然,不仅仅是这单个原因,后续会细讲。添加完依赖再运行程序后,就会同时运行leakCanary。退出后,手机桌面会自动生成leakcanary的图标。此时点开图标,如果有内存泄露情况发生,里面就会提示相应信息。界面如下:

    为什么会产生内存泄露

    · 长生命周期的对象持有了短生命周期的引用导致本应该被回收的内存无法回收。为了维持多任务环境的正常运行,Android 系统会为每个应用的堆大小设置硬性上限。随着内存泄露的不断累积,APP会逐渐消耗完内存,导致内存溢出,最终引发OOM。

    常见内存泄露场景

    单例

    public class SingletonActivityContext {
        private static SingletonActivityContext instance;
        private Context context;
        private SingletonActivityContext(Context context){
            this.context = context;
        }
        public static SingletonActivityContext getInstance(Context context){
            if (instance == null ){
                instance = new SingletonActivityContext(context);
            }
            return instance;
        }
    }
    

    单例的static变量由于static特性,使得其生命周期跟我们应用生命周期是一样长的,这时候如果使用不当,很容易造成内存泄漏。例如上述代码,如果传入Context是Activity的话,则当Activity退出的时候,其内存并不会被回收。因为代码中的单例对象持有了activity的引用,会导致activity想被内存回收的时候无法被回收。其解决方式,就是注意传入Context是Application即可:

    这样单例的生命周期跟我们应用的生命周期一样长,就不会有内存泄露问题。

    非静态内部类创建静态实例造成的内部泄露

    public class StaticLeakActivity extends Activity {
        public static innerStaticClass mData = null;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            if (mData == null){
                mData = new innerStaticClass();
            }
        }
        private class innerStaticClass{
    
        }
    }
    

    例如,上述代码,可以避免innerStaticClass对象的重复创建,但这样也会造成内存泄漏。因为在这里,innerStaticClass默认会持有外部StaticLeakActivity的引用,由于其被static修饰,导致innerStaticClass这个变量的生命周期跟我们应用的生命周期一样长,这就致使StaticLeakActivity想被内存回收的时候无法被回收。

    Handler

    handler造成内存泄漏的场景非常普遍,很多时候,我们为了避免ANR,不在主线程做耗时操作,这时候处理网络任务或者处理网络回调的时候,我们都需要借助handler来处理。handler内部跟Message、MessageQueue相互关联在一起,万一handler发送消息的时候,Message没有被处理完成,这个Message以及发送该Message的对象就将被我们的线程所一直持有,这里的handler实际上就是个TLS变量(生命周期与整个Activity生命周期不一致),因此这种实现方式很难保证跟Activity生命周期一致,这样就很容易导致内存泄漏。

    public class HandlerLeakActivity extends Activity {
        private final Handler mLeakHandler = new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
            }
        };
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mLeakHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                }
            },1000*60);
            finish();
        }
    }
    

    对应解决方法很简单:
    1、将Handler声明为静态的(避免在Activity内使用静态类),这样Handler的生命周期就与activity生命周期无关了;
    2、通过弱引用的方式引入Activity,在Handler内部持有外部类HandlerLeakActivity弱引用,避免将Activity作为Context直接传进去。

    线程所造成的内存泄漏

    在平时开发过程中,线程造成的内存泄漏是常见的,因为平时开发工作中,经常会有开启线程操作,如果这个时候你定义了其中的AsyncTask或者Runnable,定义成非静态内部类的话,则当Activity想要被回收的时候,由于线程任务持有了Activity的隐式引用,会使得Activity被销毁的时候因为任务没有完成而导致无法被回收,由此导致内存泄漏。

    public class ThreadLeakActivity extends Activity {
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            testThreadLeak();
        }
    
        private void testThreadLeak() {
            new Thread((Runnable) () -> {
                SystemClock.sleep(1000);
            }).start();
        }
    }
    

    解决方式:可以将Runnable定义为静态内部类,这样可以避免Activity内存资源泄漏,同时,可以在onDestory销毁时,调用AsyncTask的cancel()等。

    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            SystemClock.sleep(1000);
        }
    }
    

    此情况与上述Handler造成的内存泄漏情况类似,原因一样,因此都是把Handler或者Runnable定义成静态内部类,从而不持有外部类的引用,这样就能使我们的外部类Activity被回收。

    WebView

    webView造成内存泄漏的原因也很简单:webView内部的一些线程持有activity对象,导致activity无法释放。反映在正常用户的操作上就是,用户反复进出webView页面,页面占用内存不断升高,最终触发GC。 有以下两种避免方式:

    一、不在 xml 中定义 Webview(这样会引用 Activity),而是在需要的时候在 Activity 中创建,并且上下文对象使用 getApplicationgContext();

    二、给使用WebView的Activity单独开启一个新进程,通过AIDL来做数据交互。

    检测内存泄漏的方案

    有问题,就有解决方法。对于内存泄漏问题,也是八仙过海、各显神通。字节有Liko、快手有KOOM,也有人单独使用profile等工具。而LeakCanary就是众多方案中的一种:

    LeakCanary: It automatically watches destroyed activities and fragments, triggers a heap dump, runs Shark Android and then displays the result.
    

    LeakCanary的图标是一只鸟,也是其单词的直接含义---金丝雀(金丝雀对有害气体具有一定程度的敏感性,因此常常在矿场里检测矿井中气体)。

    启动时机

    LeakCanary在更新至2.x版本后,最大的一个不同就是不用在Application的onCreate中对其进行初始化处理了。

    观察其Manifest.xml文件可见端倪,此处有一个ContentProvider注册。对应ContentProvider为:

    ContentProvier 一般会在 Application 被创建之前被加载,LeakCanary 在其 onCreate() 方法中调用了 AppWatcher.manualInstall(application) 进行初始化。这种方式的确是方便了开发者,但是仔细想想弊端还是很大的,如果所有第三方库都如此操作,开发者就没法控制应用启动时间了。现在很多APP为了用户体验对启动时间有严格限制,包括按需延迟初始化第三方库。但在 LeakCanary 中,这个问题并不存在,因为它本身就是一个只在 debug 版本中使用的库,并不会对 release 版本有任何影响。(这里知道为什么只允许debugImplementation了吧)

    Watcher和Activity的监测时机

    在这里AppWatcher.manualInstall(application)调用的是InternalAppWatcher.install(application),并在其中进行初始化:

    在这里,①是判断是否是在主线程中调用,如果不是就会抛出异常;②负责监听Activity的onDestory();③负责监听Fragment的onDestory()。

    这里,我们先来看下ActivityDestroyWatcher.install()的源码:

    可见application.registerActivityLifecycleCallbacks()在这里是注册 Activity 生命周期监听,

    而对应的lifecycleCallbacks中,则是监听到 onDestroy() 之后,通过 objectWatcher监测 Activity。

    如此,可以说,install() 方法中注册了 Activity 生命周期监听,在监听到 onDestroy() 时,调用 objectWatcher.watch() 开始监测 Activity。

    到这里了,可以发现,下一步的突破方法在watch()。我们来观察一下此方法:

    在这里,①removeWeaklyReachableObjects()是把gc前ReferenceQueue中的引用清除;
    ②KeyedWeakReference()是将activity等包装为弱引用,并于ReferenceQueue建立关联;
    ③5s之后进行检测(5秒内gc完成)。

    要注意的是③中的checkRetainedExecutor是传入参数,此处由InternalAppWatcher.kt中的checkRetainedExecutor做传入参数:

    可见,5秒钟是这么来的。继续观察③中的moveToRetained():

    5秒时间内,清除所有弱引用对象,进行gc操作:

    如果gc操作完成,则上述变量watchObjects肯定清空,则retainedRef必定为null,如果没有清空,则触发内存泄漏处理。(当然5秒内也不一定会触发gc,所以之后的内存泄漏处理会主动gc再判断一次)

    这里的onObjectRetained()实现代码在InternalLeakCanary中:

    这一块的逻辑是确认是否存在内存泄漏,

    可以看到,调用 checkRetainedCount() 判断当前泄露实例个数如果小于 5 个,则给用户一个通知,不会进行heap dump操作,并在 5s 后再次发起检测(所以,当泄露引用到达 5 个时才会发起heap dump)ReferenceQueue。这里最终会执行到HeapDumpTrigger中的dumpheap():

    这里的主要流程就是dump hprof文件,然后开启HeapAnalyzerService来分析heap文件。

    很明显,这里我们要先观察heapDumper.dumpHeap()中的内部实现:

    通读方法可以看出其逻辑是发出特定的Notification,同时:

    通过Debug.dumpHprofData(heapDumpFile.absolutePath)来dump hprof文件,至此完成dump heap流程,再回到最初dumpHeap()中,完成dump heap后,就会调用HeapAnalyzerService的runAnalysis()来分析heap文件。

    可见,这里在服务中通过序列化的方式传输文件对象,最后在analyzeHeap()中来进行分析:

    而analyzeHeap()则又是调用了HeapAnalyzer的analyze():

    此方法中又是通过ConstantMemoryMetricsDualSourceProvider()来读取hprof文件,

    然后调用FindLeakInput的analyzeGraph()进行分析:

    在这里leakingObjectFinder.findLeakingObjectIds(graph)是从hprof中获取泄漏的对象id集合,在这主要是收集没有被回收的弱引用。随后通过findLeaks()来判断这些没有被回收的弱引用有无内存泄漏。

    其方式是通过计算到gcroot的最短引用路径,来判断是否发生泄漏。

    最终通过调用buildLeakTraces()来构建包含引用链信息的LeakTrace,呈现分析效果。具体分析的其他细节,这里就不赘述了。这里的所有内存分析使用了Square的shark库(主要逻辑在 moudle leakcanary-analyzer 中),大致流程就是读取hprof文件,找到之前标记的泄漏对象,寻找gcroot,然后保存分析结果至数据库,点击对应通知跳转后便可看到对象数据的展示。上述检测流程是关于Activity的,而LeakCanary不仅可以检测Activity,还可以检测Fragement(AndroidX包下)、ViewModel、RootView、Service对象。

    Fragment的监测时机

    在最开始的install()中,③处开始便是对Fragment的检测:

    观察其FragmentDestroyWatcher.install()流程会发现最终会看到AndroidOFragmentDestroyWatcher:

    最终在 onFragmentViewDestroyed()和 onFragmentDestroyed()中分别将 Fragment中的View 和 Fragment 加入 watchedObjects 里面等待检测。

    ViewModel的检测时机

    关于ViewModel的检测时机,则要关注ViewModelClearedWatcher。ViewModelClearedWatcher继承自ViewModel,其本身是一个ViewModel,且实现了onCleared(),这个方法类似Activity的onDestroy(),在ViewModel销毁的时候会执行。

    观察代码不难发现,当ViewModelClearedWatcher创建的时候,通过反射拿到宿主的mMap,这样便得到宿主中所有的ViewModel。当ViewModelClearedWatcher被销毁时,会回调onCleared(),这时会遍历宿主中所有的ViewModel,执行reachabilityWatcher的expectWeaklyReachable方法,判断ViewModel是否都已经释放。

    RootView的检测

    需要注意的是,这里是RootView,即根View,不是所有View。LeakCanary默认检测所有的根View。具体代码就不贴了,都是一个套路。通过监听View的OnAttachStateChangeListener销毁监听,当View和Window绑定和取消绑定的时会回调此方法。

    流程图

    以Activity的检测流程为例,其流程图如下:

    总结一下

    总结一下,LeakCanary流程中最经常被问到的两个问题:

    1、如何确定内存泄漏对象 WeakReference,其双参数构造函数支持传入一个ReferenceQueue,当其关联的对象回收时,会将WeakReference加入ReferenceQueue中。LeakCanary继承ReferenceQueue,将每个需要监测的对象WeakReference加入一个map中。在GC过后,通过removeWeaklyReachableObjects()遍历ReferenceQueue,通过key值删除map中已回收的对象,剩下的就可以确定发生了内存泄露。

    2、如何确定从GCroot到泄漏对象的引用链 在确定内存泄露的对象后,checkRetainedObjects(),启动前台服务HeapAnalyzerService,这时候就能在通知中看到了。而HeapAnalyzerService中则是调用了HeapAnalyzer的analyze方法进行堆内存分析,此功能由Shark库实现。

    相关文章

      网友评论

          本文标题:LeakCanary原理解析

          本文链接:https://www.haomeiwen.com/subject/mhatddtx.html