概述
一、概述
如果想要自己实现一个简单的卡顿监控功能,可以看下这篇文章。我们都知道,Android程序 是基于事件驱动的,程序主线程一直在执行Looper的loop,loop的循环不断的读取事件进行处理,没有事件就等待着,退出程序也就退出这个循环。在Looper中,处理消息前后会打印log:
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
...
try {
msg.target.dispatchMessage(msg);
...
} finally {
...
}
...
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
我们给主线程的Looper设置一个Printer,这样可以根据前后打印log的时间来获取事件的处理时长,如果处理时间过长,那就导致不能及时处理下一个事件,自然也就会导致出现卡顿或者ANR,所以当时长超过一定阈值时,我们就记录到日志文件中,从而方便我们排查问题。
二、代码实现
下面看看具体的代码实现:
/**
* 监控 Message 执行时长,检测卡顿
*/
class BlockCatch(context: Context) {
private val startSymbol = ">>>>> Dispatching"
private val endSymbol = "<<<<< Finished"
private val thresholdValue = 600L//message处理完成的时间超过多少毫秒认为卡顿
private val writeLogThreshold = 3000L//message处理时间超过多少毫秒还是没有处理,记录日志,以免出现anr时没有msgEnd导致日志不会写入
private val collectLogGap = 32L//收集主线程堆栈的时间间隔毫秒
private val msgStart = 1
private val msgCollectLog = 2
private val msgEnd = 3
private val handlerThread: HandlerThread = HandlerThread("BlockCatch")
private var handler: Handler? = null
private val logFilePath: String? = context.externalCacheDir?.path
private var lastFrameTimeNanos: Long = 0
init {
if (!logFilePath.isNullOrEmpty()) {
handlerThread.start()
handler = object : Handler(handlerThread.looper) {
private val logMap = SparseArray<String>()//存储主线程堆栈信息
private val countMap = SparseIntArray()//存储堆栈信息的计数
private var startTime = 0L
override fun handleMessage(msg: Message) {
when (msg.what) {
msgStart -> {
startTime = msg.obj as Long
sendEmptyMessageDelayed(msgCollectLog, collectLogGap)
}
msgCollectLog -> {
Log.i("TAG", "msgCollectLog -------------------------->")
val stackTrace = Looper.getMainLooper().thread.stackTrace
val sb = StringBuilder()
stackTrace.forEach {
sb.append(it.toString()).append("n")
}
val string = sb.toString()
val hash = string.hashCode()
val count = countMap.get(hash)
if (count == 0) {
logMap.put(hash, string)
}
countMap.put(hash, count + 1)
sendEmptyMessageDelayed(msgCollectLog, collectLogGap)
}
msgEnd -> {
val time = msg.obj as Long
val delay = time - startTime
if (delay >= thresholdValue) {//消息处理时间超过thresholdValue
try {
val file = File(logFilePath, "BlockCatchLog.txt")
val fileOutputStream = FileOutputStream(file, true)
fileOutputStream.write("BlockCatch: t $time d $delay-->n".toByteArray())
for (i in 0 until logMap.size()) {
val key = logMap.keyAt(i)
val count = countMap.get(key)
if (count > 1) {
fileOutputStream.write(logMap.get(key).toByteArray())
fileOutputStream.write("<-c $countn".toByteArray())
} else {
Log.i("TAG", "msgBlock: skip")
}
}
fileOutputStream.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
logMap.clear()
countMap.clear()
Log.i("TAG", "msgBlock ${logMap.size()} $time<---------------")
}
}
}
}
Looper.getMainLooper().setMessageLogging { x ->
//
Log.i("TAG", "looper MessageLogging time ${System.currentTimeMillis()} $x")
if (x.startsWith(startSymbol)) {
handlerStart()
} else if (x.startsWith(endSymbol)) {
handlerEnd()
}
}
//
Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
//
override fun doFrame(frameTimeNanos: Long) {
//
Choreographer.getInstance().postFrameCallback(this)
//
if (lastFrameTimeNanos == 0L) {
//
lastFrameTimeNanos = frameTimeNanos
//
} else {
//
val diff = frameTimeNanos - lastFrameTimeNanos
//
lastFrameTimeNanos = 0
//
if (diff > 17000000) {
//
Log.i("TAG", "Choreographer doFrame diff " + diff / 1000000)
//
}
//
}
//
}
//
})
}
}
private fun handlerStart() {
val time = System.currentTimeMillis()
val msg = Message.obtain()
msg.obj = time
msg.what = msgStart
handler?.sendMessage(msg)
}
private fun handlerEnd() {
val time = System.currentTimeMillis()
handler?.removeMessages(msgCollectLog)
val msg = Message.obtain()
msg.obj = time
msg.what = msgEnd
handler?.sendMessage(msg)
}
}
在开始处理事件的时候,记录开始时间,开始收集主线程的栈信息,每间16毫秒收集一次。这样做是因为我们要知道处理消息过程中执行了哪些方法,如果在事件处理结束后,栈已经回退,这样就不知道这中间到底执行了哪个方法。
因为每16ms就获取一次栈信息,如果一个方法执行的时长有多个16ms,那就会被重复记录多次。我们只需要知道执行了哪些方法,以及方法执行了多少个16ms。这里用 logMap 来保存栈信息,countMap来保存多少个16ms,获取到栈信息后,计算它的hashCode,如果countMap没有这个hashCode的记录就保存到 logMap中,如果有就将它的次数加1。
在处理事件结束后,用结束时间与开始时间相减,判断是否大于 thresholdValue ,如果小于则表示没有耗时的方法,就清空countMap 和 hashCode 。如果大于,则将 hashCode 里面的内容写到文件中记录下来,完成后情况它们。
上面的这些操作都在HandlerThread子线程中,因为不能影响到主线程的工作,不然我们自己就成了那个导致卡顿的元凶了。
使用的话,只需要在Application中初始化就可以了。
这只是一个简单的监控,还有很多问题,比如无法监听到onKey,onClick方法里面的阻塞。(输入事件由native层调用InputEventSender的dispatchInputEventFinished方法开始处理,此时Looper阻塞在MessageQueue.nativePollOnce,也就是说MessageQueue此时没有消息可处理)
2022.11.14注:
该方法与协程不兼容,logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what) toString方法会导致协程里面不断的调用toString方法,出现栈溢出。
最后
以上就是洁净豆芽为你收集整理的Android卡顿监控一、概述二、代码实现的全部内容,希望文章能够帮你解决Android卡顿监控一、概述二、代码实现所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复