What is blockcanary?

Blockcanary, developed by Domestic developer MarkZhai, is a performance monitoring component that monitors mainthread operations transparently and outputs useful information to help developers analyze, locate problems, and quickly optimize their applications

The following is an example of the official principle:

Introduction to the

Making address: blockcanary

The characteristics of

  • noninvasive
  • Using a simple
  • Real-time monitoring
  • Provides complete stack and memory information

Android Rendering Mechanism

Android sends a VSYNC signal every 16ms, triggering the RENDERING of the UI. If each rendering is successful, then you can achieve the 60fps required for smooth graphics. In order to achieve 60fps, this means that most of the application’s operations must be completed within 16ms. If it goes beyond 16ms then frame loss may occur.

This article mainly analyzes the principle of Blockcanary. For detailed mechanism and optimization of rendering, please refer to the following article:

Android Performance optimization – Render optimization

How does Blockcanary work?

1, Gradle import library

 debugImplementation 'com. Making. Markzhai: blockcanary - android: 1.5.0'
 releaseImplementation 'com. Making. Markzhai: blockcanary - no - op: 1.5.0'
 
Copy the code

2. Customize the Application and initialize it in onCreate

public class ExampleApplication extends Application {

    @Override public void onCreate() { super.onCreate(); BlockCanary.install(this, new BlockCanaryContext()).start(); }}Copy the code

What is the core execution process for Blockcanary?

The core principle of Blockcanary is to set a Printer to the MainLooper of the main ActivityThread by customizing it. MainLooper will call Printer to print before and after dispatch message. The system obtains the time difference before and after the execution and determines whether the time difference exceeds the threshold. If the number is exceeded, the stack information and CPU information recorded will be notified to the foreground.

Key class functions

class instructions
BlockCanary Appearance class that provides initialization and starts and stops listening
BlockCanaryContext Context: You can configure the ID, current network information, choke threshold, and log saving path
BlockCanaryInternals Blockcanary’s core scheduling class, This includes the return of monitor (set to the printer of the MainLooper), stackSampler (stack message handler), cpuSampler (CPU message handler), mInterceptorChain (registered interceptor), and onBlockEvent Dispatches and interceptors
LooperMonitor The Printer interface is inherited for setting to MainLooper. The execution time difference before and after the Dispatch of MainLooper is obtained by copying println, and the information collection of stackSampler and cpuSampler is controlled.
StackSampler It is used to get the stack information of the thread and store the collected stack information in a LinkHashMap with a timestamp of key. Through mCurrentThread. GetStackTrace () for the current thread StackTraceElement
CpuSampler It is used to obtain THE CPU information and store the collected CPU information in a LinkHashMap with the time stamp of key. Obtain the CPU information by reading the /proc/stat file
DisplayService Inheriting the BlockInterceptor interceptor, the onBlock callback triggers the sending of foreground notifications
DisplayActivity Use to display the recorded exception information Activity

Code execution flow

Leakcanary’s core process consists of three main steps.

1, init- Initialization

2. Monitor-listens to the dispatch time difference of MainLooper and pushes notifications to the foreground

3. Dump – Collect thread stack information and CPU information

Here first on the overall flow chart, it is recommended to view the source code.

Below we will analyze the source code related to the above three steps.

1, the init

Based on usage in Application, let’s first look at the install method

public static BlockCanary install(Context context, BlockCanaryContext BlockCanaryContext) {/ / BlockCanaryContext applicationContext init will save applications and users to set the configuration parameters BlockCanaryContext.init(context, blockCanaryContext); //etEnabled will be enabled based on the user's notification bar message configurationsetEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
        return get();
    }
    
Copy the code

Next look at the implementation of the get method:

// Create a BlockCanary object using the singleton public static BlockCanaryget() {
        if (sInstance == null) {
            synchronized (BlockCanary.class) {
                if(sInstance == null) { sInstance = new BlockCanary(); }}}return sInstance;
    }
Copy the code

Next we look at the BlockCanary object constructor implementation as follows:

