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.