前言
面试中经常会被问到这样一个问题或者它的类似问题。
你如何用程序的方式检测导致UI卡顿的问题,并且定位?
思考
我个人觉得这其实是一个还不错的综合体,考察点也还算多,也可以衍生出一些题目。
先来拆解一下关键词,程序的方式检测、UI卡顿、定位:
- 程序的方式检测:不是通过图形工具、不是通过交互来检测,而是通过编写工具类在应用运行时来检测
- UI卡顿:UI线程做了一些耗时的操作
- 定位:通过日志打印出耗时超过阈值的方法
技术补完
StackTrace&StackTraceElement
先来堆栈追踪
部分的知识点,因为这块 api 用的多,但了解少。
StackTrace
StackTrace(堆栈跟踪),用栈的形式保存方法的调用信息。
这个其实我们一点也不陌生,在处理异常的时候,经常会这么写一句:e.printStackTrace()
,即打印调用异常的堆栈信息,方便我们在异常发生的时候能定位到具体的问题。
StackTraceElement
1 | /** |
获取StackTraceElement
的方法有2种,返回值为StackTraceElement[]
。
- Thread#getStackTrace(),常见代码
Thread.currentThread().getStackTrace()
- Throwable#getStackTrace()
StackTraceElement[]
包含了StackTrace
的内容,遍历它可以得到方法见的调用过程,即可以得到当前方法以及其调用者的方法名、调用行数等信息。
1 |
|
输出,如图:
Looper
主线程里有个Looper
,在loop
方法中会不断取出Message
,调用其绑定的 Handler 在主线程执行。
1 | loop(){ |
关键点,我们只要有办法检测:msg.target.dispatchMessage(msg)
的执行时间,判断执行时间是否大于我们设置的某个阈值就可以判断UI线程是否有耗时操作了。此代码执行前后,如果设置了logging
,会分别打印出:>>>>> Dispatching to
和 <<<<< Finished to
这样的日志。
实操
BlockCanary
1 |
|
LoggerMonitor
1 |
|
假设我们的阈值设置为 1000ms,当str
匹配到>>> Dispatching
时,调用LoggerMonitor.INSTANCE.start()
,在 1000ms后执行一个任务,打印出UI线程的堆栈信息(这次在子线程中执行)。正常情况,我们的msg.target.dispatchMessage(msg)
执行耗时小于 1000ms,所以str
匹配到<<<<< Finished
,就会调用LoggerMonitor.INSTANCE.remove()
移除任务。
这里还用到了 HandlerThread,也利用了 Looper,只不过这个 Looper 在子线程中。
测试环节
Application#onCreate
1 | class DemoApplication : Application() { |
耗时模拟
1 | btn_test.setOnClickListener { |
点击按钮,打印出 log:
1 | 2020-03-10 01:40:10.509 4740-4765/info.hellovass.build_example E/TAG: java.lang.Thread.sleep(Native Method) |
会打印出耗时相关的代码信息,然后可以通过日志定位到耗时的方法。
Choreographer???
2020-03-10 01:40:11.516 4740-4740/info.hellovass.build_example I/Choreographer: Skipped 119 frames! The application may be doing too much work on its main thread.
这个日志似乎怎么感觉那么熟悉呢?其实日常开发中,如果我们操作不当(UI线程里执行耗时的操作)就能看到。而这个Choreographer
,好像和啥 Android
、16ms绘制一帧
等关键词有着说不清的关系。
没错,正是在下。
Android 系统每隔 16ms 发出 VSYNC 信号,触发对 UI 的渲染。Android SDK 包含了一个相关类,以及相应的回调。理论上来说,两次回调的时间周期应该在 16ms以内,超过了,我们则认为是一次造成卡顿的耗时操作,于是:
1 | class BlockCanary2 private constructor() { |
第一次的时候开始检测,如果超过阈值则输出相关堆栈信息;否则移除。
衍生
这就完了嘛?兄dei,想多了,如果生产环境的性能检测工具这么写,那真的就是面向离职编程了2333。
实际要考虑的问题简直不要太多!
- Handler#hasCallbacks 方法仅支持 Android Q 及以上
- 阈值为1000ms,第一个方法耗时980ms,第二个方法耗时20ms,这时候打印的堆栈是20ms的方法的堆栈
- 阈值还是为1000ms,第一个方法耗时500ms,第二个方法耗时500ms,这时候只打印了第二个方法的堆栈
- 怎么把这些堆栈信息同步到远端
- …
写在最后
突然想起好久之前看到过@markzhai发布过一个检测UI卡顿的工具,但当时年轻不懂其中的奥秘,连随手star都忘了。
这次一定!