private BlockCanary() {/ / initialization lockCanaryInternals scheduling BlockCanaryInternals setContext (BlockCanaryContext. The get ()); mBlockCanaryCore = BlockCanaryInternals.getInstance(); // Add interceptors (chain of responsibility) to BlockCanaryInternals. BlockCanaryContext is null for BlockInterceptor mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());if(! BlockCanaryContext.get().displayNotification()) {return; } //DisplayService is added only when notification bar messages are enabled. When caton occur will launch a notification bar through the DisplayService message mBlockCanaryCore. AddBlockInterceptor (new DisplayService ()); }Copy the code

Next we look at the BlockCanaryInternals constructor, which is implemented as follows:

public BlockCanaryInternals() {/ / initialization stack collector stackSampler = new stackSampler (stars) getMainLooper () getThread (), sContext. ProvideDumpInterval ()); / / initialize the CPU collector cpuSampler = new cpuSampler (sContext. ProvideDumpInterval ()); // Initialize LooperMonitor and implement the onBlockEvent callback, which is called after the threshold is firedsetMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {

            @Override
            public void onBlockEvent(long realTimeStart, long realTimeEnd,
                                     long threadTimeStart, long threadTimeEnd) {
                ArrayList<String> threadStackEntries = stackSampler
                        .getThreadStackEntries(realTimeStart, realTimeEnd);
                if(! threadStackEntries.isEmpty()) { BlockInfo blockInfo = BlockInfo.newInstance() .setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd) .setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd)) .setRecentCpuRate(cpuSampler.getCpuRateInfo()) .setThreadStackEntries(threadStackEntries) .flushString(); LogWriter.save(blockInfo.toString());if(mInterceptorChain.size() ! = 0) {for (BlockInterceptor interceptor : mInterceptorChain) {
                            interceptor.onBlock(getContext().provideContext(), blockInfo);
                        }
                    }
                }
            }
        }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));

        LogWriter.cleanObsolete();
    }
Copy the code

2, monitor

First, let’s look at the use of printer in the loop() method of Looper of the system, as follows:

   for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return; } // Execute Printer println method final Printer logging = me.if(logging ! = null) { logging.println(">>>>> Dispatching to " + msg.target + "" +
                        msg.callback + ":" + msg.what);
            }

            final long traceTag = me.mTraceTag;
            long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
            if (thresholdOverride > 0) {
                slowDispatchThresholdMs = thresholdOverride;
                slowDeliveryThresholdMs = thresholdOverride;
            }
            final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
            final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

            final boolean needStartTime = logSlowDelivery || logSlowDispatch;
            final boolean needEndTime = logSlowDispatch;

            if(traceTag ! = 0 && Trace.isTagEnabled(traceTag)) { Trace.traceBegin(traceTag, msg.target.getTraceName(msg)); } final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0; final long dispatchEnd; try { msg.target.dispatchMessage(msg); dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } finally {if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logSlowDelivery) {
                if (slowDeliveryDetected) {
                    if ((dispatchStart - msg.when) <= 10) {
                        Slog.w(TAG, "Drained");
                        slowDeliveryDetected = false; }}else {
                    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                            msg)) {
                        // Once we write a slow delivery log, suppress until the queue drains.
                        slowDeliveryDetected = true; }}}if (logSlowDispatch) {
                showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg); } // After executing dispatchMessage, execute println method for Printerif(logging ! = null) { logging.println("<<<<< Finished to " + msg.target + "" + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted. final long newIdent = Binder.clearCallingIdentity(); if (ident ! = newIdent) { Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass().getName() + " " + msg.callback + " what=" + msg.what); } msg.recycleUnchecked(); }Copy the code

When the install initialization is complete, the start() method is called as follows:

  public void start() {
        if(! mMonitorStarted) { mMonitorStarted =true; Looper.getmainlooper ().setMessagelogging (mBlockCanaryCore.monitor); }}Copy the code

When MainLooper executes dispatch, it will call printer’s println method, so here we see LooperMonitor’s implementation of println method as follows:

