Matrix is a complete set of APM solution of wechat open source, which contains Resource Canary(Resource monitoring), Trace Canary(lag monitoring), IO Canary(IO monitoring) and so on.

This is the second article in the Caton analysis series that examines the principles associated with Trace Canary, based on version 0.5.2.43. The article is a bit long, suggest you browse roughly first again scrutinize, have help to you certainly. BlockCanary Is a portal detection tool for Android.

Matrix Content Overview

It can be seen that Matrix, as an APM tool, is very comprehensive in performance testing. This series of articles will analyze them one by one.

To clarify the source code structure, let’s start with the initialization process, the project address Matrix.

Matrix initialization process

The matrix. Builder inner class configures Plugins.

// create Matrix builder.Builder builder = new Matrix.Builder(this); // Optional sense plugin state change, onReportIssue get/process Issue Builder.PatchListener (...) ; // Optional config plugin Builder.plugin (tracePlugin); builder.plugin(ioCanaryPlugin); // Initialize matrix.init (builder.build());Copy the code

The Plugin structure

Plugin currently configured

  • TracePlugin
  • ResourcePlugin
  • IOCanaryPlugin
  • SQLiteLintPlugin
  • ThreadWatcher
  • BatteryCanaryPlugin
  • MemoryCanaryPlugin

This article analyzes the TracePlugin, which is related to lag /UI rendering efficiency.

Matrix.Builder invokes the build method to trigger the Matrix constructor.

private Matrix(Application app, PluginListener listener, HashSet<Plugin> plugins) {
    this.application = app;
    this.pluginListener = listener;
    this.plugins = plugins;
    //(1)
    AppActiveMatrixDelegate.INSTANCE.init(application);
    for(Plugin plugin : plugins) { //(2) plugin.init(application, pluginListener); pluginListener.onInit(plugin); }}Copy the code
  1. AppActiveMatrixDelegate is an enumeration class (inexplicably, enumerations don’t perform very well, just like a normal singleton class), Register ActivityLifecycle and ComponentCallbacks2 listeners for the application in its init method, It is used to retrieve all Activity life cycle states and memory shortage states (onTrimMemory/onLowMemory) in the application for later use.

  2. All plug-ins are iterated internally and initialized by calling their init method, after which the pluginListener lifecycle method onInit is notified.

The PluginListener contains the following lifecycle:

# -> PluginListenerPublic interface PluginListener {// initialize void onInit(Plugin Plugin); Void onStart(Plugin Plugin); Void onStop(Plugin Plugin); Void onDestroy(Plugin Plugin); // Plugins capture issues, including caton, ANR, etc. Void onReportIssue(Issue Issue); }Copy the code

Generally speaking, the upper layer needs to customize a PluginListener, because the onReportIssue method is the key method for specific Issue processing, and the official sample method is to pop up an IssuesListActivity to display the Issue specific information when the Issue is received. The DefaultPluginListener defined by the Matrix framework does nothing. As an access party, we may do richer processing, such as serialization to local, uploading to the cloud, etc., all of which start with customizing PluginListener and implementing the onReportIssue method.

The patchListener method simply assigns values to member variables.

# -> Matrix.Builder
public Builder patchListener(PluginListener pluginListener) {
    this.pluginListener = pluginListener;
    return this;
}
Copy the code

Finally, the Matrix init method assigns a value to its static member variable, sInstance.

# -> Matrix
public static Matrix init(Matrix matrix) {
    if (matrix == null) {
        throw new RuntimeException("Matrix init, Matrix should not be null.");
    }
    synchronized (Matrix.class) {
        if (sInstance == null) {
            sInstance = matrix;
        } else {
            MatrixLog.e(TAG, "Matrix instance is already set. this invoking will be ignored"); }}return sInstance;
}
Copy the code

Matrix structure

You can see that Matrix provides the log manager MatrixLogImpl and various methods for manipulating all the plugins within it.

Moving on, let’s take a look at how the Caton (UI rendering performance) analysis module TracePlugin works.

TracePlugin

It is the Tracer manager, which internally defines four trackers.

  • AnrTracer ANR monitoring
  • EvilMethodTracer time-consuming function monitoring
  • FrameTracer Frame rate monitoring
  • StartupTracer Startup time

Take a look at the class diagram:

These tracers are derived from Tracer, which is an abstract class with no abstract methods and is implemented by default for inherited interfaces.

To see what these tracers can do, let’s look at the interface that Tracer inherits from and implements.

1. LooperObserver

It is an abstract class that defines three important methods in internal dispatchBegin/doFrame/dispatchEnd, but just empty implementation, the three methods to monitor the main thread of the Handler associated message processing. When the main thread processes a message, dispatchBegin is called, doFrame is called, and then dispatchEnd is called. The reason for this is that there are usually two ways to detect caton.

  1. Listen for message processing by the main thread Handler
  2. Listen for Choreographer’s Frame callbacks (doFrame)

The first method is implemented through a Logger object inside a Hook Looper. Before and after Looper distributes and processes messages, the logger will print logs. Hook is equivalent to getting the time before and after a message. According to the time difference between the two, a lot of stuck analysis can be done. See BlockCanary for details

The second approach is the Choreographer Open API, where the upper layer can set up FrameCallback listeners to get onFrame callbacks after each frame has been drawn. Popular FPS monitoring tools, such as TinyDancer and Takt, calculate FPS by analyzing the time difference between two frames.

In fact, earlier versions of Matrix used the second approach, and the latest version uses the first approach because it gives you a more complete and clear stack information.

At this point, we can infer that Tracer has cognitive frame rate changes, the statistical caton ability, so with the frame rate, function takes statistical correlation of Tracer (FrameTracer/EvilMethodTracer/AnrTracer) will continue to autotype doFrame method, in order to achieve specific functions.

2. ITracer

It is an interface that inherits IAppForeground interface. Generally, there are four abstract methods: onStartTrace, onCloseTrace, isAlive, onForeground. The first three methods describe the life cycle of Tracer and are managed by TracePlugin. The onForeground method of Tracer is called back when the foreground and background states of the Activity change, so Tracer has the ability to sense the foreground and background states of the Activity, which can be used for startup analysis.

Most of the interface methods in Tracer are empty implementations, and the implementation is left to the required Tracer. Let’s look at the specific Tracer implementation that TraceCanary contains.

FrameTracer

Let’s start with the FrameTracer, which overwrites the doFrame to listen for the callback of each frame and send information like timestamp, frame drop, page name, and so on to the IDoFrameListener.

# -> FrameTracer -> doFrame
@Override
public void doFrame(final long lastFrameNanos, final long frameNanos) {
    if(! isDrawing) {return;
    }
    isDrawing = false;
    final int droppedCount = (int) ((frameNanos - lastFrameNanos) / REFRESH_RATE_MS);
    for(final IDoFrameListener listener : MDoFrameListenerList) {// Send listener. DoFrameSync (lastFrameNanos, frameNanos, getScene(), droppedCount);if(null ! = listener.gethandler ()) {listener.gethandler ().post(new AsyncDoFrameTask(listener, lastFrameNanos, frameNanos, getScene(), droppedCount)); }}}Copy the code

You can see that callbacks are sent synchronously and asynchronously in the code, and that the upper layer registers the listener through the Register method on the FrameTracer.

# FrameTracer
public void register(IDoFrameListener listener) {
    if (FrameBeat.getInstance().isPause()) {
        FrameBeat.getInstance().resume();
    }
    if(! mDoFrameListenerList.contains(listener)) { mDoFrameListenerList.add(listener); } } public void unregister(IDoFrameListener listener) { mDoFrameListenerList.remove(listener);if (!FrameBeat.getInstance().isPause() && mDoFrameListenerList.isEmpty()) {
        FrameBeat.getInstance().removeListener(this);
    }
}
Copy the code

EvilMethodTracer

It has the function of checking the time function, and the ANR is the most serious time case, so let’s first look at how the ANR check is done.

ANR check

Let’s start with the constructor

public EvilMethodTracer(TracePlugin plugin, TraceConfig config) { super(plugin); this.mTraceConfig = config; / / create ANR delay detection tools regularly 5 s mLazyScheduler = new LazyScheduler (MatrixHandlerThread. GetDefaultHandlerThread (), Constants.DEFAULT_ANR); mActivityCreatedInfoMap = new HashMap<>(); }Copy the code

LazyScheduler is a delayed task utility class constructed with HandlerThread and Delay.

The internal ILazyTask interface defines the callback method onTimeExpire for delayed task execution. The setUp method starts burying the bomb (ANR and time consuming methods) and the cancel method disarms the bomb. This means that if cancel is not performed within 5 seconds of calling the setUp method, the onTimeExpire method will be triggered.

With that in mind, let’s look at the doFrame method.

# -> EvilMethodTracer
@Override
public void doFrame(long lastFrameNanos, long frameNanos) {
    if (isIgnoreFrame) {
        mActivityCreatedInfoMap.clear();
        setIgnoreFrame(false);
        getMethodBeat().resetIndex();
        return; } int index = getMethodBeat().getCurIndex(); // If the time difference between two frames is greater than the lag threshold (one second by default), the buffer information will be generated. // If a series of checks are met, the lag detection will be triggeredif (hasEntered && frameNanos - lastFrameNanos > mTraceConfig.getEvilThresholdNano()) {
        MatrixLog.e(TAG, "[doFrame] dropped frame too much! lastIndex:%s index:%s", 0, index); handleBuffer(Type.NORMAL, 0, index - 1, getMethodBeat().getBuffer(), (frameNanos - lastFrameNanos) / Constants.TIME_MILLIS_TO_NANO); } getMethodBeat().resetIndex(); mLazyScheduler.cancel(); SetUp (this,false);
}
Copy the code

If the next doFrame is not executed within 5 seconds, the onTimeExpire method of EvilMethodTracer is called back.

# -> EvilMethodTracer
@Override
public void onTimeExpire() {
    // maybe ANR
    if (isBackground()) {
        MatrixLog.w(TAG, "[onTimeExpire] pass this time, on Background!");
        return;
    }
    long happenedAnrTime = getMethodBeat().getCurrentDiffTime();
    MatrixLog.w(TAG, "[onTimeExpire] maybe ANR!");
    setIgnoreFrame(true);
    getMethodBeat().lockBuffer(false); HandleBuffer (type.anr, 0, getMethodBeat().getCurIndex() -1, getMethodBeat().getBuffer(), null, Constants.DEFAULT_ANR, happenedAnrTime, -1); }Copy the code

How is it detected for ordinary time consuming functions? EvilMethodTracer works like this:

  1. The first step is to log the execution time of each function, which involves pegging the entry and exit of each function. Finally, we write the member variable sBuffer in MethodBeat, which is an array of type long, with different bits describing the function ID and the duration of the function. The reason why a long value is used to record the time result is to compress data and save memory. The official data is that the buffer length of the recorded data is 100W and the memory usage is about 7.6m.
  2. DoFrame checks the time difference between two frames. If the time difference is greater than the lag threshold (1s by default), the handleBuffer is called to trigger a statistical task.
  3. Start AnalyseTask in handlerBuffer, analyze and filter method, call stack, and function time, and save them in jsonObject.
  4. Call sendReport to turn the jsonObject into an Issue object and send the event to the PluginListener.

Function in the pile

The TraceMethodAdapter inner class for MethodTracer is responsible for inserting MethodBeat’s I method before and O method after each method execution. Staking is implemented using ASM, a commonly used dynamic technique for manipulating bytecode, which can be used for non-invasive burying statistics. EvilMethodTracer also uses it to analyze time-consuming functions.

# -> MethodTracer.TraceMethodAdapter
@Override
protected void onMethodEnter() {
    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
    if(traceMethod ! = null) { traceMethodCount.incrementAndGet(); mv.visitLdcInsn(traceMethod.id); / / entry into piles mv. VisitMethodInsn (INVOKESTATIC, TraceBuildConstants MATRIX_TRACE_CLASS,"i"."(I)V".false);
    }
}

@Override
protected void onMethodExit(int opcode) {
    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
    if(traceMethod ! = null) {if (hasWindowFocusMethod && mTraceConfig.isActivityOrSubClass(className, mCollectedClassExtendMap)
                && mCollectedMethodMap.containsKey(traceMethod.getMethodName())) {
            TraceMethod windowFocusChangeMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
                    TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
            if(windowFocusChangeMethod.equals(traceMethod)) { traceWindowFocusChangeMethod(mv); } } traceMethodCount.incrementAndGet(); mv.visitLdcInsn(traceMethod.id); / / export pile mv. VisitMethodInsn (INVOKESTATIC, TraceBuildConstants MATRIX_TRACE_CLASS,"o"."(I)V".false); }}Copy the code

Matrix through a proxy tasks transformClassesWithDexTask during compilation, the global class files as input, using ASM tools, efficiently to scan all the class files and pile. To minimize performance loss, the scanning process filters out default or anonymous constructors as well as simple and time-consuming functions such as GET /set.

In order to record the function execution process conveniently and efficiently, the Matrix plug-in assigns an independent ID to each piling function. In the piling process, the function signature and assigned ID of piling are recorded, and a methodMap file is output after piling, which is used as analytical support after data reporting. This file is generated during APK construction. The directory is located under build/ Matrix_Output, named Debug_methodmap(Debug Build), and the filtered methods are logged in the Debug_ignoremethodmap file. The file generation rules are in the MethodCollector class for interested parties to explore.

So let’s look at the content of the generated file.

Each line of the file represents a piling method. Take the first action as an example:

- 1, 1, sample. Tencent. Matrix. IO. TestIOActivity onWindowFocusChanged V (Z)Copy the code
  • -1 The first number indicates the Id of the assignment method, and -1 indicates the onWindowFocusChanged method that the peg added to the activity. Other methods count from 1.
  • 1 represents the method permission modifier. The common value is ACC_PUBLIC = 1; ACC_PRIVATE = 2; ACC_PROTECTED = 4; ACC_STATIC = 8, etc. 1 means public method.
  • The name of the class sample. Tencent. Matrix. IO. TestIOActivity
  • The method name onWindowFocusChanged
  • Parameter and return value type Z indicates that the parameter is of Boolean type, and V indicates that the return value is null.

Next, to see what happens in practice, we simulate a time-consuming function that is called when a button is clicked.

// Click the button to trigger the zoom time, loop 200 times public voidtestJank(View view) {
    for(int i = 0; i < 200; i++) { wrapper(); }} // The wrapper method is used to test the call depth voidwrapper() { tryHeavyMethod(); } // Dump memory is a time-consuming method private voidtryHeavyMethod() {
    Debug.getMemoryInfo(new Debug.MemoryInfo());
}
Copy the code

Run to get the following Issue:

Our main concern is

  1. The cost bad function indicates the total time.
  2. Stack bad function call stack.
  3. StackKey bad entry method Id

What about stack(0,28,1,1988\n 1,31,1,136)? Each group is separated by a newline character. A group of four numbers is denoted as follows:

  • 0 method call depth. For example, when A calls B and B calls C, the call depth of A, B, and C is 0,1, and 2 respectively.
  • 28 methodId, corresponding to the first column in the methodMap file generated above.
  • 1 Number of calls
  • 1998 Total function time, including the call time of sub-functions.

We can verify the results by looking back at the methodMap function.

There is a bug in the stack. The final time-consuming method in our code is tryHeavyMethod, but with a wrapper in the middle, the stack can’t recognize it. Matrix will probably fix this later.

The stackKey is the entry point to the time-consuming function. In this case, testJank calls wrapper, which calls tryHeavyMethod. StackKey statistics are based on a function with a depth of 0, and 28 corresponds to testJank.

FPSTracer

FPS testing tool with other similar principle, listening to the Choreographer. FrameCallback callback, the callback method doFrame called Vsync signal is coming, every time the upper monitor this callback interface twice before the callback and calculate the time difference, The default refresh frequency of the Android system is 16.6ms. The time difference divided by the refresh frequency is the condition of frame loss.

The difference between FPSTracer and FPSTracer is that it can count the average frame rate over time and define a gradient between good and bad frame rates.

# -> FPSTracer.DropStatusprivate enum DropStatus { DROPPED_FROZEN(4), DROPPED_HIGH(3), DROPPED_MIDDLE(2), DROPPED_NORMAL(1), DROPPED_BEST(0); int index; DropStatus(int index) { this.index = index; }}Copy the code
  • DROPPED_FROZEN loses 42 or more frames (70%)
  • DEFAULT_DROPPED_HIGH Drop more than 24 frames and less than 42 frames (40% drop frames)
  • DEFAULT_DROPPED_MIDDLE Drops more than 9 frames but less than 24 frames (15% drops frames)
  • DEFAULT_DROPPED_NORMAL Drops more than 3 frames but less than 9 frames (5% drops frames)
  • DROPPED_BEST drops within 3 frames

Core method code snippet

# FPSTracer -> doReport
private void doReport() {
    LinkedList<Integer> reportList;
    synchronized (this.getClass()) {
        if (mFrameDataList.isEmpty()) {
            return; } reportList = mFrameDataList; mFrameDataList = new LinkedList<>(); } // Dump the data to the mPendingReportSet collectionfor (int trueId : reportList) {
        int scene = trueId >> 22;
        int durTime = trueId & 0x3FFFFF;
        LinkedList<Integer> list = mPendingReportSet.get(scene);
        if(null == list) { list = new LinkedList<>(); mPendingReportSet.put(scene, list); } list.add(durTime); } reportList.clear(); // Statistical analysisfor (int i = 0; i < mPendingReportSet.size(); i++) {
        int key = mPendingReportSet.keyAt(i);
        LinkedList<Integer> list = mPendingReportSet.get(key);
        if (null == list) {
            continue;
        }
        int sumTime = 0;
        int markIndex = 0;
        int count = 0;

        int[] dropLevel = new int[DropStatus.values().length]; // record the level of frames dropped each time
        int[] dropSum = new int[DropStatus.values().length]; // record the sum of frames dropped each time
        int refreshRate = (int) Constants.DEFAULT_DEVICE_REFRESH_RATE * OFFSET_TO_MS;
        for(Integer period : list) { sumTime += period; count++; int tmp = period / refreshRate - 1; // Write the drop frame to the arrayif (tmp >= Constants.DEFAULT_DROPPED_FROZEN) {
                dropLevel[DropStatus.DROPPED_FROZEN.index]++;
                dropSum[DropStatus.DROPPED_FROZEN.index] += tmp;
            } else if (tmp >= Constants.DEFAULT_DROPPED_HIGH) {
                dropLevel[DropStatus.DROPPED_HIGH.index]++;
                dropSum[DropStatus.DROPPED_HIGH.index] += tmp;
            } else if (tmp >= Constants.DEFAULT_DROPPED_MIDDLE) {
                dropLevel[DropStatus.DROPPED_MIDDLE.index]++;
                dropSum[DropStatus.DROPPED_MIDDLE.index] += tmp;
            } else if (tmp >= Constants.DEFAULT_DROPPED_NORMAL) {
                dropLevel[DropStatus.DROPPED_NORMAL.index]++;
                dropSum[DropStatus.DROPPED_NORMAL.index] += tmp;
            } else{ dropLevel[DropStatus.DROPPED_BEST.index]++; dropSum[DropStatus.DROPPED_BEST.index] += (tmp < 0 ? 0 : tmp); } // Reach the sharding time sendReport onceif (sumTime >= mTraceConfig.getTimeSliceMs() * OFFSET_TO_MS) { // if it reaches report time
                float fps = Math.min(60.f, 1000.f * OFFSET_TO_MS * (count - markIndex) / sumTime);
                MatrixLog.i(TAG, "scene:%s fps:%s sumTime:%s [%s:%s]", mSceneIdToSceneMap.get(key), fps, sumTime, count, markIndex); try { JSONObject dropLevelObject = new JSONObject(); . JSONObject dropSumObject = new JSONObject(); . JSONObject resultObject = new JSONObject(); resultObject = DeviceUtil.getDeviceInfo(resultObject, getPlugin().getApplication()); resultObject.put(SharePluginInfo.ISSUE_SCENE, mSceneIdToSceneMap.get(key)); resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject); resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject); resultObject.put(SharePluginInfo.ISSUE_FPS, fps); sendReport(resultObject); } catch (JSONException e) { MatrixLog.e(TAG,"json error", e);
                }


                dropLevel = new int[DropStatus.values().length];
                dropSum = new int[DropStatus.values().length];
                markIndex = count;
                sumTime = 0;
            }
        }

        // delete has reported data
        if (markIndex > 0) {
            for(int index = 0; index < markIndex; index++) { list.removeFirst(); }}... }}Copy the code

