BlockCanary是一个Android平台的非侵入式的性能监控组件,应用只需要实现一个抽象类,提供一些该组件需要的上下文环境,就可以在平时使用应用的时候检测主线程上的各种卡慢问题,并通过组件提供的各种信息分析出原因并进行修复。(作者原述)
很敬佩作者能够写出这么高质量的代码,BlockCanary的代码设计简洁明了,结构清晰,从第一行代码install走下去,很快就能了解到整个框架的思路。去网上搜了下作者,发现作者12年毕业,自己很汗颜。
本文主要介绍BlockCanary的实现原理和源码设计。
项目地址
https://github.com/markzhai/AndroidPerformanceMonitor
设计思路
首先我们确定一个事实,所有的UI操作都要经过Looper和Handler的处理。如图所示。
ActivityThread是个android私有类,在应用层无法直接访问(通过反射可以访问),Handler是一个应用层经常自定义的类。Looper是个公开类,而且被final修饰,所以无法被继承,这样就给了应用层分析的机会。下面是Lo
从代码中可以看到,在dispatchMessage的前后,Looper都做了一次logging的判断,进行log的打印。非常幸运的是,android提供了一个公开的接口,允许各app自定义这个Printer。该接口是:
大家应该都知道主线程默认会生成一个Looper,这个Looper其实就是在ActivityThread中被创建的,在创建之后,也
BlockCanary的关键代码就在Looper. setMessageLogging上,通过注册自己的Printer,得到UI事件的开始处理时间和结束处理时间,若时间超过block阈值,则提取主线程的调用栈信息和cpu使用信息。
下图是作者绘制的流程图,非常明了的展示了BlockCanary的工作方式。一个额外注意的地方就是在dispatchMessage的同时,会执行一个计时器,如果超过了某个阈值,就会后
接下来就开始代码分析吧。
源码分析
BlockCanary号称APP一行代码即可搞定。
就从BlockCanary.install这个方法开始吧。
BlockCanary.install
public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
BlockCanaryContext.init(context, blockCanaryContext);
setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification())
- 6
该方法包括三个步骤
1. BlockCanaryContext.init(context, blockCanaryContext);。就是把app设置的参数传递给BlockCanaryContext。
2. setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification()); DisplayActivity是用来展示block信息的界面,该方法就是根据app设置是否显示通知来设置该activity是否展示。通常在debug的时候为true,release的时候为false。
3. get()。使用懒汉的单例模式,生成BlockCanary实例.
BlockCanary()
private BlockCanary() {
BlockCanaryInternals.setContext(BlockCanaryContext.get());
mBlockCanaryCore = BlockCanaryInternals.getInstance();
mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
if (!BlockCanaryContext.get().displayNotification()) {
return;
}
mBlockCanaryCore.addBlockInterceptor(new DisplayService());
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
BlockCanaryInternals是BlockCanary的内部实现,在这个类中创建了Looper的printer。mBlockCanaryCore是其单例对象。代码当中,还有两句addBlockInterceptor,该方法是注册两个block的拦截器,供UI或者APP额外处理block的事件。mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());是在APP中实现,
mBlockCanaryCore.addBlockInterceptor(new DisplayService())是BlockCanary的内部实现,用来发出block通知,调出DisplayActivity展示block信息。
BlockInterceptor的定义如下。
void onBlock(Context context, BlockInfo blockInfo);
}
- 1
- 2
- 3
BlockCanaryInternals()
5. public BlockCanaryInternals() {
stackSampler = new StackSampler(
Looper.getMainLooper().getThread(),
sContext.provideDumpInterval());
cpuSampler = new CpuSampler(sContext.provideDumpInterval());
setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {
@Override
public void onBlockEvent(long realTimeStart, long realTimeEnd,
long threadTimeStart, long threadTimeEnd) {
// Get recent thread-stack entries and cpu usage
ArrayList<String> threadStackEntries = stackSampler
.getThreadStackEntries(realTimeStart, realTimeEnd);
if (!threadStackEntries.isEmpty()) {
BlockInfo blockInfo = BlockInfo.newInstance()
.setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd)
.setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd))
.setRecentCpuRate(cpuSampler.getCpuRateInfo())
.setThreadStackEntries(threadStackEntries)
.flushString();
LogWriter.save(blockInfo.toString());
if (mInterceptorChain.size() != 0) {
for (BlockInterceptor interceptor : mInterceptorChain) {
interceptor.onBlock(getContext().provideContext(), blockInfo);
}
}
}
}
}, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));
LogWriter.cleanObsolete();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
这个方法做了四件事:
1. new StackSampler。该类是用来dump thread的stack信息。传递的参数是主线程和dump间隔。
2. new CpuSampler。该类是用来dump cpu的使用情况,传递的参数是dump间隔。
3. setMonitor。终于创建监听器了。到LooperMonitor类中,发现该类实现了Printer,并且实现了println方法。还记得文章开始提到的Looper.loop()中dispatchMessage前后的logging方法吗?没错,最终就会调用到LooperMonitor.println()中来。
4. onBlockEvent是在当block事件发生后,监听器要做的事情,包括:保存已经dump下来的主线程调用栈,cpu使用情况和内存使用情况以及监听的app的基本信息等;通知block拦截器分别处理各自的事情,也就是通知DisplayService去创建通知栏信息。
5. LogWriter.cleanObsolete(),删除过时的log。BlockCanary默认保存两天的log。
终于初始化完了。接下来就该启动监听了吧。还记得Application的那唯一一句代码吗?除了install,可千万别忘了后面还有个start呢。
Start monitor
public void start() {
if (!mMonitorStarted) {
mMonitorStarted = true;
Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这句代码可真是BlockCanary最关键的一句代码。mBlockCanaryCore.monitor就是刚才BlockCanaryInternals构造函数中setMonitor设置的。
监听器设置了,UI事件的响应可以监听了。接下来我们继续分析监听器的处理过程。需要详细分析LooperMonitor这个类。
LooperMonitor
先分析该类中最重要的一个方法:println。
public void println(String x) {
if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
return;
}
if (!mPrintingStarted) {
mStartTimestamp = System.currentTimeMillis();
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
stopDump();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
可以看到这个方法的主要思想是:如果是dispatchMessage之前调用,则记录开始时间和startDump。mStartTimestamp表示的是当前系统时间,mStartThreadTimestamp表示的是当前线程纯运行时间。SystemClock.currentThreadTimeMillis()这个方法返回的时间并不是当前线程的已存活时间,而是当前线程处于运行状态的总时间。如果该线程执行过sleep或者wait,那么sleep和wait的时间是不记录到该方法里的。如果是dispatchMessage之后调用,则根据时间差判断是否block,如果block,则发送block事件,会调用到前面提到的onBlockEvent方法中,同时停止dump。isBlock和notifyBlockEvent的方法很简单,不在详述。
doDump
private void startDump() {
if (null != BlockCanaryInternals.getInstance().stackSampler) {
BlockCanaryInternals.getInstance().stackSampler.start();
}
if (null != BlockCanaryInternals.getInstance().cpuSampler) {
BlockCanaryInternals.getInstance().cpuSampler.start();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
还记得在BlockCanaryInternals的构造函数中创建的stackSampler和cpuSampler吗?该他们上场了。这两个类都继承AbstractSampler,区别在于doSample()上。本文主要讲这个方法。先看StackSampler的。
@Override
protected void doSample() {
StringBuilder stringBuilder = new StringBuilder();
for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
stringBuilder
.append(stackTraceElement.toString())
.append(BlockInfo.SEPARATOR);
}
synchronized (sStackMap) {
if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
sStackMap.remove(sStackMap.keySet().iterator().next());
}
sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
代码很简单,提取当前线程的调用栈。mCurrentThread其实就是主线程,因为在new StackSampler传递参数时,代码中用的是Looper.getMainLooper().getThread(),然后以当前时间戳为key放到sStackMap中。sStackMap是HashMap吗?是,但作者将他定义是一个LinkedHashMap。LinkedHashMap和HashMap的区别是:LinkedHashMap能够记录entry的插入顺序,而HashMap无法做到,所以当进行遍历时,LinkedHashMap是按照先插入后输出的顺序进行,而HashMap的顺序是未知的。在该场景下LinkedHashMap输出的内容基本上可以保证按时间戳从小到大排列。
接下来我们再看CpuSampler的doSample方法:
protected void doSample() {
BufferedReader cpuReader = null;
BufferedReader pidReader = null;
try {
cpuReader = new BufferedReader(new InputStreamReader(
new FileInputStream("/proc/stat")), BUFFER_SIZE);
String cpuRate = cpuReader.readLine();
if (cpuRate == null) {
cpuRate = "";
}
if (mPid == 0) {
mPid = android.os.Process.myPid();
}
pidReader = new BufferedReader(new InputStreamReader(
new FileInputStream("/proc/" + mPid + "/stat")), BUFFER_SIZE);
String pidCpuRate = pidReader.readLine();
if (pidCpuRate == null) {
pidCpuRate = "";
}
parse(cpuRate, pidCpuRate);
} catch (Throwable throwable) {
Log.e(TAG, "doSample: ", throwable);
} finally {
try {
if (cpuReader != null) {
cpuReader.close();
}
if (pidReader != null) {
pidReader.close();
}
} catch (IOException exception) {
Log.e(TAG, "doSample: ", exception);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
这个方法就是读取/proc/下的两个stat内存文件。可以直接通过adb shell去查看这两个文件中的原始内容是什么。
/proc/stat的内容如下:
/proc/${pid}/stat的内容如下:
然后执行parse方法进行解析。
整个监听的过程中就分析完了,还有一些UI的代码本文就不做分析了。