@override public void println(String x)if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
            return;
        }
        if(! MStartTimestamp = system.currentTimemillis (); mStartTimestamp = system.currentTimemillis (); mStartThreadTimestamp = SystemClock.currentThreadTimeMillis(); mPrintingStarted =true; // Start collecting stack and CPU information startDump(); }else{// final long endTime = system.currentTimemillis (); mPrintingStarted =false; // Check whether the time consumption exceeds the thresholdif(isBlock(endTime)) { notifyBlockEvent(endTime); } stopDump(); }} private Boolean isBlock(long endTime) {returnendTime - mStartTimestamp > mBlockThresholdMillis; } private void notifyBlockEvent(final Long endTime) {final Long startTime = mStartTimestamp; final long startThreadTime = mStartThreadTimestamp; final long endThreadTime = SystemClock.currentThreadTimeMillis(); HandlerThreadFactory.getWriteLogThreadHandler().post(newRunnable() {
            @Override
            public void run() { mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime); }}); }Copy the code

When the time difference exceeds the threshold, onBlockEvent is called back. This is implemented in the BlockCanaryInternals constructor as follows:

 setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() { @Override public void onBlockEvent(long realTimeStart, long realTimeEnd, long threadTimeStart, Long threadTimeEnd) {// Based on the start and end times, ArrayList<String> threadStackEntries = StackSampler.getThreadStackEntries (realTimeStart, realTimeEnd);if(! ThreadStackEntries. IsEmpty ()) {/ / build BlockInfo object, Set information BlockInfo BlockInfo = blockinfo.newinstance ().setMainThreadTimecost (realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd) .setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd)) .setRecentCpuRate(cpuSampler.getCpuRateInfo()) .setThreadStackEntries(threadStackEntries) .flushString(); Logwriter.save (blockinfo.toString ()); // Iterate over interceptors, notificationsif(mInterceptorChain.size() ! = 0) {for (BlockInterceptor interceptor : mInterceptorChain) {
                            interceptor.onBlock(getContext().provideContext(), blockInfo);
                        }
                    }
                }
            }
        }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));
Copy the code

Finally, let’s look at the DisplayService implementation of the interceptor, which sends a foreground notification as follows:

  @Override
    public void onBlock(Context context, BlockInfo blockInfo) {
        Intent intent = new Intent(context, DisplayActivity.class);
        intent.putExtra("show_latest", blockInfo.timeStart);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, FLAG_UPDATE_CURRENT);
        String contentTitle = context.getString(R.string.block_canary_class_has_blocked, blockInfo.timeStart);
        String contentText = context.getString(R.string.block_canary_notification_message);
        show(context, contentTitle, contentText, pendingIntent);
    }
Copy the code

3, dump

From the above process, we can know that when the println before dispatchMessage is triggered, the start method of dump will be executed, and when the println after dispatchMessage is triggered, the stop method of dump will be executed.

 private void startDump() {
        if(null ! = BlockCanaryInternals.getInstance().stackSampler) { BlockCanaryInternals.getInstance().stackSampler.start(); }if(null ! = BlockCanaryInternals.getInstance().cpuSampler) { BlockCanaryInternals.getInstance().cpuSampler.start(); } } private voidstopDump() {
        if(null ! = BlockCanaryInternals.getInstance().stackSampler) { BlockCanaryInternals.getInstance().stackSampler.stop(); }if (null != BlockCanaryInternals.getInstance().cpuSampler) {
            BlockCanaryInternals.getInstance().cpuSampler.stop();
        }
    }
Copy the code

Next we will introduce the Stacksampler and CpuSampler.

1, Stacksampler

