来,带你手写个性能检测工具
发布日期:2021-05-14 09:35:53 浏览次数:25 分类:精选文章

本文共 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 的。具体代码我就不贴了,不难,只是给还未了解到这块的同学多传输些信息。


这是我的公众号,欢迎关注支持下,谢谢!

上一篇:你真的了解GC吗?
下一篇:Android App的构建与运行

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2025年04月20日 02时07分26秒