0%

卡顿检测 - BlockCanaryEx浅析

BlockCanaryEx检测主线程卡顿的原理是什么?

给Looper设置自定义的Printer,Looper的loop()方法在处理一个消息的前后会调用Printer的println()方法,通过这个方法就可以计算主线程处理每一个消息的耗时。

这一原理与BlockCanary相同,不同的是两者采集的信息不同。

BlockCanaryEx与BlockCanary有什么区别?

  1. BlockCanaryEx的运行时代码修改自BlockCanary,ui和大部分功能基本一致;
  2. BlockCanaryEx添加了方法采样,知道主线程中所有方法的执行时间和执行次数;
  3. 当应用卡顿时,BlockCanaryEx更关注app代码中,哪些方法耗时最多,重点记录和显示这些耗时方法;
  4. 添加了gc采样,当应用卡顿时,我们可以知道卡顿时是否发生了gc,以及gc的时间;
  5. 监控view性能,计算卡顿时,view的measure,layout,draw消耗的时间。

BlockCanaryEx在编译期,通过字节码注入,将方法采样器注入到你的代码中

入口在哪?

BlockCanaryEx.install() -> BlockMonitor.install()

BlockMonitor.install() 主要调用了

  1. BlockMonitor.ensureMonitorInstalled() -> Looper.getMainLooper().setMessageLogging(LOOPER_MONITOR)
  2. connectServiceIfNot()

LooperMonitor里做了什么?

实现了Printer接口的println()方法

在接收到Looper的第一个消息时,调用一次ViewPerformanceSampler.install()

在api level为[20,25]之间(Android 4.4 到 Android7.1)hook掉android.os.Trace的traceBegin和traceEnd方法,替换为ViewPerformanceSampler的traceBegin和traceEnd方法

为什么要hook掉android.os.Trace的traceBegin和traceEnd方法?

android framework里已经使用Trace的traceBegin和traceEnd方法在每一个关键处进行了打点记录,hook了这两个方法,就相当于给framework的每个关键动作的地方都做了耗时统计

这一点去查看android framework源码查看Trace类里各个常量的使用位置就知道,例如TRACE_TAG_VIEW、TRACE_TAG_INPUT等

例如:

  1. LayoutInflater.inflate()方法一开始会调用Trace.traceBegin(Trace.TRACE_TAG_VIEW, “inflate”)
  2. ViewRootImpl的performMeasure()中会调用Trace.traceBegin(Trace.TRACE_TAG_VIEW, “measure”);

如何hook掉android.os.Trace的traceBegin和traceEnd方法?

热修复框架 AndFix 原理:

Java层的每一个方法在虚拟机实现里面都对应着一个ArtMethod的结构体,只要把原方法的结构体内容替换成新的结构体的内容,在调用原方法的时候,真正执行的指令会是新方法的指令。

BlockCanaryEx这里采用的是 native 替换方法结构体 的思路,源码使用的Epic的代码

https://github.com/tiann/epic

Epic 是一个在虚拟机层面、以 Java Method 为粒度的 运行时 AOP Hook 框架。简单来说,Epic 就是 ART 上的 Dexposed(支持 Android 4.0 ~ 10.0)。它可以拦截本进程内部几乎任意的 Java 方法调用,可用于实现 AOP 编程、运行时插桩、性能分析、安全审计等。

具体分析见Epic源码分析专题页

参考资料:

在api level为[20,25]之间(Android 4.4 到 Android7.1)进行了hook,为什么是这个版本区间?

因为ART是Android 4.4上才有的,支持到Android

ViewPerformanceSampler的traceBegin和traceEnd方法做了什么?

traceBegin()记录了traceTag为TRACE_TAG_VIEW的Trace事件的名称和事件开始事件

traceEnd()中根据Trace的事件名称记录一个事件type,再把事件开始时间和结束时间,一起存入一个新创建的ViewPerformanceInfo对象中,再把这个对象存入一个名为VIEW_PERFORMANCE_INFOS的List中

ViewPerformanceSampler的VIEW_PERFORMANCE_INFOS什么时候被使用?

在LooperMonitor中检测到Looper处理完一个消息后,取出ViewPerformanceSampler的VIEW_PERFORMANCE_INFOS的一个副本,并清空VIEW_PERFORMANCE_INFOS,这个List的副本就表示Looper处理一个消息期间View相关操作的耗时信息

通过Config.isBlock()判断是否发生阻塞,发生阻塞则会将List通过LooperMonitor.BlockListener的onBlockEvent()传播出去

BlockMonitor.install()中调用connectServiceIfNot()是在做什么?

通过bindService绑定BlockSamplerService,获取ISamplerService代理对象

BlockSamplerService运行在单独的进程中

BlockSamplerService是干什么的?

主要是提供ISamplerService对象,事情都在ISamplerService中处理

ISamplerService的方法在哪些地方调用?

在BlockMonitor的BLOCK_LISTENER的方法中使用,BLOCK_LISTENER是LooperMonitor.BlockListener一个实例

LooperMonitor.BlockListener的onStart()会在Looper处理一个消息前调用

onStart()内部会调用ISamplerService的resetSampler()

LooperMonitor.BlockListener的onBlockEvent()会在Looper处理一个消息后检测到发生了阻塞时调用

onBlockEvent()中会调用ISamplerService的getCurrentCpuInfo(realStartTime, realEndTime)和popGcInfoBetween(realStartTime, realEndTime)

