BlockCanaryEx检测主线程卡顿的原理是什么?
给Looper设置自定义的Printer,Looper的loop()方法在处理一个消息的前后会调用Printer的println()方法,通过这个方法就可以计算主线程处理每一个消息的耗时。
这一原理与BlockCanary相同,不同的是两者采集的信息不同。
BlockCanaryEx与BlockCanary有什么区别?
- BlockCanaryEx的运行时代码修改自BlockCanary,ui和大部分功能基本一致;
- BlockCanaryEx添加了方法采样,知道主线程中所有方法的执行时间和执行次数;
- 当应用卡顿时,BlockCanaryEx更关注app代码中,哪些方法耗时最多,重点记录和显示这些耗时方法;
- 添加了gc采样,当应用卡顿时,我们可以知道卡顿时是否发生了gc,以及gc的时间;
- 监控view性能,计算卡顿时,view的measure,layout,draw消耗的时间。
BlockCanaryEx在编译期,通过字节码注入,将方法采样器注入到你的代码中
入口在哪?
BlockCanaryEx.install() -> BlockMonitor.install()
BlockMonitor.install() 主要调用了
- BlockMonitor.ensureMonitorInstalled() -> Looper.getMainLooper().setMessageLogging(LOOPER_MONITOR)
- 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等
例如:
- LayoutInflater.inflate()方法一开始会调用Trace.traceBegin(Trace.TRACE_TAG_VIEW, “inflate”)
- ViewRootImpl的performMeasure()中会调用Trace.traceBegin(Trace.TRACE_TAG_VIEW, “measure”);
如何hook掉android.os.Trace的traceBegin和traceEnd方法?
热修复框架 AndFix 原理:
Java层的每一个方法在虚拟机实现里面都对应着一个ArtMethod的结构体,只要把原方法的结构体内容替换成新的结构体的内容,在调用原方法的时候,真正执行的指令会是新方法的指令。
BlockCanaryEx这里采用的是 native 替换方法结构体 的思路,源码使用的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
通过Config.isBlock()判断是否发生阻塞,发生阻塞则会将List
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 | GcSampler.startIfNot(pid); |
GcSampler.startIfNot(pid)启动了GcSamplerThread类型的线程,无限循环不停的在logcat中获取gc日志,每一行的日志对应一个GcInfo对象,该对象有两个属性
1 | public class GcInfo { |
所有获取的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(),做的是:
- 读取/proc/stat 和 /proc/[pid]>/stat 文件中的信息,拼接为一个String
- 把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()方法,内部逻辑主要为:
- 从sMethodInfoPool中取出List
- 从ISamplerService.getCurrentCpuInfo()取出CpuInfo
- 从ISamplerService.popGcInfoBetween()取出List
- 将卡顿相关诊断信息封装为一个BlockInfo对象,发送给各个注册的BlockObserver
BlockMonitor的sMethodInfoPool里面存的是什么?是什么时候填充的?
存的是MethodInfo
1 | public class MethodInfo { |
存储了一个方法名、方法所属类、方法参数、方法真实耗时、方法CPU耗时
BlockMonitor的sMethodInfoPool是什么时候填充的?
在BlockMonitor.reportMethodProfile()对sMethodInfoPool进行了元素的添加
BlockMonitor.reportMethodProfile()在哪被调用的?
MethodSampler.onMethodExit()中调用了BlockMonitor.reportMethodProfile()
MethodSampler.onMethodExit()在哪被调用的?
在编译时通过JavaAssist字节码插桩到项目的每一个方法的调用后会调用MethodSampler.onMethodExit(),具体实现在SamplerInjecter类的insertSamplerCode()方法
hook任意方法的原理是什么?
目前热修复框架主要有QQ空间补丁、HotFix、Tinker、Robust等。热修复框架按照原理大致可以分为三类:
- 基于 multidex机制 干预 ClassLoader 加载dex
- native 替换方法结构体
- instant-run 插桩方案
热修复框架 AndFix 原理:
Java层的每一个方法在虚拟机实现里面都对应着一个ArtMethod的结构体,只要把原方法的结构体内容替换成新的结构体的内容,在调用原方法的时候,真正执行的指令会是新方法的指令。
BlockCanaryEx这里采用的是 native 替换方法结构体 的思路,源码使用的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层的指针表示。
参考资料: