这次疫情还没有过去,但是生活依旧,还是要工作,还是要挣钱,在这里多的话不说,“武汉加油!”闲言少叙,我们直接走入主题,我们在开发复杂项目的时候,代码的迭代,修改等,都会出现UI卡顿,或者出现ANR的时候,造成的程序崩溃,等,我们如何定位到卡顿的位置等,所以国内开发者,给我送来一个福利,BlockCanary这个框架。
1.介绍 BlockCanary
BlockCanary 这个框架是android平台,非侵入式的性能监控组件。使用时提供一个抽象类,传一个上下文环境就可以使用了,使用方便.
2.UI卡顿问题
原理:
在android开发中,我们的APP的帧频性能最优的目标就是保持在60fps上。
60fps --->16ms/帧
所以我们尽量保证每次在16ms内处理完所有的CPU与GPU计算,绘制,渲染等操作,否则会造成丢帧卡顿问题。
3.UI卡顿的原因分析
1.UI线程中做耗时操作
1)主线程的作用:把事件分发给合适的view或者widget
解决办法:我们通过handler在子线程中做耗时操作
runOnUiThread方法:
View.post 方法
VIew.postDelayed方法
2.布局layout过于复杂,没办法在16ms中完成渲染
3.View的过度绘制,由于过度绘制导致在同一帧重复绘制
4.view频繁的触发measure,layout
5.内存频繁触发GC过多(在同一帧内频繁的创建临时变量)
4.BlockCanary的简单实用
1)添加开源库的依赖:
compile 'com.github.markzhai:blockcanary-android:1.5.0'
2)在Application中注册我们的BlockCanary
BlockCanary.install(this, new AppBlockContext()).start();
3).创建一个类AppBlockContext 继承 BlockCanaryContext:
public class AppBlockContext extends BlockCanaryContext {
// 实现各种上下文,包括应用标示符,用户uid,网络类型,卡慢判断阙值,Log保存位置等
/**
* Implement in your project.
*
* @return Qualifier which can specify this installation, like version + flavor.
*/
public String provideQualifier() {
return "unknown";
}
/**
* Implement in your project.
*
* @return user id
*/
public String provideUid() {
return "uid";
}
/**
* Network type
*
* @return {@link String} like 2G, 3G, 4G, wifi, etc.
*/
public String provideNetworkType() {
return "unknown";
}
/**
* Config monitor duration, after this time BlockCanary will stop, use
* with {@code BlockCanary}'s isMonitorDurationEnd
*
* @return monitor last duration (in hour)
*/
public int provideMonitorDuration() {
return -1;
}
/**
* Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
* from performance of device.
*
* @return threshold in mills
*/
public int provideBlockThreshold() {
return 1000;
}
/**
* Thread stack dump interval, use when block happens, BlockCanary will dump on main thread
* stack according to current sample cycle.
*
* Because the implementation mechanism of Looper, real dump interval would be longer than
* the period specified here (especially when cpu is busier).
*
*
* @return dump interval (in millis)
*/
public int provideDumpInterval() {
return provideBlockThreshold();
}
/**
* Path to save log, like "/blockcanary/", will save to sdcard if can.
*
* @return path of log files
*/
public String providePath() {
return "/blockcanary/";
}
/**
* If need notification to notice block.
*
* @return true if need, else if not need.
*/
public boolean displayNotification() {
return true;
}
/**
* Implement in your project, bundle files into a zip file.
*
* @param src files before compress
* @param dest files compressed
* @return true if compression is successful
*/
public boolean zip(File[] src, File dest) {
return false;
}
/**
* Implement in your project, bundled log files.
*
* @param zippedFile zipped file
*/
public void upload(File zippedFile) {
throw new UnsupportedOperationException();
}
/**
* Packages that developer concern, by default it uses process name,
* put high priority one in pre-order.
*
* @return null if simply concern only package with process name.
*/
public List concernPackages() {
return null;
}
/**
* Filter stack without any in concern package, used with @{code concernPackages}.
*
* @return true if filter, false it not.
*/
public boolean filterNonConcernStack() {
return false;
}
/**
* Provide white list, entry in white list will not be shown in ui list.
*
* @return return null if you don't need white-list filter.
*/
public List provideWhiteList() {
LinkedList whiteList = new LinkedList<>();
whiteList.add("org.chromium");
return whiteList;
}
/**
* Whether to delete files whose stack is in white list, used with white-list.
*
* @return true if delete, false it not.
*/
public boolean deleteFilesInWhiteList() {
return true;
}
/**
* Block interceptor, developer may provide their own actions.
*/
public void onBlock(Context context, BlockInfo blockInfo) {
}
}
5.BlockCanary的原理源码实现:
在ActivityThread中有一个main方法,在main方法中会创建一个Looper,在Looper当中会关联一个MessageQueue消息队列,主线程创建好MainLooper之后,他就会在应用的生命周期内不断的轮训,通过Looper.loop方法。然后获取到我们消息队列当中的message,最后通知我们的主线程去更新UI.
2)实现的核心原理
通过Hander.postMessage发送一个消息给主线程(sMainLooper.loop),主线程会通过轮训器Looper不断的轮训MessageQueue中的消息队列,通过queue.next方法获取消息队列中的消息,然后我们计算出调用dispatchMessage方法的前后时间值(T1,T2),通过T2减去T1的时间差来判断是否超过我们之前设定好的阈值,如果超过了我们设定的阈值,我们就dump出我们收集的信息,来定位我们UI卡顿的原因。
如果我们在调用dispatchmessage这个方法的时候,超过我们设定的阈值的0.8倍的时候,也会Dump出我们需要的信息。
3)我们通过源码进行分析
BlockCanary.install(this, new AppBlockContext()).start();
首先我们看看他的入口,install这个方法,我们点开:
/**
* Install {@link BlockCanary}
*
* @param context Application context
* @param blockCanaryContext BlockCanary context
* @return {@link BlockCanary}
*/
public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
BlockCanaryContext.init(context, blockCanaryContext);
setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
return get();
}
这里调用三行代码.我们接着点init方法:
static void init(Context context, BlockCanaryContext blockCanaryContext) {
sApplicationContext = context;
sInstance = blockCanaryContext;
}
这个init方法就做了一个赋值的操作,将我们传递过来的context进行赋值。
我们返回到install方法中。看setEnabled方法:
更据用户的通知栏消息,来开启或者关闭展示我们BlockCanary这个消息界面.
我们看这个方法的第三个参数,dispalyNotification这个方法就是决定开启或者关闭:
/**
* If need notification to notice block.
*
* @return true if need, else if not need.
*/
public boolean displayNotification() {
return true;
}
这里默认返回的true。如果是debug方法是true,relese返回false
我们在看看get方法的实现:
/**
* Get {@link BlockCanary} singleton.
*
* @return {@link BlockCanary} instance
*/
public static BlockCanary get() {
if (sInstance == null) {
synchronized (BlockCanary.class) {
if (sInstance == null) {
sInstance = new BlockCanary();
}
}
}
return sInstance;
}
他其实就是一个单例模式,我们看看这个BlockCanary是如何实现的那?
private BlockCanary() {
BlockCanaryInternals.setContext(BlockCanaryContext.get());
mBlockCanaryCore = BlockCanaryInternals.getInstance();
mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
if (!BlockCanaryContext.get().displayNotification()) {
return;
}
mBlockCanaryCore.addBlockInterceptor(new DisplayService());
}
BlockCanaryInternals.setContext,做了一个赋值操作,
mBlockCanaryCode这个类型的变量就是BlockCanaryInternals,我们看一下getInstance方法:
/**
* Get BlockCanaryInternals singleton
*
* @return BlockCanaryInternals instance
*/
static BlockCanaryInternals getInstance() {
if (sInstance == null) {
synchronized (BlockCanaryInternals.class) {
if (sInstance == null) {
sInstance = new BlockCanaryInternals();
}
}
}
return sInstance;
}
一个单例模式完成了BlockCanaryInternals的实例化。
我们接着看addBlockInterceptor这行代码:
这是一个拦截器,传入一个上下文
主要代码,判断是否开启,展开这个拦截器,通知我们的DisplayActivity。
接下来我们看一看BlockCanaryInternals这个类(这个类是一个核心类)
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 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)stackSampleer
参数一:传入我们的主线程
参数二:Dump的间隔时间
2) cpuSampler
他会dump出我们cup的一些情况
3)LooperMonitor
这是一个非常重要的东西,如何打印上下时间(T1,T2),就是通过它控制的,然后通过onBlockEvent回调监听并打印数据。
4)cleanObsolete这个方法就是删除我们打印的日志
上边讲解的是BlockCanary的install初始化的,接下来我们讲解start()的打印是如何打印的:
/**
* Start monitoring.
*/
public void start() {
if (!mMonitorStarted) {
mMonitorStarted = true;
Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
}
}
最关键的代码:就是Looper.getMainLooper这行代码:调取主线程的setMessageLogging方法,来打点我们时间。
我们接下来看看代码Monitor是如何实现的:
这个类实现了Printer这个接口。
这个类中重要的方法是:
@Override
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();
}
}
这个方法就是用来打点时间的,
首先看代码实现:
首先判断dispacthMessage这个方法之前调用的,如果是就会记录开始时间,调用这个startDump这个方法,来打印出我们的堆栈信息。
接下来我们看看这个startDump这个方法的实现:
private void startDump() {
if (null != BlockCanaryInternals.getInstance().stackSampler) {
BlockCanaryInternals.getInstance().stackSampler.start();
}
if (null != BlockCanaryInternals.getInstance().cpuSampler) {
BlockCanaryInternals.getInstance().cpuSampler.start();
}
}
这个方法主要就是通过BlockCanaryInternals中的stackSampler和cpuSampler分别打印出重要信息。
我们继续深入,看看他们的start方法实现:
public void start() {
if (mShouldSample.get()) {
return;
}
mShouldSample.set(true);
HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
BlockCanaryInternals.getInstance().getSampleDelay());
}
这里没啥说的,主要看看postDelayed这个方法的第一个参数,mRunnalbe:
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
doSample();
if (mShouldSample.get()) {
HandlerThreadFactory.getTimerThreadHandler()
.postDelayed(mRunnable, mSampleInterval);
}
}
};
我们在看看doSample是什么?
abstract void doSample();
这是一个抽象方法,这也就是意味着stackSampler和cpuSampler是有不同实现的,
我们接着看看这个抽象方法的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());
}
}
到这里就是真正要打印的数据了:
我们看最后一行代码:执行了打印,第一个参数是以我们的当前时间戳为例,并放到HashMap当中,我们看看是什么HashMap?
private static final LinkedHashMap sStackMap = new LinkedHashMap<>();
他是一个linkHashMap: 为什么要用这个HashMap,因为这个LinkHashMap能够记录插入的顺序。
所以这里是按着先后顺序插入的,
我们回到这Printer这个接口的println方法:
@Override
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();
}
}
我们看看这个isBlock这个方法:
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
到这里我们就明白了,这里是不是就是我们BlockCanary的核心原理,T2减去T1的时间,并判断是否打印并返回true,就会执行notifyBlockEvent,我们看看这个实现:
private void notifyBlockEvent(final long endTime) {
final long startTime = mStartTimestamp;
final long startThreadTime = mStartThreadTimestamp;
final long endThreadTime = SystemClock.currentThreadTimeMillis();
HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
@Override
public void run() {
mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
}
});
}
我们看看这个方法中的LooperMonitor,this这个代码,onBlockEvent是不是就是我们前面的监听时间回调?
到这里是不是就把我们要打印的数据通过这个回调方法返回去了.所以到这里我们的分析就全部完成了。如果对您有帮助,麻烦关注我一下,我会给你继续带来干货。