BlockCanary
预善其事必先利其器,blockcancary是一个卡顿检测框架
如果卡顿说明,我们在主线程做了一下耗时操作
如果了解 handle原理,就可以知道Android应用程序的主线程消息循环(Message Loop)会通过 dispatchMessage()
方法处理消息。主线程的消息循环是由 Looper
类管理的,它负责接收消息并将其分发给对应的处理器(Handler)进行处理。
以下是主线程消息循环的基本工作原理:
- 当应用程序启动时,主线程会创建一个
Looper
对象,并调用Looper.prepare()
方法初始化消息循环。 - 在消息循环运行之前,需要调用
Looper.loop()
方法,它会不断地从消息队列中取出消息并将其分发给对应的处理器。 - 当有消息到达时,
Looper
会将消息传递给Handler
对象,然后调用dispatchMessage()
方法将消息传递给Handler
的handleMessage()
方法。 handleMessage()
方法是在主线程中执行的,可以根据消息的类型和内容来执行相应的操作。
所以,如果我们想知道主线程是否卡顿,我们只需要知道消息的执行(dispatchMessage)时间就行了
非常巧的是,Android的sdk为我们预留了方法,在消息处理之前,和处理之后都调用了logging.println()方法
java
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
.....省略
final 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);
}
}
这样通过实现自定义的Printer,就可以获取主线程每次消息的执行时间,同时这也是BlockCanary开源库的实现原理,不过由于这个库已经很多年没维护了,内部一些api没有适配高版本的Androidsdk,所以直接依赖无法使用了
这里原理还是很简单的,相比于去把开源库的源码拉下来自己适配,我们也可以写一个简易版的卡顿检测方法
kotlin
private fun startBlockTest(){
val mainThread=Thread.currentThread()
val handlerThread=HandlerThread("blockTest")
handlerThread.start()//开启线程
val handler=Handler(handlerThread.looper)
val stringBuilder=StringBuilder()
val runnable= Runnable {
for (stackTraceElement in mainThread.stackTrace){
stringBuilder
.append(stackTraceElement.toString())
.append("\n")
}
}
Looper.getMainLooper().setMessageLogging(object : Printer {
var start:Boolean=true
var startTime:Long=0
override fun println(x: String?) {
if (start){//dispatch之前
start=false
startTime=System.currentTimeMillis()
handler.postDelayed(runnable,800)
}else{//dispatch之后
start=true
val gap = System.currentTimeMillis() - startTime
if (gap>=1000) {//耗时超过一秒
Timber.tag("block").d("有耗时操作出现" + gap + "ms" + x)
Timber.tag("block").d(stringBuilder.toString())
}
handler.removeCallbacks(runnable)
stringBuilder.clear()
}
}
})
}
可以看到10多行代码还是很简洁的,直接在application里面调用就行
主要思路呢就是
- 重写println 方法,
- 在dispatch 之前记录开始时间,如果耗时超过800ms就导出主线程的线程栈信息
- 在dispatch之后计算消息执行的耗时,如果事件间隔超过定义的事件间隔就在控制台打印栈信息,以便定位卡顿
q:为什么导出堆信息需要延迟执行?
a:避免卡顿,导出一个线程的栈是需要暂停线程的,只有当时间接近我们定义的卡顿时间才进行导出操作。比如我定义的1000ms算卡顿,800ms进行导出栈信息
实际效果如图所示,在点击事件中进行延迟操作,点击打印的栈信息 能够定位到代码的位置
但是注意,在Jetpack Compose中,点击事件处理不依赖于传统的消息分发机制。相反,Compose使用基于函数的更现代方法来处理点击事件。所以正常来说compose中并没有作用(可以使用asm字节码插桩的方式),所以这就是为什么我使用了post来延迟的原因,但是在传统View体系还是可以的