BlockCanary
BlockCanary是markzhai同学的一个很实用的作品, 能检测到主线程的卡顿, 并将结果记录下来, 以友好的方式展示, 实属性能监测的良品, 他重用了LeakCanary的UI展示, 其它与LeakCanary的关系并不是太大.
BlockCanary使用
BlockCanary使用方式也比较简单, 要在Application中进行设置一下就可以了:
BlockCanary.install(this, new AppBlockCanaryContext()).start();
其中的AppBlockCanaryContext继承自BlockCanaryContext, 是对BlockCanary中各个参数进行配置的类, 我们可以重写其中的getXXX()方法来对其进行配置. 其中或配置的参数有很多, 看名字就可以知道了:
//卡顿阀值
int getConfigBlockThreshold();
boolean isNeedDisplay();
String getQualifier();
String getUid();
String getNetworkType();
Context getContext();
String getLogPath();
boolean zipLogFile(File[] src, File dest);
//可将卡顿日志上传到自己的服务
void uploadLogFile(File zippedFile);
String getStackFoldPrefix();
int getConfigDumpIntervalMillis();
然后就可以进行操作了, 在某个消息执行时间超过设定的标准时会弹出通知进行提醒.
BlockCanary分析
BlockCanary的检测原理, 作者已经进行了说明, 就是在主线程消息循环打出日志的地入手, 当一个消息操作时间超过阀值后, 记录系统各种资源的状态, 并展示出来. 下面从代码的层面学习一下.
还是从Application的install开始分析,
public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
BlockCanaryContext.init(context, blockCanaryContext);
setEnabled(context, DisplayBlockActivity.class, BlockCanaryContext.get().isNeedDisplay());
return get();
}
其中的get()
方法是BlockCanary作为单例向外提供拿实例的方法, 在第一次取实例的时候构造对象, 构造时创建一个BlockCanaryCore类型的成员, 也是类似单例模式, 在构造BlockCanaryCore的时候进行了初始化工作:
public BlockCanaryCore() {
threadStackSampler = new ThreadStackSampler(Looper.getMainLooper().getThread(),
sBlockCanaryContext.getConfigDumpIntervalMillis());
cpuSampler = new CpuSampler(sBlockCanaryContext.getConfigDumpIntervalMillis());
setMainLooperPrinter(new LooperPrinter(new BlockListener() {
@Override
public void onBlockEvent(long realTimeStart, long realTimeEnd,
long threadTimeStart, long threadTimeEnd) {
// Get recent thread-stack entries and cpu usage
ArrayList<String> threadStackEntries = threadStackSampler
.getThreadStackEntries(realTimeStart, realTimeEnd);
// Log.d("BlockCanary", "threadStackEntries: " + threadStackEntries.size());
if (!threadStackEntries.isEmpty()) {
Block block = Block.newInstance()
.setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd)
.setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd))
.setRecentCpuRate(cpuSampler.getCpuRateInfo())
.setThreadStackEntries(threadStackEntries)
.flushString();
LogWriter.saveLooperLog(block.toString());
if (getContext().isNeedDisplay() && mOnBlockEventInterceptor != null) {
mOnBlockEventInterceptor.onBlockEvent(getContext().getContext(), block.timeStart);
}
}
}
}, getContext().getConfigBlockThreshold()));
LogWriter.cleanOldFiles();
}
其中创建两个真正去拿系统状态的对象:threadStackSampler和cpuSampler.
创建一个LooperPrinter的对象设置给系统, 被系统当作记录日志Printer使用.
系统会在执行每个消息的前后都会调用Printer的println方法来记录日志, 下面看看LoopPrinter中是怎么写的.
public void println(String x) {
if (!mStartedPrinting) {
mStartTimeMillis = System.currentTimeMillis();
mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
mStartedPrinting = true;
startDump();
} else {
final long endTime = System.currentTimeMillis();
mStartedPrinting = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
stopDump();
}
}
从消息执行开始startDump()
, 开始记录系统的各种资源状态, 在下次执行时, 也就是消息执行结束时判断是否达到了卡顿的阀值, 如果达到通知监听(在构造时传入)已经卡顿,同时停止记录系统资源状态. 其中的开始记录和结束记录都是直接调用了构造BlockCanaryCore时创建的两个监视资源的成员.
卡顿的监听也是在BlockCanaryCore构造时创建的, 他在收到卡顿通知后取得资源监视器中存储的数据, 存储到外存, 最后通知mOnBlockEventInterceptor, 而这个成员是什么时候传入的呢? 答案是在构造BlockCanary的时候, 先得到BlockCanaryCore实例, 然后就是初始化通知. 这里是通过反射取得Notifier的实例, 并将之设置为BlockCanaryCore中的mOnBlockEventInterceptor成员. 在Notifier中就直接弹出通知, 展示数据了.
下面看看两个资源监控的两个对象是怎么工作的, ThreadStackSampler
protected void doSample() {
// Log.d("BlockCanary", "sample thread stack: [" + mThreadStackEntries.size() + ", " + mMaxEntryCount + "]");
StringBuilder stringBuilder = new StringBuilder();
// Fetch thread stack info
for (StackTraceElement stackTraceElement : mThread.getStackTrace()) {
stringBuilder.append(stackTraceElement.toString())
.append(Block.SEPARATOR);
}
// Eliminate obsolete entry
synchronized (mThreadStackEntries) {
if (mThreadStackEntries.size() == mMaxEntryCount && mMaxEntryCount > 0) {
mThreadStackEntries.remove(mThreadStackEntries.keySet().iterator().next());
}
mThreadStackEntries.put(System.currentTimeMillis(), stringBuilder.toString());
}
}
直接去拿主线程的栈信息, 每半秒去拿一次, 记录下来, 如果发生卡顿就显之显示出来
拿CPU的信息较麻烦, 从/proc/stat
下面拿实时的CPU状态, 再从/proc/" + mPid + "/stat
中读取进程时间, 再计算各CPU时间占比和CPU的工作状态. 下面两张图是BlockCanary的Demo中得到的实例, 看起来和LeakCanary挺像, 其实数据完全不一样
尾巴
有听说UC通过在Handler中放入idel消息来检测每条消息的耗时, 相比之下这种方式要容易理解得多, 还听说这个项目已经运用在多个实际项目中, 别等了, 快点加入吧, 哪怕是在自己本地跑一下也能对自己系统的卡顿有个客观的了解.