Performance optimization is something that almost every Android development needs to consider. LeakCanary and Hugo are commonly used performance detection tools. This time, I want to talk about BlockCanary. Since this library has not been updated for a long time, many people may not know it, but this does not prevent us from understanding its implementation principle.

OK, start blowing water body

Function is introduced

First, give the GitHub address

Github.com/markzhai/An…

The main function of this library is to detect the operation duration in the main thread. If the operation duration exceeds a certain amount of time (default: 1000 ms), it will record the block information and inform the developer.

The integration steps

  • And rely on

    Dependencies {/ / development version or release testing (not recommended) / / implementation 'com. Making. Markzhai: blockcanary - android: 1.5.0' / / only test in the development of version DebugImplementation 'com. Making. Markzhai: blockcanary - android: 1.5.0' releaseImplementation 'com. Making. Markzhai: blockcanary - no - op: 1.5.0'}Copy the code
  • Init in the custom Application onCreate() method

    class MyApplication : Application() {
    
        override fun onCreate() {
            super.onCreate()
            BlockCanary.install(this, BlockCanaryContext()).start()
        }
    }
    Copy the code
  • Don’t forget the Application binding in AndroidMainfest.xml

           android:name=".MyApplication"
    Copy the code

Specific detection effect

  • Let’s take a look at the layout. It’s just a button

  • The button is listened to and clicked and sleeps for 2,000 milliseconds

        fun clickView(view: View) {
    	    SystemClock.sleep(2000)
        }
    Copy the code
  • After clicking, BlockCanary collects the information

Isn’t it amazing to be able to pinpoint where the blockage is and when.

Used to summarize

  • The ability to calculate how long a method is running in the main thread
  • Locate the running method on the code

So once these two problems are solved, we can write our own simple BlockCanary.

The specific implementation

The running time

em… Running time in the main thread… The main thread… The main thread…

Oh!

Since the ActivityThread is kept in an infinite loop by MainLooper, the operation on the main thread is basically performed by Posting messages to MessageQueue, which is retrieved and executed by MainLooper. Therefore, we can record the time A before execution. Time B is also recorded after execution, and b-a gives the running time!

Looper loop()

public static void loop() { final Looper me = myLooper(); ... the for (;;) {··· Final Printer Logging = me.mlogging; if (logging ! = null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); }, MSG. Target. DispatchMessage (MSG); ... the if (logging! = null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); }...}}Copy the code

Isn’t that what we want, to print before the method is executed, and then print after the method is executed.

Let’s see how me. MLogging is assigned.

Me calls myLooper() to get it

    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
Copy the code

So mLogging is actually the current mLogging of Looper

	private Printer mLogging;

    public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }
Copy the code

Looper will automatically call our println() method when Printer’s println() method is rewritten

Complete code:

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {super.oncreate (savedInstanceState) setContentView(r.layout.activity_main) initPrinter()} // Use printerStart Println private var printerStart = true // Records the time before the method is executed. 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) } }Copy the code

Run the App and click the button:

Com. Example. Testblockcanary I/Printer: method of operation of total length: 1 com. Example. Testblockcanary I/Printer: Methods running total length: 3 com. Example. Testblockcanary I/Printer: method running total length: 3 com. Example. Testblockcanary I/Printer: Methods running total duration: 4 com. Example. Testblockcanary I/Printer: method running total length: 3 com. Example. Testblockcanary I/Printer: method running total length: 2009Copy the code

As you can see, our println() method is called whenever the main thread executes a method, but we only need to check for longer methods, so we add an interception:

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}")}}}}}Copy the code

After running, click:

Com. Example. Testblockcanary I/Printer: method of operation of total length: 2002Copy the code

With the first part of the function complete, we can start the second part:

Location code

em…

I moved this code from BlockCanary source code for adjustment, because it is some SDK method scheduling, can explain not much:

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", "Total running time of method: ${it} ") / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - / / / / stack information output val stringBuilder = stringBuilder () (stackTraceElement in Looper.getMainLooper().getThread().getStackTrace()) { stringBuilder Append (stackTraceElement. ToString ()). Append (BlockInfo. The SEPARATOR)} Log. I (" Printer ", "StackTrace: ${stringBuilder.toString()}") //--------------------------------------------// } } } } }Copy the code

Run, click the effect:

Com. Example. Testblockcanary I/Printer: method of operation of total length: 2006 com. 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)Copy the code

As can be seen from the above, it does output the current thread stack information.

The clickView() method is blocked. The clickView() method is blocked.

em…

When you call println() for the second time, the clickView() method is already out of the stack, so you have to output the stack when clickView() is executed. That is, before minTime, I select minTime * 0.8. In case you forget what minTime is, here’s a reminder:

Private val minTime = 1000LCopy the code

When printerStart is true, we use handler to send a delay message minTime * 0.8 to record stack information. If printerStart is false, we use handler to send a delay message minTime * 0.8 to record stack information. If the total execution time is less than minTime, we will not output, otherwise we will output stack information.

  • Initialize Handler

    private lateinit var delayHandler: Handler private fun initDelayHandler() {val handlerThread = handlerThread ("DelayThread") handlerThread.start() delayHandler = Handler(handlerThread.looper) }Copy the code
  • Extract the stack message function separately

    Private val stringBuilder = stringBuilder () private val runnable = runnable {// For (stackTraceElement in Looper.getMainLooper().getThread().getStackTrace()) { stringBuilder .append(stackTraceElement.toString()) .append(BlockInfo.SEPARATOR) } }Copy the code
  • Send and destroy 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()}")
                        }
                    }
                }
            }
        }
    Copy the code
  • Run, click effect:

    Com. Example. Testblockcanary I/Printer: method of operation of total length: 2011 com. 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)Copy the code

Finally output the correct stack information!!


And here’s an additional piece of information:

In the new version of LeakCanary, you can simply rely on it and use it without having to initialize it in the Application!

Since we are also going to write a performance testing tool, we can also follow this approach to implement.

This is done in the onCreate() method of the ContentProvider, because the App initializes the ContentProvider first and then the Application. I will not paste the specific code, it is not difficult, but for the students who have not yet understood this piece of more transmission of information.