Analysis of blockcanary source code

Time:2022-5-6

Analysis of blockcanary source code

Before explaining the source code of blockcanal, we still need to add some pre knowledge points. This article doesn’t talk about the principle of handler. Students who don’t understand it go to Baidu to have a look.

What is carton

Before we talk about the Caton problem, we need to talk about itFrame rateThis concept. The frame rate is the frequency at which bitmap images in frames appear continuously on the display. I will give an example, movie play. A movie is actually a collection of many photos (frames), so why does it look like a continuous process? Because there is more than one picture in the film every second. In fact, the number of pictures in a movie is usually 20-30 per second. If 24 pictures appear in a movie every second, the frame rate of the movie is 24. The frame rate is how many frames appear in a second.

I know what frame rate is, so the problem is, why is there a jam? Caton’s performance in our vision is that the original animation picture is smooth, but now it is not smooth. As we mentioned above, animation is actually composed of many pictures. If in a 24 frame movie, there is a second suddenly, and the frame is dropped in this second. That is, the original 0 The picture of 23 becomes 0 10… 12… 23. If a frame in the middle is not rendered, it will not be smooth visually. That’s the Caton phenomenon. The above is the phenomenon of Caton in the film. What about our Android system?

Android rendering mechanism

Before the advent of high brush mobile phones, the frame rate of our mobile phone screen was 60. It means that 60 images will appear in one second. Then there must be a picture rendering in 16ms** The Android system sends a Vsync signal every 16ms to trigger the rendering of the UI. If each rendering is successful, 60 frames required for a smooth picture can be achieved. In order to achieve 60fps, this means that most operations of the program must be completed within 16ms. If it exceeds 16ms, the frame may be lost** If the frequency of frame dropping is very high, that is, it leads to jamming.

Analysis of blockcanary source code

So in Android,BlockCanaryHow to help us do the carton test. Today we’ll talk about itBlockCanaryCheck the principle of Caton.

Generally, we start our Caton detection through the following code methods.

public class DemoApplication extends Application {
    @Override
    public void onCreate() {
        // ...
        // Do it on main process
        BlockCanary.install(this, new AppBlockCanaryContext()).start();
    }
}

This code mainly has two parts: one is install and the other is start. Let’s look at the install section first

Install phase
BlockCanary#install()
public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
    //BlockCanaryContext. Init will save the ApplicationContext of the application and the configuration parameters set by the user
        BlockCanaryContext.init(context, blockCanaryContext);
    //Etenabled will be enabled according to the user's notification bar message configuration
        setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
        return get();
    }
BlockCanary#get()
//A blockcanary object is created using a singleton    
public static BlockCanary get() {
    if (sInstance == null) {
        synchronized (BlockCanary.class) {
            if (sInstance == null) {
                sInstance = new BlockCanary();
            }
        }
    }
    return sInstance;
}
BlockCanary()
private BlockCanary() {
      //Initialize the blockcanaryinternal scheduling class
      BlockCanaryInternals.setContext(BlockCanaryContext.get());
      mBlockCanaryCore = BlockCanaryInternals.getInstance();
      //Add an interceptor (chain of responsibility) for blockcanaryinternals. Blockcanarycontext is an empty implementation of blockinterceptor
      mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
      if (!BlockCanaryContext.get().displayNotification()) {
          return;
      }
      //Displayservice is only added when the notification bar message is opened. When a jam occurs, the notification bar message will be initiated through displayservice
      mBlockCanaryCore.addBlockInterceptor(new DisplayService());

  }
