
本文共 8876 字,大约阅读时间需要 29 分钟。
性能优化,基本每位 Android 开发都需要考虑这个问题。像LeakCanary、hugo
这些都是大家常用的性能检测工具,而这次我要讲的是 BlockCanary,由于这个库很久没更新了,所以可能很多人不认识,但是这并不妨碍我们去理解它的实现原理。
OK,开始吹水正文

功能介绍
首先,先祭出GitHub地址
https://github.com/markzhai/AndroidPerformanceMonitor
该库的功能主要为检测在主线程运行的操作时长,假如运行超过一定时间(默认为1000毫秒),则会记录 block 信息并提示给开发者。
集成步骤
-
加上依赖
dependencies { // 无论是开发版还是正式版都会检测(不建议) // implementation 'com.github.markzhai:blockcanary-android:1.5.0' // 仅在开发版时检测 debugImplementation 'com.github.markzhai:blockcanary-android:1.5.0' releaseImplementation 'com.github.markzhai:blockcanary-no-op:1.5.0'}
-
在自定义的 Application 的 onCreate() 方法中进行初始化
class MyApplication : Application() { override fun onCreate() { super.onCreate() BlockCanary.install(this, BlockCanaryContext()).start() }}
-
别忘了在 AndroidMainfest.xml 中进行 Application 绑定
android:name=".MyApplication"
具体检测效果
-
先看看布局效果,就一个按钮
-
对按钮进行监听并点击后睡眠 2000 毫秒
fun clickView(view: View) { SystemClock.sleep(2000) }
-
点击后,BlockCanary 收集到的信息
是不是很神奇,能够定位到阻塞的位置和阻塞的时间。

使用总结
- 能够计算在主线程中运行方法的时长
- 定位运行的方法在代码上的位置
所以只要解决了以上两个问题,我们就可以自己手写个简单的 BlockCanary 了。
具体实现
运行时长
em…在主线程中的运行时长…主线程…主线程…
哦!

由于 ActivityThread 被 MainLooper 一直死循环,所以在主线程运行操作基本都是通过 post 消息到 MessageQueue 中,由 MainLooper 取出并执行,那我们可以在执行前记录时间 A,在执行后也记录时间 B,B - A 得到的便是运行时长!
好,我们来看看 Looper 的 loop() 源码:
public static void loop() { final Looper me = myLooper(); ··· for (;;) { ··· 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); } ··· } }

这不就是我们想要的吗,在执行方法前进行输出,在执行方法后再进行输出。
我们来看看 me.mLogging 是怎么赋值的。
me 调用 myLooper() 获取
public static @Nullable Looper myLooper() { return sThreadLocal.get(); }
所以 mLogging 其实就是当前 Looper 的 mLogging
private Printer mLogging; public void setMessageLogging(@Nullable Printer printer) { mLogging = printer; }
OK,思路我们有了,只要调用 setMessageLogging 进行赋值,并重写 Printer 的 println() 方法,到时 Looper 就会自动调用我们的 println() 方法了
完整代码:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initPrinter() } //使用 printerStart 进行判断是方法执行前还是执行后调用 println private var printerStart = true //记录方法执行前的时间 private var printerStartTime = 0L private fun initPrinter() { Looper.getMainLooper().setMessageLogging { if (printerStart) { printerStart = false printerStartTime = System.currentTimeMillis(); } else { printerStart = true Log.i("Printer", "方法运行的总时长:${System.currentTimeMillis() - printerStartTime}") } } } fun clickView(view: View) { SystemClock.sleep(2000) }}
运行 App,点击按钮:
com.example.testblockcanary I/Printer: 方法运行的总时长:1com.example.testblockcanary I/Printer: 方法运行的总时长:3com.example.testblockcanary I/Printer: 方法运行的总时长:3com.example.testblockcanary I/Printer: 方法运行的总时长:4com.example.testblockcanary I/Printer: 方法运行的总时长:3com.example.testblockcanary I/Printer: 方法运行的总时长:2009
可以看到,只要在主线程执行方法的时候,都会调用我们的 println() 方法,但是,我们只是为了检测耗时较长的方法即可,所以多加一个拦截:
//方法执行时间超过该值才输出 private val minTime = 1000L private fun initPrinter() { Looper.getMainLooper().setMessageLogging { if (printerStart) { printerStart = false printerStartTime = System.currentTimeMillis(); } else { printerStart = true (System.currentTimeMillis() - printerStartTime).let { if (it >= minTime){ Log.i("Printer", "方法运行的总时长:${it}") } } } } }
运行后,点击:
com.example.testblockcanary I/Printer: 方法运行的总时长:2002

