Android卡顿监控

一、概述

如果想要自己实现一个简单的卡顿监控功能,可以看下这篇文章。我们都知道,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 $count\n".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方法,出现栈溢出。

猜你喜欢

转载自blog.csdn.net/ganduwei/article/details/108359358