The whole process is as follows

  1. FPSTracer defines the member variable mFrameDataList of type LinkedList to record the time difference and the scene(activity or fragment name) information.
  2. Calculate the doFrame time difference twice, recorded in an int. The high 10 bits indicate sceneId, and the low 22 bits indicate the elapsed time ms*OFFSET_TO_MS(the default value is 100).
  3. Frame information is collected every two minutes (default getFPSReportInterval, official sample: 10 seconds). After the interval, the onTimeExpire callback method is triggered.
  4. OnTimeExpire calls doReport for statistical analysis.
  5. In the same scenario, if the total frame time exceeds the fragment time (getTimeSliceMs default is 6 seconds, official sample is 1 second), sendReport is triggered to send out the number of frames dropped and the frame drop time of each level.

There are a few details that need to be worked out, such as the fact that the page is not still and there are no UI drawing tasks, and the frame rate statistics for this period of time are meaningless. In fact, FPSTracer does a filter on the above mFrameDataList inserts, which store per-frame time information.

# FPSTracer -> doFrame
@Override
public void doFrame(long lastFrameNanos, long frameNanos)if(! isInvalid && isDrawing && isEnterAnimationComplete() && mTraceConfig.isTargetScene(getScene())) { handleDoFrame(lastFrameNanos, frameNanos, getScene()); } isDrawing =false; } private void handleDoFrame(long lastFrameNanos, long frameNanos, String scene) { int sceneId; . // Get scene information inttrueId = 0x0; // bit operation, write sceneId and time information to an inttrueId |= sceneId;
    trueId = trueId << 22;
    long offset = frameNanos - lastFrameNanos;
    trueId |= ((offset / FACTOR) & 0x3FFFFF);
    if (offset >= 5 * 1000000000L) {
        MatrixLog.w(TAG, "[handleDoFrame] WARNING drop frame! offset:%s scene%s", offset, scene); } // Add to mFrameDataList synchronized (this.getClass()) {mFrameDataList. Add (trueId); }}Copy the code

See conditions! isInvalid && isDrawing && isEnterAnimationComplete() && mTraceConfig.isTargetScene(getScene())

  1. IsInvalid indicates whether the activity is illegal. The value is false when the activity resumes and true when the activity is paused. That is, only the Resume phase is counted, because the activity is really drawn from onResume.
  2. FPSTracer adds a Draw Listener () to the DecorView when onActivityResumegetDecorView().getViewTreeObserver().addOnDrawListener()) listen for the view to draw, set this variable to true when calling onDraw, and onFrame end to false. Therefore, frame information will not be collected for a period of time at rest.
  3. IsEnterAnimationComplete Entry animation completes.
  4. IsTargetScene FPSTrace can be configured to monitor the interface whitelist, default all monitoring.