The execution process of start() is as follows:

 public void start() {
        if (mShouldSample.get()) {
            return;
        }
        mShouldSample.set(true); HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable); / / by a HandlerThread delay execution mRunnable HandlerThreadFactory. GetTimerThreadHandler () postDelayed (mRunnable, BlockCanaryInternals.getInstance().getSampleDelay()); } private Runnable mRunnable = new; private Runnable mRunnable = newRunnable() {
        @Override
        public void run() {// Abstract methoddoSample(); // Continue the collectionif(mShouldSample.get()) { HandlerThreadFactory.getTimerThreadHandler() .postDelayed(mRunnable, mSampleInterval); }}}; / / StacksamplerdoSample() implements @override protected voiddoSample() { StringBuilder stringBuilder = new StringBuilder(); / / by mCurrentThread getStackTrace () to obtain StackTraceElement, join the StringBuilderfor(StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) { stringBuilder .append(stackTraceElement.toString()) .append(BlockInfo.SEPARATOR); } synchronized (sStackMap) {// synchronized Lru algorithm, which controls the length of LinkHashMapif(sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) { sStackMap.remove(sStackMap.keySet().iterator().next()); } // Add to the map sStackMap.put(system.currentTimemillis (), stringBuild.toString ()); }}Copy the code

The execution process of stop() is as follows:

 public void stop() {
        if(! mShouldSample.get()) {return; } // set the control variable mshouldsample.set ()false); / / cancel the handler news HandlerThreadFactory getTimerThreadHandler () removeCallbacks (mRunnable); }Copy the code

2, CpuSampler

The rest of the execution process is the same as StackSampler. Here we analyze doSample implementation, as follows:

// Mainly by obtaining /proc/statFile to get CPU information protected voiddoSample() {
        BufferedReader cpuReader = null;
        BufferedReader pidReader = null;

        try {
            cpuReader = new BufferedReader(new InputStreamReader(
                    new FileInputStream("/proc/stat")), BUFFER_SIZE);
            String cpuRate = cpuReader.readLine();
            if (cpuRate == null) {
                cpuRate = "";
            }

            if (mPid == 0) {
                mPid = android.os.Process.myPid();
            }
            pidReader = new BufferedReader(new InputStreamReader(
                    new FileInputStream("/proc/" + mPid + "/stat")), BUFFER_SIZE);
            String pidCpuRate = pidReader.readLine();
            if (pidCpuRate == null) {
                pidCpuRate = "";
            }

            parse(cpuRate, pidCpuRate);
        } catch (Throwable throwable) {
            Log.e(TAG, "doSample: ", throwable);
        } finally {
            try {
                if(cpuReader ! = null) { cpuReader.close(); }if(pidReader ! = null) { pidReader.close(); } } catch (IOException exception) { Log.e(TAG,"doSample: ", exception); }}}Copy the code

How does Blockcanary determine the delay?

The core principle of Blockcanary is to set a Printer to the MainLooper of the main ActivityThread by customizing it. MainLooper will call Printer to print before and after dispatch message. The system obtains the time difference before and after the execution and determines whether the time difference exceeds the threshold. If it exceeds, it is judged to be stuck.

How does LeakCanary get the thread stack information?

Through mCurrentThread. GetStackTrace () method, get StackTraceElement traversal, into a StringBuilder value, and stored in the a key for the timestamp LinkHashMap.

How does LeakCanary obtain information about the CPU?

CPU usage is calculated by reading the /proc/stat file to obtain information about all CPU activity. After parsing the information, it is converted to a StringBuilder value and stored in a LinkHashMap with the key as the timestamp.

conclusion

thinking

Blockcanary makes full use of Loop mechanism, executes println of printer for output before and after executing dispatchMessage inLoop method of MainLooper, and provides method to set printer. By analyzing the time difference before and after printing and the threshold value, so as to determine whether the lag.

The resources

Android Performance optimization – Render optimization

Android UI block monitoring framework BlockCanary principle analysis

recommended

Android source series – Decrypt OkHttp

Android source series – Decrypt Retrofit

Android source series – Decrypt Glide

Android source series – Decrypt EventBus

Android source series – Decrypt RxJava

Android source series – Decrypt LeakCanary

Decrypt BlockCanary

about

Welcome to follow my personal public account

Wechat search: one code one floating life, or search the public ID: LIFE2Code

  • Author: Huang Junbin
  • Blog: junbin. Tech
  • GitHub: junbin1011
  • Zhihu: @ JunBin