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