【开源解码】之LeakCanary简析

769 阅读11分钟

LeakCanary

  • 由Square开源的一款轻量第三方内存泄漏检测工具
  • 原理:watch一个即将要销毁的对象
  • 内存泄漏的根本原因是较长生命周期对象持有了较短生命周期对象的引用导致较短生命周期对象无法被及时回收

LeakCanary是这样做的:

  • 1手动触发GC然后分析强引用的GC引用链
  • 2如果存在GC引用链,说明有内存泄漏,会在你的手机上弹出个提示框
  • 3记录了每一次内存泄漏的GC引用链,通过它可以直接定位到内存泄漏的未释放的对象

常见的内存泄漏分析

  • 单例:如单例持有了ActivityContext,导致Activity无法被及时回收
    • 解决办法:传入ApplicationContext,使单例的生命周期与应用生命周期一致
  • 非静态内部类创建静态实例:
    • 如下:mResource持有了StaticLeakActivity的引用;而mResource的生命周期与应用生命周期一致,导致StaticLeakActivity无法被回收;
    • 解决方式,将非静态内部类修改为静态内部类,这样就不会持有Activity的引用了,Activity可以被及时回收
    class StaticLeakActivity extends Activity{
        private static noneStaticClass mResource = null;
        
        @Override
        protected void onCreate(Bundle savedInstanceState){
            super.onCreate(savedInstanceState);
            if(mResource==null){
                mResource = new noneStaticClass();
            }
        }
        
        private class noneStaticClass{
            
        }
        
    }
    
  • Handler内存泄漏:Handler与Activity生命周期是不一致的,这样很容易导致内存泄漏
    • 如:用handler延迟十分钟发送消息,然后调用finish方法,由于Message持有了handler引用,而handler又持有了Activit引用,消息没有发送完成,于是造成了内存泄漏
    • 解决办法:
      • 1.将Handler声明为静态的,这样其与Activity的生命周期就无关了
      • 2.通过弱引用的方式引入Activity,避免直接将Activity作为Context传入
  • 线程造成的内存泄漏:子线程耗时任务等
    • 解决办法:AsyncTask或者子线程设置为静态的,使其与Activity生命周期不相关,也可以在Activity销毁时调用AsyncTask的cancel()方法
  • WebView内存泄漏:加载复杂网页时webview占用了过多的内存
    • 解决办法:由于系统分配内存是以进程为单位进行划分的,所以可以这样
      • 首先将webview所处的Activity放到单独的进程中
      • 然后调用android.os.Process.killProcess(android.os.Process.myPid())主动杀掉当前进程,这样系统就会自动回收内存

LeakCanary原理

  • 1.Activity DestroyWeakReference之后将它放在一个WeakReference
  • 2.这个WeakReferenceReferenceQueue关联到一个ReferenceQueue
  • 3.查看ReferenceQueue是否存在Activity的引用
  • 4.如果该Activity泄漏了,Dump出heap信息,然后再去分析泄漏路径。

4种引用类型

  • 强引用:不会回收,内存不足时,宁愿抛出错误终止程序都不会回收
  • 软引用:内存足够,不回收;内存不足时,会回收
  • 弱引用:与软引用区别是生命周期更短,只要垃圾回收器扫描到就会回收
  • 虚引用:不会决定一个对象的生命周期,垃圾回收器任何情况下都可以回收它

引用队列ReferenceQueue

  • 软引用/弱引用 都可以与ReferenceQueue联合使用
  • 对象被垃圾回收,Java虚拟机就会把这个引用加入到与之关联的引用队列中。

源码解析

  • 通过LeakCanary.install(this)开启LeakCanary
  • install调用了buildAndInstall创建了一个RefWatcher对象,用于监听Activity内存泄漏
  • RefWatcher会启动一个ActivityRefWatcher,通过ActivityLifecycleCallbacks把Activity的ondestory生命周期关联
    • ActivityRefWatcher在onActivityDestroyed,也就是Activity即将销毁时监控Activity -> ActivityRefwatcher.this.onActivityDestroyed(activity);
    • onActivityDestroyed的内部实现就是refWatcher.watch(activity);
  • 然后在线程池中去开始分析内存泄漏 -> watch
    • ensureGoneAsync:开启异步线程执行,在线程池中执行Runnable
  • watch最终会走到checkForLeak方法:(checkForLeak是LeakCanary中最核心的方法)
    • 首先会把.hprof转为内存快照Snapshot,Snapshot对象中包含所有对象引用的路径,然后就可以查找到内存泄漏的路径
    • 然后会优化gcroots
    • 找出泄漏的对象/找出泄漏对象的最短路径
  • 如何查找内存泄漏引用和最短泄漏路径
    • findLeakingReference:找到内存泄漏引用
      • 1.在snapshot内存快照中找到第一个弱引用,因为对象没有被回收,所以第一个弱引用就是内存泄漏的对象
      • 2.遍历这个对象的所有实例
      • 3.如果key值和最开始定义封装的key值相同,那么返回这个泄漏对象
    • findLeakTrace:找到最短泄漏路径
      • 1.解析hprof文件,把这个文件封装成snapshot
      • 2.根据弱引用和前年定义的key值,确定泄漏的对象
      • 3.找到最短泄漏路径,最为结果反馈出来

面试相关

Application应用场景

  • 1.初始化全局对象、环境配置变量
  • 2.获取应用程序当前的内存使用情况
  • 3.监听应用程序内所有Activity的生命周期
  • 4.监听应用程序配置信息的改变
  • onTrimMemory回调方法:会根据内存使用情况(7个不同内存级别)进行自身内存资源和年不同程度的释放
  • onLowMemory:监听系统内存情况,开发者可以在onTrimMemory和onLowMemory中做一些内存优化的工作
  • onTerminate:应用程序结束会回调
  • onConfigurationChanged:配置信息改变时回调,如屏幕旋转

Android性能数据上报

1. 性能解决思路
  • 首先要量化性能指标,只有这样,才能知道如何去修改代码
  • 监控性能情况
  • 根据上报统计信息
  • 持续监控并观察
2. 应用性能种类
  • 资源消耗:电量,流量
  • 流畅度:如帧率,关系到用户体验
3. 各个性能数据指标
  • 网络请求流量
    • 什么是流量?通过运营商的网络访问Internet,运营商替我们的手机转发数据报文,数据报文的总大小(字节数)即流量
    • 解决办法:
      • 1.在日常开发中可以通过tcpdump+Wireshark抓包测试
      • 2.安卓sdk提供的TrafficStats类进行获取
  • 冷启动
    • 与热启动区别:进程是否被杀死
    • 冷启动时间获取:
      • 命令行adbshell am start -W packagename/MainActivity
      • 日志打印:起点->终点 Application的onCreate->ActivityOncreate加载完成
  • 卡顿
    • fps:帧率
      • 通过Choreographer设置framecallback,记录每一帧渲染的时间从而判断是否存在掉帧情况
      • VSYNC信号:每1/60秒发送一个信号,如果两次doFrame的时间差超过了16.6ms,就意味着卡帧了
      • 流畅度:实际帧率/理论帧率,美团腾讯等都有类似的性能指标
      • 由于Choreographer监测帧率时本身也消耗性能,加上这种方式并不能准确判断堆栈信息,所以主流的衡量方案方案是依据主线程消息处理时长
    • 主线程消息处理时长:ActivityThread
      • 通过监控主线程的消息处理时间
      • 主线程消息前后都会用logging打印消息,如果大于卡顿的阈值,说明产生了卡顿
      • 根据设备不同设定不同的阈值,依据具体开发而定
  • 内存占用:
    • RAM: 物理内存

    • PSS: 应用占用的实际物理内存

    • heap:虚拟机堆内存,与代码好坏有直接关系,从代码角度可以去优化

Blockcanary

  • 非侵入式的性能监控组件
  • UI卡顿问题:
    • 准则:尽量保证每次在16ms内处理完所有的CPU与GPU计算、绘制、渲染等操作,否则会造成丢帧卡顿问题
    • 原因:
      • 1.UI线程中做了轻微耗时操作
      • 2.布局Layout过于复杂,无法在16ms内完成渲染
      • 3.View过度绘制,导致cpu或gpu负载过重
      • 4.view频繁的触发measure、layout
      • 5.内存频繁触发gc过多,gc会暂停所有线程
  • UI线程:ActivityThread,把时间分发给合适的view或者widget
  • 子线程操作:
    • handler
    • Activity.runOnUiThread(Runnable)
    • View.post(Runnable)
    • View.postDelayed(Runnable, long)

使用

//gradle配置
implementation 'com.github.markzhai:blockcanary-android:1.5.0'
//Application中注册
BlockCanary.install(this,new BlockCanaryContext()).start();

核心实现原理

install方法

  • 主要负责成员变量初始化工作
  • BlockCanaryInternals是核心类,在onBlockEvent回调中打印了block信息

start方法

blockcanary图3