This is the end of the FPS detection process. Let’s take a look at the official sample summary report.

StartUpTrace Application startup statistics

The first thing to make clear is that it counts application startup, which includes the application creation process and not just activity startup. Statistics are destroyed once triggered, so if you want to count jumps between activities you need to manually get the StartUpTrace and call the onCreate method.

Specific statistical indicators are as follows:

Statistics project meaning
appCreateTime Application creation duration
betweenCost The application creation completes until the first Activity Create completes
activityCreate The activity executes super.oncreate() to the window to get focus
splashCost Splash Interface creation duration
allCost Total duration of window Focused on the main screen
isWarnStartUp Hot start (Application exists)

The timeline looks something like this:

In order to achieve the above statistics, hook ActivityThread message processing inner class H(member variable mH), which is a Handler object, activity creation and life cycle processing are completed through it. MH member variables are no stranger to you if you are familiar with the activity startup process. ApplicationThread acts as a messenger for binder communication and receives AMS scheduling events, such as scheduleLaunchActivity, which internally sends h.launch_activity messages via mH objects. The mH receives this message and calls the handleLaunchActivity to create the Activity object.

This belongs to the Activity startup process and is not discussed in this article. Focus on hook actions.

Hook system Handler mH

# -> StartUpHacker
public class StartUpHacker {
    private static final String TAG = "Matrix.Hacker";
    public static boolean isEnterAnimationComplete = false; public static long sApplicationCreateBeginTime = 0L; public static int sApplicationCreateBeginMethodIndex = 0; public static long sApplicationCreateEndTime = 0L; public static int sApplicationCreateEndMethodIndex = 0; public static int sApplicationCreateScene = -100; // This method is called by static code blocks and executes public static void when resolve is called by class resolvehackSysHandlerCallback() { try { sApplicationCreateBeginTime = System.currentTimeMillis(); sApplicationCreateBeginMethodIndex = MethodBeat.getCurIndex(); Class<? >forName = Class.forName("android.app.ActivityThread");
            Field field = forName.getDeclaredField("sCurrentActivityThread");
            field.setAccessible(true);
            Object activityThreadValue = field.get(forName);
            Field mH = forName.getDeclaredField("mH");
            mH.setAccessible(true); Object handler = mH.get(activityThreadValue); Class<? > handlerClass = handler.getClass().getSuperclass(); Field callbackField = handlerClass.getDeclaredField("mCallback");
            callbackField.setAccessible(true);
            Handler.Callback originalCallback = (Handler.Callback) callbackField.get(handler);
            HackCallback callback = new HackCallback(originalCallback);
            callbackField.set(handler, callback);
            MatrixLog.i(TAG, "hook system handler completed. start:%s", sApplicationCreateBeginTime);
        } catch (Exception e) {
            MatrixLog.e(TAG, "hook system handler err! %s", e.getCause().toString()); }}}Copy the code

