本文转载自:https://www.jianshu.com/p/e58992439793
BlockCanary是国内开发者MarkZhai开发的一套性能监控组件,它对主线程操作进行了完全透明的监控,并能输出有效的信息,帮助开发分析、定位到问题所在,迅速优化应用。
其特点有:
- 非侵入式,简单的两行就打开监控,不需要到处打点,破坏代码优雅性。
- 精准,输出的信息可以帮助定位到问题所在(精确到行),不需要像Logcat一样,慢慢去找。目前包括了核心监控输出文件,以及UI显示卡顿信息功能
基本使用
使用非常方便,引入
1 | dependencies { |
在应用的application中完成初始化
1 | public class DemoApplication extends Application { |
基本原理
我们都知道Android应用程序只有一个主线程ActivityThread,这个主线程会创建一个Looper(Looper.prepare),而Looper又会关联一个MessageQueue,主线程Looper会在应用的生命周期内不断轮询(Looper.loop),从MessageQueue取出Message 更新UI。
我们来看一个代码片段
1 | public static void loop() { |
msg.target其实就是Handler,看一下dispatchMessage的逻辑
1 | /** |
- 如果消息是通过Handler.post(runnable)方式投递到MQ中的,那么就回调runnable#run方法;
- 如果消息是通过Handler.sendMessage的方式投递到MQ中,那么回调handleMessage方法;
不管是哪种回调方式,回调一定发生在UI线程。因此如果应用发生卡顿,一定是在dispatchMessage中执行了耗时操作。我们通过给主线程的Looper设置一个Printer,打点统计dispatchMessage方法执行的时间,如果超出阀值,表示发生卡顿,则dump出各种信息,提供开发者分析性能瓶颈。
1 |
|
源码分析
源码分析主要分为框架初始化过程和监控过程
框架初始化过程
初始化过程主要通过下面第一行代码发起
1 | BlockCanary.install(this, new AppContext()).start(); |
在内部我们细分为install和start过程
install
1 | public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) { |
- BlockCanaryContext.init会将保存应用的applicationContext和用户设置的配置参数;
- setEnabled将根据用户的通知栏消息配置开启(displayNotification=true)或关闭(displayNotification=false)DisplayActivity (DisplayActivity是承载通知栏消息的activity)
注意该设置过程需要提交到一个单线程的IO线程池去执行。
接下来是外观类BlockCanary的创建过程
1 | public static BlockCanary get() { |
- 单例创建BlockCanary
- 核心处理类为BlockCanaryInternals
- 为BlockCanaryInternals添加拦截器(责任链)
- BlockCanaryContext对BlockInterceptor是空实现,可以忽略;
- DisplayService只在开启通知栏消息的时候添加,当卡顿发生时将通过DisplayService发起通知栏消息
接下来看核心类BlockCanaryInternals的初始化过程。
1 | public BlockCanaryInternals() { |
创建了两个采样类StackSampler和CpuSampler,即线程堆栈采样和CPU采样。
随后创建一个LooperMonitor,LooperMonitor实现了android.util.Printer接口。
随后通过调用setMonitor把创建的LooperMonitor赋值给BlockCanaryInternals的成员变量monitor。
start
即调用BlockCanary的start方法
1 | public void start() { |
将在BlockCanaryInternals中创建的LooperMonitor给主线程Looper的mLogging变量赋值。这样主线程Looper就可以消息分发前后使用LooperMonitor#println输出日志。
卡顿监控过程
根据上面原理的分析,监控的对象主要是Main Looper的Message分发耗时情况。
1 | //Looper |
主线程的所有消息都在这里调度!!
每从MQ中取出一个消息,由于我们设置了Printer为LooperMonitor,因此在调用dispatchMessage前后都可以交由我们LooperMonitor接管。
我们再次从下面这段代码入手。
1 |
|
对于单个Message而言,这个方法一定的成对调用的。
卡顿监控记录
第一次调用时,记录开始时间,并开始dump堆栈和CPU信息。
1 | //LooperMonitor |
- 两种采样依次提交到HandlerThread中进行,从而保证采样过程是在一个后台线程执行;
- 两种采样有个共同的父类AbstractSampler,采用了模板方法模式,即在父类定义了采样的抽象算法doSample及采样生命周期的管控(start和stop),不同的子类采样的算法实现是不一样的;
- 采样会周期性执行,间隔时间与卡顿阀值一致(可由开发者设置);
堆栈采样
堆栈采样很简单,直接通过Main Looper获取到主线程Thread对象,调用Thread#getStackTrace即可获取到堆栈信息
1 |
|
将堆栈拼成String,保存在LinkedHashMap中,当然保存有一定阀值,默认最多保存100条。
CPU采样
在分析代码之前我们需要先了解一下Android平台CPU的一些常识。
我们都知道Android是基于Linux系统的,Android平台关于CPU的计算是跟Linux是完全一样的。
/proc/stat文件
在Linux中CPU活动信息是保存在该文件中,该文件中的所有值都是从系统启动开始累计到当前时刻。
1 | ~$ cat /proc/stat |
第二行的数值表示的是CPU总的使用情况,所以我们只要用第一行的数字计算就可以了
下表解析第一行各数值的含义
参数 | 解析 (以下数值都是从系统启动累计到当前时刻) |
---|---|
user (38082) | 处于用户态的运行时间,不包含 nice值为负进程 |
nice (627) | nice值为负的进程所占用的CPU时间 |
system (27594) | 处于核心态的运行时间 |
idle (893908) | 除IO等待时间以外的其它等待时间iowait (12256) 从系统启动开始累计到当前时刻,IO等待时间 |
irq (581) | 硬中断时间 |
irq (581) | 软中断时间 |
stealstolen(0) | 一个其他的操作系统运行在虚拟环境下所花费的时间 |
guest(0) | 这是在Linux内核控制下为客户操作系统运行虚拟CPU所花费的时间 |
总结:总的cpu时间totalCpuTime = user + nice + system + idle + iowait + irq + softirq + stealstolen + guest
/proc/pid/stat文件
该文件包含了某一进程所有的活动的信息,该文件中的所有值都是从系统启动开始累计到当前时刻
1 | ~$ cat /proc/6873/stat |
以下只解释对我们计算Cpu使用率有用相关参数
参数 | 解析 |
---|---|
pid=6873 | 进程号 |
utime=1587 | 该任务在用户态运行的时间,单位为jiffies |
stime=41958 | 该任务在核心态运行的时间,单位为jiffies |
cutime=0 | 所有已死线程在用户态运行的时间,单位为jiffies |
cstime=0 | 所有已死在核心态运行的时间,单位为jiffies |
结论:进程的总Cpu时间processCpuTime = utime + stime + cutime + cstime,该值包括其所有线程的cpu时间。
CPU采样的代码如下:
1 |
|
卡顿条件判断及事后处理
当LooperMonitor第二次调用时,会判断第二次与第一次的时间间隔是否会超过阀值。
1 | private boolean isBlock(long endTime) { |
若超过,将视作一次卡顿。满足卡顿条件将会调用下面方法
1 | private void notifyBlockEvent(final long endTime) { |
可以看到日志的写入执行在工作线程(HandlerThread),将回调BlockListener#onBlockEvent
将堆栈采样和CPU采样数据封装为一个BlockInfo。
接下来将进行卡顿事后处理。
主要有两件事情:
- 将卡顿发生时的堆栈和CPU信息写入日志;
- 如果开启走通知栏,那么将发出一条通知栏消息;
卡顿日志记录
通过LogWriter.save(blockInfo.toString())完成
1 | public static String save(String str) { |
注意:以上代码的调用执行在工作线程HandlerThread(writer)中
通知栏消息
通知栏消息由下面代码触发
1 | if (mInterceptorChain.size() != 0) { |
其中BlockInterceptor的一个实现类为DisplayService
1 | final class DisplayService implements BlockInterceptor { |
参考资料
博主总结
博主在看过转载这篇博客后,总结如下:
BlockCanary原理是通过在主线程Handler中调用dispatchMessage方法的前后,通过Printer打印日志的地方,分别开始或者结束计时/堆栈dump/cpu采样,最终通过计算该方法耗时判断是否发生了主线程卡顿,并上报相关信息