面试相关

  1. ANR造成原因及解决办法等
    • Application Not Responding
    • 响应速度由ActivityManager和WindowManager系统服务进行监控的
    • 分类:
      • Service Timeout : 服务20s之内没有执行
      • BroadcastQueue Timeout :前台广播10s内没有执行完成
      • inputDispatching Timeout: 输入事件超过5s没有执行,包括触屏按键等事件
    • 原因:
      • 主线程中做耗时操作,如数据库数据的读取
      • 主线程被其他线程锁住,造成主线程阻塞
      • cpu被其他进程占用
    • 解决办法:
      • sharedPreference的commit()/apply()方法,apply()是异步的,尽量使用apply
      • 不要在broadCastReceiver的onReceive()方法中做耗时操作
      • Activity的生命周期函数中都不应该有太耗时的操作
  2. WatchDog-Anr是如何监控anr的
    • 使用:new ANRWatchDog().start();
    • 继承于Thread,也就是说它也是一个线程
    • 原理:
      • 创建一个监测线程
      • 该线程不断往UI线程post一个任务
      • 睡眠固定时间
      • 等该线程重新起来后检测之前post的任务是否执行了
  3. new Thread开启线程的4点弊端
    • start和run方法
      • start表示开启线程,下面的代码可以执行,当前是多线程状态,线程处于就绪状态,并不一定真正运行了,告诉cpu可以分配时间片运行
      • run方法相当于普通方法的执行,下面的方法需要等待run执行完毕才会执行,相当于不是多线程
    • 四点弊端
      • 1.多个耗时任务时就会开多个新线程,开销是非常大
      • 2.如果在线程中执行循环任务,无法停止,只能通过一个Flag来控制它的停止
      • 3.没有线程切换的接口,只能通过handler或者其他线程通信的方式
      • 4.如果从UI线程启动,则该线程优先级默认为Default,通过Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND) 设定线程优先级,降低其优先级为BACKGROUND
  4. 线程间通信:子线程--UI线程
    • 多线程编程时有两大原则:
      • 第一、不要阻塞UI线程
      • 第二、不要在UI线程之外访问UI组件
    • 线程通信两种方式:
      • 第一、将任务从工作线程抛到主线程
      • 第二、将任务从主线程抛到工作线程
    • 4.1 将任务从工作线程抛到主线程
      • 子线程hander.sendMessage,主线程handleMessage
      • 子线程handler.post(runnable),将消息抛到handler所在的主线程中去执行,上面这两种实质上都是通过消息入队和handler的消息处理
      • activity.runOnUiThread(runnable), 把更新UI的代码放在runnable中即可
      • AsyncTask:onPostExecute发送,注意使用时要预防内存泄漏,注意要声明为静态的,3.0以后AsyncTask是串行的
    • 4.2 将任务从主线程抛到工作线程
      • Thread/Runnable:不建议
        • Runnable缺点:作为匿名内部类还持有了外部类的引用,可能引发内存泄漏
      • HandlerThread:
        • 1.继承于Thread
        • 2.有自己的内部Looper对象,通过Looper.loop()进行looper循环
        • 3.HandlerThread的looper对象传递给Handler对象,然后在handleMessage()方法中执行异步任务
        • 线程优先级:(高)-20 ~ 19(低)
        • quit和quitSafely:
          • quit:调用looper.quit()清空measageQueue消息
          • quitSafely:只会清空延迟消息,非延迟消息会分发出去
      • IntentService:
        • 1.IntentService是Service类的子类
        • 2.单独开启了一个线程来处理所有的Intent请求所对应的任务
        • 3.当IntentService处理完所有的任务后,它会在适当的时候自动结束服务
  5. 多进程的4点好处与问题
    • 好处:
      • 解决OOM问题,安卓内存分配以进程为单位
      • 合理利用内存
      • 单一进程崩溃不会影响整体使用
      • 利于项目解耦、模块化开发
    • 问题:
      • Application会多次创建;解决办法,不同进程进行不同的初始化
      • 文件读写潜在的问题:在java中,文件锁基于进程和虚拟机级别
      • 静态变量和单例模式完全失效
      • 线程同步机制完全失效:不同进程有不同的虚拟机
  6. synchronized和volatile区别
    • 1.阻塞进程与否:
      • volatile关键字告诉虚拟机当前变量在寄存器中的值是不确定的,需要从主存中去获取,不会阻塞线程,单例中使用较多
      • synchronized:只有当前线程可以访问,其他线程被阻塞
    • 2.使用范围:
      • volatile:变量
      • synchronized:变量和方法
    • 3.原子性:
      • volatile:不具备原子性
      • synchronized:具备原子性
    • 单例比较推荐的写法,volatile结合synchronized
      class SingleTon{
          private static volatile SingleTon instance = null;
          private SignleTon(){}
          public static SingleTon getInstance(){
              if(instance==null){
                  synchronized(SingleTon.class){
                      if(instance==null){
                          instance = new SingleTon();
                      }
                  }
              }
              return instance;
          }