ISamplerService的resetSampler()做了什么?

三行代码:

1
2
3
GcSampler.startIfNot(pid);
GcSampler.clearGcInfoBefore(startTime);
CpuSampler.getInstance().resetSampler(pid);

GcSampler.startIfNot(pid)启动了GcSamplerThread类型的线程,无限循环不停的在logcat中获取gc日志,每一行的日志对应一个GcInfo对象,该对象有两个属性

1
2
3
4
public class GcInfo {
private long happenedTime;
private String gcLog;
}

所有获取的gc日志存储在一个类型List<GcInfo>的GC_INFO_LIST列表中

GcSampler.clearGcInfoBefore(startTime)清除了GC_INFO_LIST中时间小于startTime的记录

CpuSampler.getInstance().resetSampler(pid)是清空已采集的CPU信息

ISamplerService的getCurrentCpuInfo(realStartTime, realEndTime)做了什么?

调用了CpuSampler.getInstance().recordSample(),内部调用了doSample(),做的是:

  1. 读取/proc/stat 和 /proc/[pid]>/stat 文件中的信息,拼接为一个String
  2. 把CPU信息String保存在一个LinkedHashMap类型的mCpuInfoEntries变量中,key为执行doSample()时的时间戳,容量满时删除最早插入的元素

然后创建一个CpuInfo对象,cpuRate和isBusy属性,从mCpuInfoEntries中分析得到,CpuInfo对象 作为方法返回值

ISamplerService的popGcInfoBetween(realStartTime, realEndTime)做了什么?

调用了GcSampler.popGcInfoBetween(startTime,endTime)

从GcSampler的GC_INFO_LIST中获取startTime和endTime时间段内的GcInfo列表

为什么要单独开进程做这些事?

读取Logcat可能是非常消耗内存的事情,单独开进程不消耗主进程的内存

检测到阻塞时会做什么?

会调用BlockMonitor.BLOCK_LISTENER的onBlockEvent()方法,内部逻辑主要为:

  1. 从sMethodInfoPool中取出List
  2. 从ISamplerService.getCurrentCpuInfo()取出CpuInfo
  3. 从ISamplerService.popGcInfoBetween()取出List
  4. 将卡顿相关诊断信息封装为一个BlockInfo对象,发送给各个注册的BlockObserver

BlockMonitor的sMethodInfoPool里面存的是什么?是什么时候填充的?

存的是MethodInfo

1
2
3
4
5
6
7
public class MethodInfo {
private String cls;
private String method;
private String paramTypes;
private long costRealTimeNano;
private long costThreadTime;
}

存储了一个方法名、方法所属类、方法参数、方法真实耗时、方法CPU耗时

BlockMonitor的sMethodInfoPool是什么时候填充的?

在BlockMonitor.reportMethodProfile()对sMethodInfoPool进行了元素的添加

BlockMonitor.reportMethodProfile()在哪被调用的?

MethodSampler.onMethodExit()中调用了BlockMonitor.reportMethodProfile()

MethodSampler.onMethodExit()在哪被调用的?

在编译时通过JavaAssist字节码插桩到项目的每一个方法的调用后会调用MethodSampler.onMethodExit(),具体实现在SamplerInjecter类的insertSamplerCode()方法

hook任意方法的原理是什么?

目前热修复框架主要有QQ空间补丁、HotFix、Tinker、Robust等。热修复框架按照原理大致可以分为三类:

  1. 基于 multidex机制 干预 ClassLoader 加载dex
  2. native 替换方法结构体
  3. instant-run 插桩方案

热修复框架 AndFix 原理:

Java层的每一个方法在虚拟机实现里面都对应着一个ArtMethod的结构体,只要把原方法的结构体内容替换成新的结构体的内容,在调用原方法的时候,真正执行的指令会是新方法的指令。

BlockCanaryEx这里采用的是 native 替换方法结构体 的思路,源码使用的Epic的代码

https://github.com/tiann/epic

Epic 是一个在虚拟机层面、以 Java Method 为粒度的 运行时 AOP Hook 框架。简单来说,Epic 就是 ART 上的 Dexposed(支持 Android 4.0 ~ 10.0)。它可以拦截本进程内部几乎任意的 Java 方法调用,可用于实现 AOP 编程、运行时插桩、性能分析、安全审计等。

参考资料:

native层替换方法结构体来实现hook,这个原理是怎么知道的?

通过查看java.lang.reflect.Method的invoke()方法源码得知的

Method.invoke()的函数定义如下:

private native Object invoke(Object receiver, Object[] args, boolean accessible)

最终会调用 jni 层art/runtime/reflection.cc 的 InvokeMethod方法,方法定义如下:

object InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod, jobject javaReceiver, jobject javaArgs, bool accessible)

InvokeMethod 的第二个参数 javaMethod 就是Java层我们进行反射调用的那个Method对象,在jni层反映为一个jobject;InvokeMethod这个native方法首先通过 mirror::ArtMethod::FromReflectedMethod 获取了Java对象的在native层的 ArtMethod指针

AndFix的实现里面,也正是使用这个 FromReflectedMethod 方法拿到Java层Method对应native层的ArtMethod指针,然后执行替换的。

在native层替换的那个 ArtMethod 不是在 Java 层也有对应的东西?

在Method的父类Executable有一个long类型的artMethod属性,实际上就是native层的指针表示。

参考资料: