Android 卡顿专题
目录
目录
3、接下来看核心类BlockCanaryInternals的初始化过程
BlockCanary源码分析:
1、入口
BlockCanary.install(this, new AppContext()).start();
这里this, 代表的是applicationContext, AppContext继承的BlockCanaryContext,install函数中做了两个事情
将复写的AppContext对象复制给BlockCanaryContext,setEnable的作用如下
/**
* setEnabled将根据用户的通知栏消息配置开启(displayNotification=true)
* 或关闭(displayNotification=false)DisplayActivity
* (DisplayActivity是承载通知栏消息的activity)
* @param context
* @param componentClass
* @param enabled
*/
private static void setEnabled(Context context,
final Class<?> componentClass,
final boolean enabled) {
final Context appContext = context.getApplicationContext();
executeOnFileIoThread(new Runnable() {
@Override
public void run() {
setEnabledBlocking(appContext, componentClass, enabled);
}
});
}
setEnable是根据用户而配置的AppContext中的
displayNotification返回值,如果可以依据情况复写什么时候显示这个notification;默认的实现方式是debug版本的时候打开;
2、外观类BlockCanary的创建
public static BlockCanary get() {
if (sInstance == null) {
synchronized (BlockCanary.class) {
if (sInstance == null) {
sInstance = new BlockCanary();
}
}
}
return sInstance;
}
//私有构造函数
private BlockCanary() {
BlockCanaryInternals.setContext(BlockCanaryContext.get());
mBlockCanaryCore = BlockCanaryInternals.getInstance();
mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
if (!BlockCanaryContext.get().displayNotification()) {
return;
}
mBlockCanaryCore.addBlockInterceptor(new DisplayService());
}
a、外观类是单例模式的实现,setContext是设置用户复写的appContext的类型;
b、blockCanaryCore是核心类,在这个类中维护了一个
private List<BlockInterceptor> mInterceptorChain = new LinkedList<>();
可以用来添加实现了BlockInterceptor接口的拦截器;
mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());这里面的blockCanaryContext实现了BlockInterceptor接口,其中的onBlock方法可以自己根据业务需要实现;代表的含义是当拦截到了卡顿时间,想要做什么样子的操作
c、 mBlockCanaryCore.addBlockInterceptor(new DisplayService());这句是发生卡顿的时候,打开Notification的核心,界面是一个
DisplayActivity;
3、接下来看核心类BlockCanaryInternals的初始化过程
public BlockCanaryInternals() {
// 初始化stack的采集器
stackSampler = new StackSampler(
Looper.getMainLooper().getThread(),
sContext.provideDumpInterval());
// 初始化cpu的采集器
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();
}
创建了两个采样类StackSampler和CpuSampler,即线程堆栈采样和CPU采样。
随后创建一个LooperMonitor,LooperMonitor实现了android.util.Printer接口。
随后通过调用setMonitor把创建的LooperMonitor赋值给BlockCanaryInternals的成员变量monitor。
public BlockCanaryInternals() {
// 在这里采集的是主线程的堆栈信息
stackSampler = new StackSampler(
Looper.getMainLooper().getThread(),
sContext.provideDumpInterval());
// 这里采集的是cpu的信息
cpuSampler = new CpuSampler(sContext.provideDumpInterval());
/**
* 这里是整个的核心函数,设置成员变量monitor用作BlockCanary中start函数中set的那个
* monitor
*/
setMonitor(new LooperMonitor(new MyBlockListener(),
getContext().provideBlockThreshold(),
getContext().stopWhenDebugging()));
LogWriter.cleanObsolete();
}
/**
* 这里就是回调函数。发生卡顿之后用于记录卡顿的信息
*/
private class MyBlockListener implements LooperMonitor.BlockListener {
@Override
public void onBlockEvent(long realStartTime, long realTimeEnd, long threadTimeStart, long threadTimeEnd) {
// Get recent thread-stack entries and cpu usage
ArrayList<String> threadStackEntries = stackSampler
.getThreadStackEntries(realStartTime, realTimeEnd);
if (!threadStackEntries.isEmpty()) {
BlockInfo blockInfo = BlockInfo.newInstance()
//这里打出来的是卡顿的耗时
.setMainThreadTimeCost(realStartTime, realTimeEnd, threadTimeStart, threadTimeEnd)
.setCpuBusyFlag(cpuSampler.isCpuBusy(realStartTime, realTimeEnd))
.setRecentCpuRate(cpuSampler.getCpuRateInfo())
.setThreadStackEntries(threadStackEntries)
.flushString();
// 保存文件
LogWriter.save(blockInfo.toString());
if (mInterceptorChain.size() != 0) {
// 在这里回调实现的拦截器接口的onBlock方法(BlockCanaryContext + DisplayService)
for (BlockInterceptor interceptor : mInterceptorChain) {
interceptor.onBlock(getContext().provideContext(), blockInfo);
}
}
}
}
}
4、start方法
即调用BlockCanary的start方法
public void start() {
if (!mMonitorStarted) {
mMonitorStarted = true;
Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
}
}
将在BlockCanaryInternals中创建的LooperMonitor给主线程Looper的mLogging变量赋值。这样主线程Looper就可以消息分发前后使用LooperMonitor#println输出日志。
5、卡顿的监控方法
首先要了解Android应用程序只有一个主线程ActivityThread,这个主线程会创建一个Looper(Looper.prepare),而Looper又会关联一个MessageQueue,主线程Looper会在应用的生命周期内不断轮询(Looper.loop),从MessageQueue取出Message 更新UI。
/**
* Handle system messages here.
*/
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
- 如果消息是通过Handler.post(runnable)方式投递到MQ中的,那么就回调runnable#run方法;
- 如果消息是通过Handler.sendMessage的方式投递到MQ中,那么回调handleMessage方法;
不管是哪种回调方式,回调一定发生在UI线程。因此如果应用发生卡顿,一定是在dispatchMessage中执行了耗时操作。我们通过给主线程的Looper设置一个Printer,打点统计dispatchMessage方法执行的时间,如果超出阀值,表示发生卡顿,则dump出各种信息,提供开发者分析性能瓶颈。
我们通过给printer设置自定义的monitor即可完成卡顿的打点工作,通过loop.java中提供的setMessageLogging方法更改系统的monitor为我们上面自定义的LooperMonitor;
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
......
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
}
主线程的所有消息都在这里调度(msg.target.dispatchMessage即 handle的dispatchMessage方法)!!
每从MQ中取出一个消息,由于我们设置了Printer为LooperMonitor,因此在调用dispatchMessage前后都可以交由我们LooperMonitor接管。
我们再次从下面这段代码入手。
@Override
public void println(String x) {
Log.e("LooperMonitor", "####println: x" + x);
// 注意哈,mStopWhenDebugging 代表的是debug的时候不会打印
if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
return;
}
// 默认mPrintingStarted为false
if (!mPrintingStarted) {
// 记录开始的时间
mStartTimestamp = System.currentTimeMillis();
Log.e("LooperMonitor", "####println: mStartTimestamp" + mStartTimestamp);
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
//开始dump卡顿信息
startDump();
} else {
//记录结束时间(此处是dispatch之后logging.println调用)
final long endTime = System.currentTimeMillis();
Log.e("LooperMonitor", "####println: endTime" + endTime);
mPrintingStarted = false;
// 比较两次的时间间隔是否是卡顿阈值
if (isBlock(endTime)) {
// 回调blockListener的onBlockEvent方法
notifyBlockEvent(endTime);
}
stopDump();
}
}
接着我们再回顾下blockListener的onBlockEvent方法中做了啥;
这里是保存信息到blockInfo这个变量中,然后回调拦截器中方法,进行notifaction或日志输出;
private class MyBlockListener implements LooperMonitor.BlockListener {
@Override
public void onBlockEvent(long realStartTime, long realTimeEnd, long threadTimeStart, long threadTimeEnd) {
// Get recent thread-stack entries and cpu usage
ArrayList<String> threadStackEntries = stackSampler
.getThreadStackEntries(realStartTime, realTimeEnd);
if (!threadStackEntries.isEmpty()) {
BlockInfo blockInfo = BlockInfo.newInstance()
//这里打出来的是卡顿的耗时
.setMainThreadTimeCost(realStartTime, realTimeEnd, threadTimeStart, threadTimeEnd)
.setCpuBusyFlag(cpuSampler.isCpuBusy(realStartTime, realTimeEnd))
.setRecentCpuRate(cpuSampler.getCpuRateInfo())
.setThreadStackEntries(threadStackEntries)
.flushString();
// 保存文件
LogWriter.save(blockInfo.toString());
if (mInterceptorChain.size() != 0) {
// 在这里回调实现的拦截器接口的onBlock方法(BlockCanaryContext + DisplayService)
for (BlockInterceptor interceptor : mInterceptorChain) {
interceptor.onBlock(getContext().provideContext(), blockInfo);
}
}
}
}
}
6、采集器分析
在我们自定义的LooperMonitor中会在hanlder的dispatchMessage中于卡顿前后分别调用loopMonitor.print方法。在print方法中
第一次会调用startDump方法,第二次调用stopDump方法;
/**
* dump CPU & Stack的信息
*/
private void startDump() {
if (null != BlockCanaryInternals.getInstance().stackSampler) {
BlockCanaryInternals.getInstance().stackSampler.start();
}
if (null != BlockCanaryInternals.getInstance().cpuSampler) {
BlockCanaryInternals.getInstance().cpuSampler.start();
}
}
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
// doSample抽象方法,由StackSampler和CpuSampler实现不同的采集方法
doSample();
if (mShouldSample.get()) {
HandlerThreadFactory.getTimerThreadHandler()
.postDelayed(mRunnable, mSampleInterval);
}
}
};
public AbstractSampler(long sampleInterval) {
if (0 == sampleInterval) {
sampleInterval = DEFAULT_SAMPLE_INTERVAL;
}
mSampleInterval = sampleInterval;
}
// 在LooperMonitor.startDump处调用启用子线程开启采样
public void start() {
if (mShouldSample.get()) {
return;
}
mShouldSample.set(true);
HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
BlockCanaryInternals.getInstance().getSampleDelay());
}
public void stop() {
if (!mShouldSample.get()) {
return;
}
mShouldSample.set(false);
HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
}
abstract void doSample();
6.1 Stack采集器
比较简单,直接通过mCurrentThread.getStackTrace方法获取到卡顿的堆栈信息;
/**
* 采集Stack的信息
*/
@Override
protected void doSample() {
StringBuilder stringBuilder = new StringBuilder();
/**
* 获取当前线程的堆栈信息
*/
for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
//TODO 在这里可以过滤自己想要过滤的堆栈信息
//TODO 比如在这里过滤自己比较关注的包名
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());
}
}
6.2 CPU采集器
略,详见CpuSampler.java
以下是节选自http://blog.zhaiyifan.cn/2016/01/16/BlockCanaryTransparentPerformanceMonitor/
功能
BlockCanary会在发生卡顿(通过MonitorEnv的getConfigBlockThreshold设置)的时候记录各种信息,输出到配置目录下的文件,并弹出消息栏通知(可关闭)。
简单的使用如在开发、测试、Monkey的时候,Debug包启用
- 开发可以通过图形展示界面直接看信息,然后进行修复
- 测试可以把log丢给开发,也可以通过卡慢详情页右上角的更多按钮,分享到各种聊天软件(不要怀疑,就是抄的LeakCanary)
- Monkey生成一堆的log,找个专人慢慢过滤记录下重要的卡慢吧
还可以通过Release包用户端定时开启监控并上报log,后台匹配堆栈过滤同类原因,提供给开发更大的样本环境来优化应用。
本项目提供了一个友好的展示界面,供开发测试直接查看卡慢信息(基于LeakCanary的界面修改)。
dump的信息包括:
- 基本信息:安装包标示、机型、api等级、uid、CPU内核数、进程名、内存、版本号等
- 耗时信息:实际耗时、主线程时钟耗时、卡顿开始时间和结束时间
- CPU信息:时间段内CPU是否忙,时间段内的系统CPU/应用CPU占比,I/O占CPU使用率
- 堆栈信息:发生卡慢前的最近堆栈,可以用来帮助定位卡慢发生的地方和重现路径
sample如下图,可以精确定位到代码中哪一个类的哪一行造成了卡慢。
总结
BlockCanary作为一个Android组件,目前还有局限性,因为其在一个完整的监控系统中只是一个生产者,还需要对应的消费者去分析日志,比如归类排序,以便看出哪些卡慢更有修复价值,需要优先处理(TODO 这里可以实现耗时的打印日志排序,已经过滤和应用相关的信息);又比如需要过滤机型,有些奇葩机型的问题造成的卡慢,到底要不要去修复是要斟酌的。扯远一点的话,像是埋点除了统计外,完全还能用来做链路监控,比如一个完整的流程是A -> B -> D -> E, 但是某个时间节点突然A -> B -> D后没有到达E,这时候监控平台就可以发出预警,让开发人员及时定位。很多监控方案都需要C/S两端的配合。