BlockCanaryInternals.getInstance()
static BlockCanaryInternals getInstance() {
    if (sInstance == null) {
        synchronized (BlockCanaryInternals.class) {
            if (sInstance == null) {
                sInstance = new BlockCanaryInternals();
            }
        }
    }
    return sInstance;
}
BlockCanaryInternals
public BlockCanaryInternals() {
        //Initialize stack collector
        stackSampler = new StackSampler(
                Looper.getMainLooper().getThread(),
                sContext.provideDumpInterval());
        //Initialize CPU collector
        cpuSampler = new CpuSampler(sContext.provideDumpInterval());

        //Initialize loopermonitor and implement the callback of onblockevent, which will be called after triggering the threshold, which is more important
        setMonitor(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();
    }

After the initialization of install is completed, the start () method will be called. The implementation is as follows:

Start phase
BlockCanary#start()
//BlockCanary#start()
public void start() {
    if (!mMonitorStarted) {
        mMonitorStarted = true;
        //Set the monitor in the mainloopmonitor
        Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
    }
}

The implementation is also relatively simple, that is, get the looper of the main thread, and then set the loopermonitor created in the previous step to messagelogging in the looper of the main thread.

Here, and then? Lying trough, no, I was also very confused when I first looked at the source code here. Then I went to GitHub to see it. Then, I saw such a picture.

Analysis of blockcanary source code

From this figure, I can see that the real start of detection is not start (), but the loop () function in looper

Looper#loop
public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    if (me.mInLoop) {
        Slog.w(TAG, "Loop again would have the queued messages be executed"
               + " before this one completed.");
    }

    me.mInLoop = true;
    final MessageQueue queue = me.mQueue;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    // Allow overriding a threshold with a system prop. e.g.
    // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
    final int thresholdOverride =
        SystemProperties.getInt("log.looper."
                                + Process.myUid() + "."
                                + Thread.currentThread().getName()
                                + ".slow", 0);

    boolean slowDeliveryDetected = false;

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                            msg.callback + ": " + msg.what);
        }
        // Make sure the observer won't change while processing a transaction.
        final Observer observer = sObserver;

        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;
        Object token = null;
        if (observer != null) {
            token = observer.messageDispatchStarting();
        }
        long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
        try {
            msg.target.dispatchMessage(msg);
            if (observer != null) {
                observer.messageDispatched(token, msg);
            }
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } catch (Exception exception) {
            if (observer != null) {
                observer.dispatchingThrewException(token, msg, exception);
            }
            throw exception;
        } finally {
            ThreadLocalWorkSource.restore(origWorkSource);
            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);
        }

        if (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();
    }
}

The code in loop () is very long. We don’t need to pay too much attention to other parts when explaining blockcanary. Remember what we did in start? We set itsetMessageLogging。 So take a look firstsetMessageLoggingmethod

Looper#setMessageLogging
public void setMessageLogging(@Nullable Printer printer) {
    mLogging = printer;
}

In fact, it will be createdLooperMonitorAssigned to mlogging, then we only need to focus on the code of mlogging in loop(). We found that println was called twice. One is inmsg.target.dispatchMessage(msg)Before, one was inmsg.target.dispatchMessage(msg)After that. That is to say, these two calls, one is before processing the signal and the other is after processing the signal. Then, by implementing the println method in loopermonitor, we can get some time difference. So, next we’ll look at the println method in loopermonitor

MainLooper#println()
//MainLooper#println()
@Override
public void println(String x) {
    //If the debug mode is used again, monitoring will not be performed
    if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
        return;
    }
    If (! Mprintingstarted) {// println executed before dispatchmesage
        //Record start time
        mStartTimestamp = System.currentTimeMillis();
        mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
        mPrintingStarted = true;
        //Start collecting stack and CPU Information
        startDump();
    }Else {// println executed after dispatchmesage
        //Get end time
        final long endTime = System.currentTimeMillis();
        mPrintingStarted = false;
        //Determine whether the time consumption exceeds the threshold
        if (isBlock(endTime)) {
            notifyBlockEvent(endTime);
        }
        stopDump();
    }
}

//Determine whether the threshold is exceeded
 private boolean isBlock(long endTime) {
     return endTime - mStartTimestamp > mBlockThresholdMillis;// This threshold is set by ourselves
 }

//If the threshold value is exceeded, call back the monitoring of Caton, indicating that Caton is blocked
private void notifyBlockEvent(final long endTime) {
    final long startTime = mStartTimestamp;
    final long startThreadTime = mStartThreadTimestamp;
    final long endThreadTime = SystemClock.currentThreadTimeMillis();
    HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
        @Override
        public void run() {
            mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
        }
    });
}

In fact, the source code of Caton detection here is still relatively simple. Its principle is to re implement the logging in looper, and then judge whether there is Caton through the println function. The flowchart of blockcanary also appears above. So this blog is written here. I hope it will be helpful to everyone and Caton’s understanding.