The code is simple: take the original handler. Callback inside the mH object and replace it with a new HackCallback.

# StartUpHacker.HackCallbackprivate final static class HackCallback implements Handler.Callback { private final Handler.Callback mOriginalCallback; HackCallback(Handler.Callback callback) { this.mOriginalCallback = callback; } @Override public boolean handleMessage(Message msg) { ... Boolean isLaunchActivity = isLaunchActivity(MSG);if (isLaunchActivity) {
            StartUpHacker.isEnterAnimationComplete = false;
        } else if(MSG. What = = ENTER_ANIMATION_COMPLETE) {/ / records the activity end mark transitions animation StartUpHacker. IsEnterAnimationComplete =true;
        }
        if(! isCreated) {if(isLaunchActivity | | MSG. What = = CREATE_SERVICE | | MSG. What = = RECEIVER) {/ / to the first Activity LAUNCH_ACTIVITY message, Record application created over time StartUpHacker. SApplicationCreateEndTime = SystemClock. UptimeMillis (); StartUpHacker.sApplicationCreateEndMethodIndex = MethodBeat.getCurIndex(); StartUpHacker.sApplicationCreateScene = msg.what; isCreated =true; }}if (null == mOriginalCallback) {
            return false; } // Finally let the original callback process the messagereturnmOriginalCallback.handleMessage(msg); }}Copy the code

With the Hook principle in mind, let’s look at how several key nodes of statistical time are obtained.

  1. Starting the program is actually a static block of code from the MethodBeat class, and we know that the static block is executed when the class is parsed, so it’s not unusual to use it as a starting point for the program’s timing.
  2. The LAUNCH_ACTIVITY message is sent by hook mH class.
  3. Received onActivityCreated callback perception by registered for the aplication registerActivityLifecycleCallbacks application activity within the life cycle.
  4. The onWindowFocusChanged method of the Activity corresponding to the window is dynamically copied by ASM.

Writing this, the whole Trace Canary content is basically finished, which involves a lot of knowledge, including UI drawing process, Activity start process, application start process, packaging process, ASM peg and so on. The author just according to the source code process roughly out of the most core content, most of the technical points of the branch a brush skipped, need readers to supplement, I hope we refueling together, make up the branch of the technology stack.