主流开源框架源码深入了解第5篇——BlockCanary源码分析。(源码以1.5.0版为准)
UI卡顿原理
问:为什么16ms没完成绘制就会卡顿?
我们先来了解几个概念:
- Android系统每隔16ms就会重新绘制一次Activity,因此,我们的应用必须在16ms内完成屏幕刷新的全部逻辑操作,每一帧只能停留16ms,否则就会出现掉帧现象(也就是用户看到的卡顿现象)。
- 16ms = 1000/60hz,相当于60fps(每秒帧率)。这是因为人眼与大脑之间的协作无法感知超过60fps的画面更新。12fps大概类似手动快速翻书的帧率,这个速度明显可以感知是不够顺滑的。24fps使得人眼感知的是连续线性运动,24fps是电影胶圈通常使用的帧率,这个帧率可以支撑大部分电影画面需要表达的内容。但是低于30fps是无法顺畅表现绚丽的画面内容,此时需要使用60fps来达到想要的效果。因此,如果应用没有在16ms内完成屏幕刷新的全部逻辑操作,就会发生卡顿。
- Android不允许在UI线程中做耗时的操作,否则有可能发生ANR的可能,默认情况下,在Android中Activity的最长执行时间是5秒,BroadcastReceiver的最长执行时间则是10秒,Service前台20s、后台200s未完成启动。如果超过默认最大时长,则会产生ANR。
答:Android系统每隔16ms就会发出VSYNC信号,触发对UI进行渲染,VSYNC是Vertical Synchronization(垂直同步)的缩写,可以简单的把它认为是一种定时中断。在Android 4.1中开始引入VSYNC机制。为什么是16ms?因为Android设定的刷新率是60FPS(Frame Per Second),也就是每秒60帧的刷新率,约16ms刷新一次。这就意味着,我们需要在16ms内完成下一次要刷新的界面的相关运算,以便界面刷新更新。举个例子,当运算需要24ms完成时,16ms时就无法正常刷新了,而需要等到32ms时刷新,这就是丢帧了。丢帧越多,给用户的感觉就越卡顿。
正常流畅刷新图示:
哎呀!丢帧啦。卡顿图示:
BlockCanary原理
在说原理之前,我们先来了解几个概念:
-
主线程ActivityThread:严格来说,UI主线程不是ActivityThread。ActivityThread类是Android APP进程的初始类,它的main函数是这个APP进程的入口。APP进程中UI事件的执行代码段都是由ActivityThread提供的。也就是说,Main Thread实例是存在的,只是创建它的代码我们不可见。ActivityThread的main函数就是在这个Main Thread里被执行的。这个主线程会创建一个Looper(Looper.prepare),而Looper又会关联一个MessageQueue,主线程Looper会在应用的生命周期内不断轮询(Looper.loop),从MessageQueue取出Message 更新UI。
// ActivityThread类: public static void main(String[] args) { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain"); ... Looper.prepareMainLooper(); ... ActivityThread thread = new ActivityThread(); thread.attach(false, startSeq); if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } ... // End of event ActivityThreadMain. Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); // Looper开始轮询 Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited"); }
-
Vsync信号:屏幕的刷新过程是每一行从左到右(行刷新,水平刷新,Horizontal Scanning),从上到下(屏幕刷新,垂直刷新,Vertical Scanning)。当整个屏幕刷新完毕,即一个垂直刷新周期完成,会有短暂的空白期,此时发出 VSync 信号。所以,VSync 中的 V指的是垂直刷新中的垂直-Vertical。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,VSync是Vertical Synchronization(垂直同步)的缩写,是一种在PC上很早就广泛使用的技术,可以简单的把它认为是一种定时中断。而在Android 4.1(JB)中已经开始引入VSync机制,用来同步渲染,让App的UI和SurfaceFlinger可以按硬件产生的VSync节奏进行工作。
-
界面刷新:界面上任何一个 View 的刷新请求最终都会走到 ViewRootImpl 中的 scheduleTraversals() 里来安排一次遍历绘制 View 树的任务;并且通过源码都可以知道所有的界面刷新(包括Vsync信号触发的),都会通过Choreographer 的 postCallback() 方法,将界面刷新这个 Runnable 任务以当前事件放进一个待执行的队列里,最后通过主线程的Looper的loop方法取出消息并执行。
// Looper类: public static void loop() { final Looper me = myLooper(); ... // 获取当前Looper的消息队列 final MessageQueue queue = me.mQueue; ... for (; ; ) { // 取出一个消息 Message msg = queue.next(); // might block ... // "此mLogging可通过Looper.getMainLooper().setMessageLogging方法设置自定义" final Printer logging = me.mLogging; if (logging != null) {// 消息处理前 // "若mLogging不为null,则此处可回调到该类的println方法" logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } ... try { // 消息处理 msg.target.dispatchMessage(msg); dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } finally { if (traceTag != 0) { Trace.traceEnd(traceTag); } } ... if (logging != null) {// 消息处理后 // "消息处理后,也可调用logging的println方法" logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } ... } }
-
卡顿发生点:从第3条中,我们可以看出,所有消息最终都经过dispatchMessage方法。因此界面的卡顿最终都应该是发生在Handler的dispatchMessage里。
// Handler类: public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } }
-
屏幕刷新机制,具体可参考:Android 屏幕刷新机制
Handler消息机制和View的绘制机制,具体可参考:Handler机制和View绘制流程源码分析
原理: 上面几个概念中,其实里面已包含卡顿监控的原理啦。我们在界面刷新中Looper的loop方法注释中声明:”若mLogging不为null,则此处可回调到该类的println方法”,因此我们可以通过自定义的mLogging(实际为Printer接口的子类),实现Printer接口的println方法,然后在println方法中监控是否有卡顿发生。从loop方法中,可以看出logging.println调用是成对出现的,会在消息处理前后分别调用,因此可以在自定义的println方法中通过标识来分辨是消息处理前/后,通过计算时间差与我们自己设置的阀值(我们认为消息处理的最长时间,即卡顿的临界值)比对,来监控我们的程序是否发生卡顿。
官方原理介绍示例图:
BlockCanary简介
1. 关联类功能说明
- BlockCanary:外观类,提供初始化及开始、停止监听
- BlockCanaryContext:配置上下文,可配置id、当前网络信息、卡顿阈值、log保存路径等。建议:通过自己实现继承该类的子类,配置应用标识符,用户uid,网络类型,卡顿判断阀值,Log保存位置等,可通过继承该类将卡顿信息收集上传云端或保存本地等。
- BlockCanaryInternals:blockcanary核心的调度类,内部包含了monitor(设置到MainLooper的printer)、stackSampler(栈信息处理器)、cpuSampler(cpu信息处理器)、mInterceptorChain(注册的拦截器)、以及onBlockEvent的回调及拦截器的分发。
- LooperMonitor:继承了Printer接口,用于设置到MainLooper中。通过复写println的方法来获取MainLooper的dispatch前后的执行时间差,并控制stackSampler和cpuSampler的信息采集。
- StackSampler:用于获取线程的栈信息,将采集的栈信息存储到一个以key为时间戳的LinkHashMap中。通过mCurrentThread.getStackTrace()获取当前线程的StackTraceElement。
- CpuSampler:用于获取cpu信息,将采集的cpu信息存储到一个以key为时间戳的LinkHashMap中。通过读取/proc/stat文件获取cpu的信息。
- DisplayService:继承了BlockInterceptor拦截器,onBlock回调会触发发送前台通知。
- DisplayActivity:用于显示记录的异常信息的Activity。
- HandlerThreadFactory:传入一个HandlerThread类Looper的异步Handler。HandlerThread本质上是一个线程类,它继承了Thread;HandlerThread有自己的内部Looper对象,可以进行loop循环;通过获取HandlerThread的looper对象传递给Handler对象,可以在handleMessage方法中执行异步任务;创建HandlerThread后必须先调用HandlerThread.start()方法,Thread会先调用run方法,创建Looper对象。
2. BlockCanary简单使用
// Application中
// 卡顿优化
// 指定的卡顿阀值为500毫秒——provideBlockThreshold()方法;可在onBlock方法处收集堆栈信息
BlockCanary.install(this, new AppBlockCanaryContext()).start();
/** * BlockCanary配置的各种信息(部分) */
public class AppBlockCanaryContext extends BlockCanaryContext {
// 实现各种上下文,包括应用标识符,用户uid,网络类型,卡顿判断阀值,Log保存位置等
/** * 指定的卡顿阀值 500毫秒 */
public int provideBlockThreshold() {
return 500;
}
/** * 保存日志的路径 */
public String providePath() {
return "/blockcanary/";
}
/** * 是否需要在通知栏通知卡顿 */
public boolean displayNotification() {
return true;
}
/** * 此处可收集堆栈信息,以备上传分析 * Block interceptor, developer may provide their own actions. */
public void onBlock(Context context, BlockInfo blockInfo) {
Log.i("lz","blockInfo "+blockInfo.toString());
// 获取当前执行方法的调用栈信息
// String trace = Log.getStackTraceString(new Throwable());
}
AppBlockCanaryContext具体配置可参考:AppBlockCanaryContext.java
BlockCanary源码
1. BlockCanary.install
public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
// 将上下文和我们自定义的blockCanaryContext传入
BlockCanaryContext.init(context, blockCanaryContext);
// 根据displayNotification()设置是否启用或者禁用DisplayActivity组件
setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
// 返回单例BlockCanary
return get();
}
我们可以看到install方法中干了3件事情,我们分别来分析一下。
1. BlockCanaryContext.init
// BlockCanaryContext类:
private static Context sApplicationContext;
private static BlockCanaryContext sInstance = null;
static void init(Context context, BlockCanaryContext blockCanaryContext) {
sApplicationContext = context;
// 将我们自定义的blockCanaryContext类,保存在BlockCanaryContext类的成员变量sInstance中
sInstance = blockCanaryContext;
}
第一步,实际上就是在我们使用BlockCanary时,将我们自定义的AppBlockCanaryContext保存在BlockCanaryContext类的成员变量sInstance中,以供BlockCanary可以通过sInstance,来使用我们自已配置的各种信息(包括应用标识符,用户uid,网络类型,卡顿判断阀值,Log保存位置等)。
2. setEnabled启用或禁用组件
// BlockCanaryContext类:
public static BlockCanaryContext get() {
if (sInstance == null) {
throw new RuntimeException("BlockCanaryContext null");
} else {
return sInstance;
}
}
// BlockCanary类:
// 调用newSingleThreadExecutor初始化文件IO线程池
private static final Executor fileIoExecutor = newSingleThreadExecutor("File-IO");
private static void setEnabledBlocking(Context appContext, Class<?> componentClass, boolean enabled) {
// 初始化组件对象
ComponentName component = new ComponentName(appContext, componentClass);
// 获取包管理者
PackageManager packageManager = appContext.getPackageManager();
int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED;
// 动态不杀死应用启用或者禁用组件,若enabled为true则启用,否则禁用
packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP);
}
private static void executeOnFileIoThread(Runnable runnable) {
fileIoExecutor.execute(runnable);
}
private static Executor newSingleThreadExecutor(String threadName) {
return Executors.newSingleThreadExecutor(new SingleThreadFactory(threadName));
}
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);
}
});
}
从上述代码中,可以看出来setEnabled方法,通过参数:BlockCanaryContext.get().displayNotification(),来设置DisplayActivity组件(用于显示记录的异常信息给开发者)是否启用。
- BlockCanaryContext.get()返回的实际上就是第一步中所说到的我们自定义的AppBlockCanaryContext对象的引用变量sInstance,因此,若我们自定义的AppBlockCanaryContext中定义了displayNotification()方法,则按照我们自己定义的执行,若没有定义则按照其父类,即BlockCanaryContext中的displayNotification()方法返回值执行,默认返回为true。
- setEnabled方法中,通过executeOnFileIoThread方法,使用静态常量fileIoExecutor线程池执行异步任务,根据我们传入的enabled(是否允许启用组件标识),来最终启用或者禁用对应组件。关于动态启用或者禁用组件可参考:Android动态启用和禁用四大组件。
3. get()返回单例BlockCanary对象
public static BlockCanary get() {
if (sInstance == null) {
synchronized (BlockCanary.class) {
if (sInstance == null) {
sInstance = new BlockCanary();
}
}
}
return sInstance;
}
private BlockCanary() {
// 将BlockCanaryContext.get(),即sInstance(我们自定义的AppBlockCanaryContext)
// 设置到BlockCanary核心类BlockCanaryInternals中,用来获取我们自定义配置的信息
BlockCanaryInternals.setContext(BlockCanaryContext.get());
// 初始化BlockCanaryInternals
mBlockCanaryCore = BlockCanaryInternals.getInstance();
// 添加拦截器(将自定义的AppBlockCanaryContext添加到拦截器中,可回调其onBlock方法)
mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
// 根据我们自定义的AppBlockCanaryContext获取是否展示通知,默认为true
if (!BlockCanaryContext.get().displayNotification()) {
return;
}
// 若允许展示通知,则将DisplayService继续添加到拦截器中
mBlockCanaryCore.addBlockInterceptor(new DisplayService());
}
我们从这部分源码中看到,BlockCanary的构造方法中完成了其核心类:BlockCanaryInternals的初始化与设置(包括sInstance传入和添加拦截器),那么我们再来看一看BlockCanaryInternals的初始化都有些什么操作:
// BlockCanaryInternals类:
static BlockCanaryInternals getInstance() {
if (sInstance == null) {
synchronized (BlockCanaryInternals.class) {
if (sInstance == null) {
sInstance = new BlockCanaryInternals();
}
}
}
return sInstance;
}
public BlockCanaryInternals() {
// 初始化堆栈采样器
stackSampler = new StackSampler(
Looper.getMainLooper().getThread(),
sContext.provideDumpInterval());
// 初始化cpu采样器
cpuSampler = new CpuSampler(sContext.provideDumpInterval());
// 设置监视器,传入LooperMonitor looper监控器
// LooperMonitor 实际上就是我们上面【BlockCanary原理】中讲到的Printer接口的子类
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) {
// 遍历所有拦截器成员,调用每个成员的onBlock,并将卡顿信息传入
for (BlockInterceptor interceptor : mInterceptorChain) {
interceptor.onBlock(getContext().provideContext(), blockInfo);
}
}
}
}
}, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));
LogWriter.cleanObsolete();
}
private void setMonitor(LooperMonitor looperPrinter) {
// setMonitor把创建的LooperMonitor赋值给BlockCanaryInternals的成员变量monitor。
monitor = looperPrinter;
}
BlockCanaryInternals的构造方法中,初始化了几个变量,包括:堆栈采样器、cpu采样器、looper监控器,以及looper监控器的回调方法onBlockEvent。
2. BlockCanary.start()
1. 监控卡顿
public void start() {
if (!mMonitorStarted) {
mMonitorStarted = true;
// 设置Looper中的mLogging,每次消息处理前后,
// 都可回调自定义的实现Printer接口LooperMonitor类的println方法
Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
}
}
将在BlockCanaryInternals中创建的LooperMonitor给主线程Looper的mLogging变量赋值。这样主线程Looper就可以消息分发前后使用LooperMonitor#println输出日志。此时BlockCanary已经开始监控卡顿情况,所以我们现在需要关注的就是LooperMonitor的println方法。
再回顾一下Looper的loop方法:
//Looper
for (;;) {
Message msg = queue.next();
// 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);
}
...
}
Lopper的loop方法中logging现在就是BlockCanary中实现了Printer接口的LooperMonitor类。
// LooperMonitor类:
private boolean mPrintingStarted = false;
@Override
public void println(String x) {
if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
return;
}
if (!mPrintingStarted) {
// 获取消息处理前系统当前时间
mStartTimestamp = System.currentTimeMillis();
// 获取当前线程运行时间
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
// 将此标识置为true,下此进入就是消息处理之后
mPrintingStarted = true;
// 开始获取堆栈信息
startDump();
} else {
// 获取消息处理后系统当前时间
final long endTime = System.currentTimeMillis();
// 将此标识置为true,下此进入就是下一条消息处理之前
mPrintingStarted = false;
// 判断是否发生卡顿
if (isBlock(endTime)) {
// 发生卡顿,通知卡顿事件发生
notifyBlockEvent(endTime);
}
// 停止获取堆栈信息
stopDump();
}
}
对于每一个Message消息而言,println方法都是按顺序成对出现的,因此根据mPrintingStarted是否是消息开始前的标识,来判断此消息当前的处理前后两种状态。下面我们来看一下卡顿发生的情况:
// LooperMonitor类:
private boolean isBlock(long endTime) {
// 判断消息执行时间是否超过阈值
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
// 若超过阀值,则通知卡顿事件
private void notifyBlockEvent(final long endTime) {
final long startTime = mStartTimestamp;
final long startThreadTime = mStartThreadTimestamp;
// 获取消息处理结束后线程运行时间
final long endThreadTime = SystemClock.currentThreadTimeMillis();
// HandlerThreadFactory异步线程Looper的Handler
HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
@Override
public void run() {
// 异步线程中执行onBlockEvent回调
mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
}
});
}
通过消息执行的前后时间差 – 我们自定义AppBlockCanaryContext中设置的卡顿阀值,来确定是否发生卡顿,卡顿后的回调消息是在设置为异步线程Looper的Handler中执行。
// BlockCanaryInternals类构造方法中:
setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {
@Override
public void onBlockEvent(long realTimeStart, long realTimeEnd,
long threadTimeStart, long threadTimeEnd) {
// 根据开始及结束时间,从堆栈采集器的map当中获取记录信息
ArrayList<String> threadStackEntries = stackSampler
.getThreadStackEntries(realTimeStart, realTimeEnd);
if (!threadStackEntries.isEmpty()) {
// 构建 BlockInfo对象,设置相关的信息
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()));
最后若拦截器成员中存在DisplayService,则会发送前台的通知,代码如下:
// DisplayService类:
@Override
public void onBlock(Context context, BlockInfo blockInfo) {
Intent intent = new Intent(context, DisplayActivity.class);
intent.putExtra("show_latest", blockInfo.timeStart);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, FLAG_UPDATE_CURRENT);
String contentTitle = context.getString(R.string.block_canary_class_has_blocked, blockInfo.timeStart);
String contentText = context.getString(R.string.block_canary_notification_message);
// 根据不同的sdk兼容所有版本的通知栏显示
show(context, contentTitle, contentText, pendingIntent);
}
2. 卡顿信息记录
// LooperMonitor类:
private void startDump() {
if (null != BlockCanaryInternals.getInstance().stackSampler) {
// 开始记录堆栈信息
BlockCanaryInternals.getInstance().stackSampler.start();
}
if (null != BlockCanaryInternals.getInstance().cpuSampler) {
// 开始记录cpu信息
BlockCanaryInternals.getInstance().cpuSampler.start();
}
}
private void stopDump() {
if (null != BlockCanaryInternals.getInstance().stackSampler) {
// 停止记录堆栈信息
BlockCanaryInternals.getInstance().stackSampler.stop();
}
if (null != BlockCanaryInternals.getInstance().cpuSampler) {
// 停止记录cpu信息
BlockCanaryInternals.getInstance().cpuSampler.stop();
}
}
public void start() {
// mShouldSample实际上是AtomicBoolean原子布尔值。
if (mShouldSample.get()) {
return;
}
// 原子布尔值,能够保证在高并发的情况下只有一个线程能够访问这个属性值。
// 原子布尔值具体详情,参考:https://www.jianshu.com/p/8a44d4a819bc
mShouldSample.set(true);
// 移除上一次任务
HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
// 延迟 卡顿阀值*0.8 的时间执行相应信息的收集
HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
BlockCanaryInternals.getInstance().getSampleDelay());
}
public void stop() {
if (!mShouldSample.get()) {
return;
}
mShouldSample.set(false);
// 移除任务
HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
}
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
// 调用doSample方法,执行相应操作
doSample();
// 若此原子布尔值为true,即此时为开始记录堆栈信息
if (mShouldSample.get()) {
// 延迟 卡顿阀值 时间执行任务
HandlerThreadFactory.getTimerThreadHandler()
.postDelayed(mRunnable, mSampleInterval);
}
}
};
// BlockCanaryInternals类:
long getSampleDelay() {
// 卡顿阀值的0.8
return (long) (BlockCanaryInternals.getContext().provideBlockThreshold() * 0.8f);
}
卡顿信息的记录,实际上是通过CpuSampler和StackSampler两者相同父类AbstractSampler类,提供的方法start和stop记录,而start方法中通过HandlerThreadFactory获取异步的TimerThreadHandler发送延时消息,最后分别调用CpuSampler类和StackSampler类中,继承自AbstractSampler抽象方法doSample()完成的卡顿信息的记录。下面分别看一下CpuSampler类和StackSampler类的doSample()方法的实现。
-
StackSampler类的doSample()方法
private static final LinkedHashMap<Long, String> sStackMap = new LinkedHashMap<>(); @Override protected void doSample() { StringBuilder stringBuilder = new StringBuilder(); // mCurrentThread.getStackTrace():返回一个表示该线程堆栈转储的堆栈跟踪元素数组。 // 通过mCurrentThread.getStackTrace()获取StackTraceElement,加入到StringBuilder for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) { stringBuilder .append(stackTraceElement.toString()) .append(BlockInfo.SEPARATOR); } synchronized (sStackMap) { // Lru算法,控制LinkHashMap的长度,移除最早添加进来的数据 if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) { sStackMap.remove(sStackMap.keySet().iterator().next()); } // 以当前系统时间为key,存储此处的堆栈信息 sStackMap.put(System.currentTimeMillis(), stringBuilder.toString()); } }
-
CpuSampler类的doSample()方法
// 主要通过获取/proc/stat文件 去获取cpu的信息 @Override protected void doSample() { BufferedReader cpuReader = null; BufferedReader pidReader = null; try { // 通过bufferReader读取 /proc 下的cpu文件 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(); } // 通过bufferReader读取 /proc 下的内存文件 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); } } } private void parse(String cpuRate, String pidCpuRate) { String[] cpuInfoArray = cpuRate.split(" "); if (cpuInfoArray.length < 9) { return; } long user = Long.parseLong(cpuInfoArray[2]); long nice = Long.parseLong(cpuInfoArray[3]); long system = Long.parseLong(cpuInfoArray[4]); long idle = Long.parseLong(cpuInfoArray[5]); long ioWait = Long.parseLong(cpuInfoArray[6]); long total = user + nice + system + idle + ioWait + Long.parseLong(cpuInfoArray[7]) + Long.parseLong(cpuInfoArray[8]); String[] pidCpuInfoList = pidCpuRate.split(" "); if (pidCpuInfoList.length < 17) { return; } long appCpuTime = Long.parseLong(pidCpuInfoList[13]) + Long.parseLong(pidCpuInfoList[14]) + Long.parseLong(pidCpuInfoList[15]) + Long.parseLong(pidCpuInfoList[16]); if (mTotalLast != 0) { StringBuilder stringBuilder = new StringBuilder(); long idleTime = idle - mIdleLast; long totalTime = total - mTotalLast; stringBuilder .append("cpu:") .append((totalTime - idleTime) * 100L / totalTime) .append("% ") .append("app:") .append((appCpuTime - mAppCpuTimeLast) * 100L / totalTime) .append("% ") .append("[") .append("user:").append((user - mUserLast) * 100L / totalTime) .append("% ") .append("system:").append((system - mSystemLast) * 100L / totalTime) .append("% ") .append("ioWait:").append((ioWait - mIoWaitLast) * 100L / totalTime) .append("% ]"); synchronized (mCpuInfoEntries) { mCpuInfoEntries.put(System.currentTimeMillis(), stringBuilder.toString()); if (mCpuInfoEntries.size() > MAX_ENTRY_COUNT) { for (Map.Entry<Long, String> entry : mCpuInfoEntries.entrySet()) { Long key = entry.getKey(); mCpuInfoEntries.remove(key); break; } } } } mUserLast = user; mSystemLast = system; mIdleLast = idle; mIoWaitLast = ioWait; mTotalLast = total; mAppCpuTimeLast = appCpuTime; }
Android平台CPU的一些常识:
- Android是基于Linux系统的,Android平台关于CPU的计算是跟Linux是完全一样的。
- 在Linux中CPU活动信息是保存在/proc/stat文件中,该文件中的所有值都是从系统启动开始累计到当前时刻。
- /proc/stat文件内容:
> cat /proc/stat 1. cpu 2255 34 2290 22625563 6290 127 456 2. cpu0 1132 34 1441 11311718 3675 127 438 3. cpu1 1123 0 849 11313845 2614 0 18 4. intr 114930548 113199788 3 0 5 263 0 4 [... lots more numbers ...] 5. ctxt 1990473 6. btime 1062191376 7. processes 2915 8. procs_running 1 9. procs_blocked 0
这些数字指明了CPU执行不同的任务所消耗的时间(从系统启动开始累计到当前时刻)。时间单位是USER_HZ或jiffies(通常是百分之一秒)。
- 解析3中第一行各数值的含义
参数 解析 (以下数值都是从系统启动累计到当前时刻)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文件:包含了某一进程所有的活动的信息,该文件中的所有值都是从系统启动开始累计到当前时刻
cat /proc/6873/stat 6873 (a.out) R 6723 6873 6723 34819 6873 8388608 77 0 0 0 41958 31 0 0 25 0 3 0 5882654 1409024 56 4294967295 134512640 134513720 3215579040 0 2097798 0 0 0 0 0 0 0 17 0 0 0
计算CPU使用率有用相关参数:
参数 解析 pid=6873 进程号 utime=1587 该任务在用户态运行的时间,单位为jiffies stime=41958 该任务在核心态运行的时间,单位为jiffies cutime=0 所有已死线程在用户态运行的时间,单位为jiffies cstime=0 所有已死在核心态运行的时间,单位为jiffies
结论:进程的总CPU时间processCpuTime = utime + stime + cutime + cstime,该值包括其所有线程的CPU时间。
3. 卡顿日志记录
卡顿发生时,会回调LooperMonitor的onBlockEvent方法,而此方法中,会将卡顿信息写入本地日志文件,日志的路径在自定义的AppBlockCanaryContext中定义。
// BlockCanaryInternals类构造方法中:
setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {
@Override
public void onBlockEvent(long realTimeStart, long realTimeEnd,
long threadTimeStart, long threadTimeEnd) {
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类:
public static String save(String str) {
String path;
synchronized (SAVE_DELETE_LOCK) {
path = save("looper", str);
}
return path;
}
private static String save(String logFileName, String str) {
String path = "";
BufferedWriter writer = null;
try {
// 根据开发者自己配置的日志存储路径,生成文件
File file = BlockCanaryInternals.detectedBlockDirectory();
long time = System.currentTimeMillis();
path = file.getAbsolutePath() + "/"
+ logFileName + "-"
+ FILE_NAME_FORMATTER.format(time) + ".log";
// 写入卡顿信息
OutputStreamWriter out =
new OutputStreamWriter(new FileOutputStream(path, true), "UTF-8");
writer = new BufferedWriter(out);
writer.write(BlockInfo.SEPARATOR);
writer.write("**********************");
writer.write(BlockInfo.SEPARATOR);
writer.write(TIME_FORMATTER.format(time) + "(write log time)");
writer.write(BlockInfo.SEPARATOR);
writer.write(BlockInfo.SEPARATOR);
writer.write(str);
writer.write(BlockInfo.SEPARATOR);
writer.flush();
writer.close();
writer = null;
} catch (Throwable t) {
Log.e(TAG, "save: ", t);
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (Exception e) {
Log.e(TAG, "save: ", e);
}
}
return path;
}
BlockCanary卡顿参数解读
- cpuCore:手机cpu个数。
- processName:应用包名。
- freeMemory: 手机剩余内存,单位KB。
- totalMemory: 手机内训总和,单位KB。
- timecost: 该Message(事件)执行时间,单位 ms。
- threadtimecost: 该Message(事件)执行线程时间(线程实际运行时间,不包含别的线程占用cpu时间),单位 ms。
- cpubusy: true表示cpu负载过重,false表示cpu负载不重。cpu负载过重导致该Message(事件) 超时,错误不在本事件处理上。
至此,BlockCanary的整体已分析完成,收工咯。
参考链接
…
注:若有什么地方阐述有误,敬请指正。期待您的点赞哦!!!
今天的文章主流开源框架之BlockCanary深入了解分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/19795.html