第一部分功能完成了,我们可以开始第二部分了:
定位代码
em…
这块代码我是从 BlockCanary 源码搬过来进行调整后,由于就是一些 SDK 方法的调度,能讲解的不多:
private fun initPrinter() { Looper.getMainLooper().setMessageLogging { if (printerStart) { printerStart = false printerStartTime = System.currentTimeMillis(); } else { printerStart = true (System.currentTimeMillis() - printerStartTime).let { if (it >= minTime){ Log.i("Printer", "方法运行的总时长:${it}") //--------------------------------------------// //获取栈信息进行输出 val stringBuilder = StringBuilder() for (stackTraceElement in Looper.getMainLooper().getThread().getStackTrace()) { stringBuilder .append(stackTraceElement.toString()) .append(BlockInfo.SEPARATOR) } Log.i("Printer", "StackTrace:${stringBuilder.toString()}") //--------------------------------------------// } } } } }
运行,点击后的效果:
com.example.testblockcanary I/Printer: 方法运行的总时长:2006com.example.testblockcanary I/Printer: StackTrace:java.lang.System.currentTimeMillis(Native Method) com.example.testblockcanary.MainActivity$initPrinter$1.println(MainActivity.kt:31) android.os.Looper.loop(Looper.java:145) android.app.ActivityThread.main(ActivityThread.java:6077) java.lang.reflect.Method.invoke(Native Method) com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
由上可以看出,确实是输出了当前的线程的栈信息。

不过,仔细看下,不太对劲啊,为什么没有定位到 clickView() 方法,不是它阻塞的吗?

em…
想想也正常,因为第二次调用 println() 方法的时候,clickView() 已经执行完出栈了,自然不会输出 clickView() 的信息,所以,我们必须在 clickView() 执行的时候就要输出栈信息,也就是在 minTime 时间前,我这里选取的时间为 minTime * 0.8。怕你们忘了 minTime 是什么,提示下:
//方法执行时间超过该值才输出private val minTime = 1000L
所以,我们在 printerStart 为 true 的时候,就使用 handler 发送一个 minTime * 0.8 的延迟消息,用于记录此时栈信息,假如 printerStart 为 false 的时候,执行总时间短于 minTime,我们就不输出,否则就输出栈信息。
-
初始化Handler
private lateinit var delayHandler: Handlerprivate fun initDelayHandler() { //让Handler的消息在子线程中运行 val handlerThread = HandlerThread("DelayThread") handlerThread.start() delayHandler = Handler(handlerThread.looper)}
-
将获取栈消息功能单独抽取出来
private val stringBuilder = StringBuilder() private val runnable = Runnable { //获取栈信息进行记录 for (stackTraceElement in Looper.getMainLooper().getThread().getStackTrace()) { stringBuilder .append(stackTraceElement.toString()) .append(BlockInfo.SEPARATOR) } }
-
runnable 的发送和销毁
private fun initPrinter() { Looper.getMainLooper().setMessageLogging { if (printerStart) { printerStart = false printerStartTime = System.currentTimeMillis(); delayHandler.removeCallbacks(runnable) //延迟minTime * 0.8发送,用于记录阻塞时的栈信息 delayHandler.postDelayed(runnable, (minTime * 0.8).toLong()) } else { printerStart = true delayHandler.removeCallbacks(runnable) (System.currentTimeMillis() - printerStartTime).let { if (it >= minTime) { Log.i("Printer", "方法运行的总时长:${it}") Log.i("Printer", "StackTrace:${stringBuilder.toString()}") } } } } }
-
运行,点击效果:
com.example.testblockcanary I/Printer: 方法运行的总时长:2011com.example.testblockcanary I/Printer: StackTrace:java.lang.Thread.sleep(Native Method) java.lang.Thread.sleep(Thread.java:371) java.lang.Thread.sleep(Thread.java:313) android.os.SystemClock.sleep(SystemClock.java:120) com.example.testblockcanary.MainActivity.clickView(MainActivity.kt:67) java.lang.reflect.Method.invoke(Native Method) androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:409) android.view.View.performClick(View.java:5610) android.view.View$PerformClick.run(View.java:22265) android.os.Handler.handleCallback(Handler.java:751) android.os.Handler.dispatchMessage(Handler.java:95) android.os.Looper.loop(Looper.java:154) android.app.ActivityThread.main(ActivityThread.java:6077) java.lang.reflect.Method.invoke(Native Method) com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)

终于输出正确的栈信息了!!!
另外,这里再提个额外的信息:
在新版的 LeakCanary 中,只要进行依赖,不需要在 Application 中进行初始化就可以直接使用了!
既然我们也是要写个性能检测工具,那我们也可以参考该做法进行实现。
其实,就是在 ContentProvider 的 onCreate() 方法中进行初始化,因为 App 在启动的时候,是优先初始化 ContentProvider 再初始化 Application 的。具体代码我就不贴了,不难,只是给还未了解到这块的同学多传输些信息。
这是我的公众号,欢迎关注支持下,谢谢!

发表评论
最新留言
关于作者
