An overview of the

Two years ago, wechat opened the Matrix project and provided APM implementation schemes for Android and ios. For Android, it mainly includes APK Checker, Resource Canary, Trace Canary, SQLite Lint, and IO Canary. This article mainly introduces the source code implementation of Trace Canary, and the rest of the source code analysis will be published later.

Code framework analysis

Trace Canary prefixes method entry and method exit points at compile time by bytecode staking. During the runtime, slow function detection, FPS detection, lag detection, and start detection use the buried point information to identify which function causes the exception.

Compilation – time method staking code analysis

The overall process of code staking is shown in the figure above. During the packaging process, hook generates Dex Task and adds the logic of method piling. Our hook point is that after Proguard, the Class has been obfuscated, so we need to worry about Class obfuscation.

The piling code logic can be roughly divided into three steps:

  • Hook the original Task, perform its own MatrixTraceTransform, and execute the original logic at the end

  • Before method staking, read the ClassMapping file to obtain the mapping relationship of the methods before and after the confusion and store it in the MappingCollector.

  • I then iterate through all the Class files in Dir and Jar, twice during the actual code execution.

    • For the first time, Class is traversed to obtain all Method information to be inserted, and the information is output to methodMap file.
    • The second time through Class, using ASM to execute Method piling logic.

Hook native packaging process

Change the actual executed Transform to a MatrixTraceTransform

public static void inject(Project project, def variant) {
        // Get gradle configuration parameters for Matrix Trace
        def configuration = project.matrix.trace
        The name of the Task / / hook
        String hackTransformTaskName = getTransformTaskName(
                configuration.hasProperty('customDexTransformName')? configuration.customDexTransformName :""."",variant.name
        )
        / / same as above
        String hackTransformTaskNameForWrapper = getTransformTaskName(
                configuration.hasProperty('customDexTransformName')? configuration.customDexTransformName :""."Builder",variant.name
        )

        project.logger.info("prepare inject dex transform :" + hackTransformTaskName +" hackTransformTaskNameForWrapper:"+hackTransformTaskNameForWrapper)

        project.getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
            @Override
            public void graphPopulated(TaskExecutionGraph taskGraph) {
                for (Task task : taskGraph.getAllTasks()) {
                    // Find the Task name to hook
                    if((task.name.equalsIgnoreCase(hackTransformTaskName) || task.name.equalsIgnoreCase(hackTransformTaskNameForWrapper)) && ! (((TransformTask) task).getTransform()instanceof MatrixTraceTransform)) {
                        project.logger.warn("find dex transform. transform class: " + task.transform.getClass() + " . task name: " + task.name)
                        project.logger.info("variant name: " + variant.name)
                        Field field = TransformTask.class.getDeclaredField("transform")
                        field.setAccessible(true)
                        // Reflection is replaced with MatrixTraceTransform, and the original transform is passed in, and the original transform logic is executed
                        field.set(task, new MatrixTraceTransform(project, variant, task.transform))
                        project.logger.warn("transform class after hook: " + task.transform.getClass())
                        break}}}})}Copy the code

MatrixTraceTransform The primary logic is in the transform method

@Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        long start = System.currentTimeMillis()
        // Whether to incrementally compile
        final boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental()
        // Redirect output to this directory
        final File rootOutput = new File(project.matrix.output, "classes/${getName()}/")
        if(! rootOutput.exists()) { rootOutput.mkdirs() }final TraceBuildConfig traceConfig = initConfig()
        Log.i("Matrix." + getName(), "[transform] isIncremental:%s rootOutput:%s", isIncremental, rootOutput.getAbsolutePath())
        // Get the Class confused mapping information and store it in the mappingCollector
        final MappingCollector mappingCollector = new MappingCollector()
        File mappingFile = new File(traceConfig.getMappingPath());
        if (mappingFile.exists() && mappingFile.isFile()) {
            MappingReader mappingReader = new MappingReader(mappingFile);
            mappingReader.read(mappingCollector)
        }

        Map<File, File> jarInputMap = new HashMap<>()
        Map<File, File> scrInputMap = new HashMap<>()

        transformInvocation.inputs.each { TransformInput input ->
            input.directoryInputs.each { DirectoryInput dirInput ->
                // Collect and redirect classes in the directory
                collectAndIdentifyDir(scrInputMap, dirInput, rootOutput, isIncremental)
            }
            input.jarInputs.each { JarInput jarInput ->
                if(jarInput.getStatus() ! = Status.REMOVED) {// Collect and redirect classes from jar packages
                    collectAndIdentifyJar(jarInputMap, scrInputMap, jarInput, rootOutput, isIncremental)
                }
            }
        }
        // Collect information about the method of piling. Each piling information is encapsulated into a TraceMethod object
        MethodCollector methodCollector = new MethodCollector(traceConfig, mappingCollector)
        HashMap<String, TraceMethod> collectedMethodMap = methodCollector.collect(scrInputMap.keySet().toList(), jarInputMap.keySet().toList())
       // Add MethodBeat I/O logic to the entries and exits that require MethodBeat
        MethodTracer methodTracer = new MethodTracer(traceConfig, collectedMethodMap, methodCollector.getCollectedClassExtendMap())
        methodTracer.trace(scrInputMap, jarInputMap)
        // Execute the original transform logic; The default transformClassesWithDexBuilderForDebug this task will transform the Class into Dex
        origTransform.transform(transformInvocation)
        Log.i("Matrix." + getName(), "[transform] cost time: %dms", System.currentTimeMillis() - start)
    }
Copy the code

Collect Class information in Dir

private void collectAndIdentifyDir(Map<File, File> dirInputMap, DirectoryInput input, File rootOutput, boolean isIncremental) {
        final File dirInput = input.file
        final File dirOutput = new File(rootOutput, input.file.getName())
        if(! dirOutput.exists()) { dirOutput.mkdirs() }// Incremental compilation
        if (isIncremental) {
            if(! dirInput.exists()) { dirOutput.deleteDir() }else {
                final Map<File, Status> obfuscatedChangedFiles = new HashMap<>()
                final String rootInputFullPath = dirInput.getAbsolutePath()
                final String rootOutputFullPath = dirOutput.getAbsolutePath()
                input.changedFiles.each { Map.Entry<File, Status> entry ->
                    final File changedFileInput = entry.getKey()
                    final String changedFileInputFullPath = changedFileInput.getAbsolutePath()
                    // The previous build output in incremental build mode is redirected to dirOutput; Replace it with the directory output
                    final File changedFileOutput = new File(
                            changedFileInputFullPath.replace(rootInputFullPath, rootOutputFullPath)
                    )
                    final Status status = entry.getValue()
                    switch (status) {
                        case Status.NOTCHANGED:
                            break
                        case Status.ADDED:
                        case Status.CHANGED:
                            // New Class files need to be scanned this time
                            dirInputMap.put(changedFileInput, changedFileOutput)
                            break
                        case Status.REMOVED:
                            // Delete the Class file directly
                            changedFileOutput.delete()
                            break
                    }
                    obfuscatedChangedFiles.put(changedFileOutput, status)
                }
                replaceChangedFile(input, obfuscatedChangedFiles)
            }
        } else {
            In full compile mode, all Class files need to be scanned
            dirInputMap.put(dirInput, dirOutput)
        }
        // reflect input, set dirOutput to its output directory
        replaceFile(input, dirOutput)
    }
Copy the code

Reflection replaces the output directory code:

  protected void replaceFile(QualifiedContent input, File newFile) {
        final Field fileField = ReflectUtil.getDeclaredFieldRecursive(input.getClass(), 'file')
        fileField.set(input, newFile
    }
Copy the code

Similarly, collect the Class information in the Jar


   private void collectAndIdentifyJar(Map<File, File> jarInputMaps, Map<File, File> dirInputMaps, JarInput input, File rootOutput, boolean isIncremental) {
        final File jarInput = input.file
        final File jarOutput = new File(rootOutput, getUniqueJarName(jarInput))
        if (IOUtil.isRealZipOrJar(jarInput)) {
            switch (input.status) {
                case Status.NOTCHANGED:
                    if (isIncremental) {
                        break
                    }
                case Status.ADDED:
                case Status.CHANGED:
                    jarInputMaps.put(jarInput, jarOutput)
                    break
                case Status.REMOVED:
                    break}}else{...// This part of the code can be ignored, wechat AutoDex custom file structure
        }

        replaceFile(input, jarOutput)
    }
Copy the code

Traverse Class for the first time and collect method pile to be inserted

The overall process is in the collect method

public HashMap collect(List<File> srcFolderList, List<File> dependencyJarList) {
        mTraceConfig.parseBlackFile(mMappingCollector);
        // Obtain the methods to be inserted that have been collected by the BASE module
        File originMethodMapFile = new File(mTraceConfig.getBaseMethodMap());
        getMethodFromBaseMethod(originMethodMapFile);
        
        Log.i(TAG, "[collect] %s method from %s", mCollectedMethodMap.size(), mTraceConfig.getBaseMethodMap());
        // Convert to the obfuscated method name
        retraceMethodMap(mMappingCollector, mCollectedMethodMap);
        // Collect only class information in directories and JAR packages
        collectMethodFromSrc(srcFolderList, true);
        collectMethodFromJar(dependencyJarList, true);
        // Collect method information in directories and JAR packages
        collectMethodFromSrc(srcFolderList, false);
        collectMethodFromJar(dependencyJarList, false);
        Log.i(TAG, "[collect] incrementCount:%s ignoreMethodCount:%s", mIncrementCount, mIgnoreCount);
        // Store the method information to the file
        saveCollectedMethod(mMappingCollector);
        // Store information about methods that do not require piling into files (including methods in the blacklist)
        saveIgnoreCollectedMethod(mMappingCollector);
        // return the set of methods to be inserted
        return mCollectedMethodMap;

    }
Copy the code

The logic for collecting method information is similar, using the following code as an example (bytecode related operations use ASM)

  private void innerCollectMethodFromSrc(File srcFile, boolean isSingle) {
        ArrayList<File> classFileList = new ArrayList<>();
        if (srcFile.isDirectory()) {
            listClassFiles(classFileList, srcFile);
        } else {
            classFileList.add(srcFile);
        }

        for (File classFile : classFileList) {
            InputStream is = null;
            try {
                is = new FileInputStream(classFile);
                ClassReader classReader = new ClassReader(is);
                ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                ClassVisitor visitor;
                if (isSingle) {
                    // Collect only Class information
                    visitor = new SingleTraceClassAdapter(Opcodes.ASM5, classWriter);
                } else {
                    // Collect Method information
                    visitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
                }
                classReader.accept(visitor, 0);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    is.close();
                } catch (Exception e) {
                    // ignore}}}}Copy the code

A SingleTraceClassAdapter can be used to collect information about classes and methods

    private class TraceClassAdapter extends ClassVisitor {
        private String className;
        private boolean isABSClass = false;
        private boolean hasWindowFocusMethod = false;

        TraceClassAdapter(int i, ClassVisitor classVisitor) {
            super(i, classVisitor);
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces);
            this.className = name;
            if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
                this.isABSClass = true;
            }
            // Store a class -> map of the parent class (used to find subclasses of activities)
            mCollectedClassExtendMap.put(className, superName);

        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc,
                                         String signature, String[] exceptions) {
            if (isABSClass) {
                return super.visitMethod(access, name, desc, signature, exceptions);
            } else {
                if(! hasWindowFocusMethod) {// Does this method match the signature of the onWindowFocusChange method?
                    hasWindowFocusMethod = mTraceConfig.isWindowFocusChangeMethod(name, desc);
                }
                Method collection is performed in CollectMethodNode
                return newCollectMethodNode(className, access, name, desc, signature, exceptions); }}@Override
        public void visitEnd(a) {
            super.visitEnd();
            // collect Activity#onWindowFocusChange
            // The onWindowFocusChange method uniformly gives a method ID of -1
            TraceMethod traceMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
                    TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
            // The onWindowFocusChange method is not overwritten. An onWindowFocusChange method will be inserted into the class later
            if(! hasWindowFocusMethod && mTraceConfig.isActivityOrSubClass(className, mCollectedClassExtendMap) && mTraceConfig.isNeedTrace(traceMethod.className, mMappingCollector)) { mCollectedMethodMap.put(traceMethod.getMethodName(), traceMethod); }}}Copy the code

If the Activity subclass copies the onWindowFocusChange method, its methodId is not -1. There is something wrong with this logic

    private class CollectMethodNode extends MethodNode {
        private String className;
        private boolean isConstructor;


        CollectMethodNode(String className, int access, String name, String desc,
                          String signature, String[] exceptions) {
            super(Opcodes.ASM5, access, name, desc, signature, exceptions);
            this.className = className;
        }

        @Override
        public void visitEnd(a) {
            super.visitEnd();
            TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);

            if ("<init>".equals(name) /*|| "<clinit>".equals(name)*/) {
                isConstructor = true;
            }
            // filter simple methods
            // Ignore empty methods, get/set methods, simple methods with no local variables,
            if ((isEmptyMethod() || isGetSetMethod() || isSingleMethod())
                    && mTraceConfig.isNeedTrace(traceMethod.className, mMappingCollector)) {
                mIgnoreCount++;
                mCollectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
                return;
            }

            // Add the method not in the blacklist to the pile set; Add methods to the blacklist to ignore pile collection
            if(mTraceConfig.isNeedTrace(traceMethod.className, mMappingCollector) && ! mCollectedMethodMap.containsKey(traceMethod.getMethodName())) { traceMethod.id = mMethodId.incrementAndGet(); mCollectedMethodMap.put(traceMethod.getMethodName(), traceMethod); mIncrementCount++; }else if (!mTraceConfig.isNeedTrace(traceMethod.className, mMappingCollector)
                    && !mCollectedBlackMethodMap.containsKey(traceMethod.className)) {
                mIgnoreCount++;
                mCollectedBlackMethodMap.put(traceMethod.getMethodName(), traceMethod);
            }

        }
    }
Copy the code

For the second time through Class, execute method piling logic

The entry point is the Trace method for MethodTracer

  public void trace(Map<File, File> srcFolderList, Map<File, File> dependencyJarList) {
        traceMethodFromSrc(srcFolderList);
        traceMethodFromJar(dependencyJarList);
    }
Copy the code

Peg directories and JAR packages respectively

private void innerTraceMethodFromSrc(File input, File output) {...if (mTraceConfig.isNeedTraceClass(classFile.getName())) {
                    is = new FileInputStream(classFile);
                    ClassReader classReader = new ClassReader(is);
                    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                    ClassVisitor classVisitor = newTraceClassAdapter(Opcodes.ASM5, classWriter); classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES); . }private void innerTraceMethodFromJar(File input, File output) {...if (mTraceConfig.isNeedTraceClass(zipEntryName)) {
                    InputStream inputStream = zipFile.getInputStream(zipEntry);
                    ClassReader classReader = new ClassReader(inputStream);
                    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                    ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
                    byte[] data = classWriter.toByteArray();
                    InputStream byteArrayInputStream = new ByteArrayInputStream(data);
                    ZipEntry newZipEntry = newZipEntry(zipEntryName); FileUtil.addZipEntry(zipOutputStream, newZipEntry, byteArrayInputStream); . }Copy the code

The core logic is in the TraceClassAdapter

private class TraceClassAdapter extends ClassVisitor {

        private String className;
        private boolean isABSClass = false;
        private boolean isMethodBeatClass = false;
        private boolean hasWindowFocusMethod = false;

        TraceClassAdapter(int i, ClassVisitor classVisitor) {
            super(i, classVisitor);
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            super.visit(version, access, name, signature, superName, interfaces);
            this.className = name;
            // Is it an abstract class or interface
            if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
                this.isABSClass = true;
            }
            // Is the MethodBeat class
            if (mTraceConfig.isMethodBeatClass(className, mCollectedClassExtendMap)) {
                isMethodBeatClass = true; }}@Override
        public MethodVisitor visitMethod(int access, String name, String desc,
                                         String signature, String[] exceptions) {
             // The interface is not inserted
            if (isABSClass) {
                return super.visitMethod(access, name, desc, signature, exceptions);
            } else {
                if(! hasWindowFocusMethod) {// Whether the onWindowFocusChange method is used
                    hasWindowFocusMethod = mTraceConfig.isWindowFocusChangeMethod(name, desc);
                }
                MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
                return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className, hasWindowFocusMethod, isMethodBeatClass); }}@Override
        public void visitEnd(a) {
            TraceMethod traceMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
                    TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
            // If the Activity subclass does not have onWindowFocusChange, insert an onWindowFocusChange method
            if(! hasWindowFocusMethod && mTraceConfig.isActivityOrSubClass(className, mCollectedClassExtendMap) && mCollectedMethodMap.containsKey(traceMethod.getMethodName())) { insertWindowFocusChangeMethod(cv); }super.visitEnd(); }}Copy the code

Add corresponding logic at the entrance and exit of the piling method to be inserted

rivate class TraceMethodAdapter extends AdviceAdapter {

        private final String methodName;
        private final String name;
        private final String className;
        private final boolean hasWindowFocusMethod;
        private final boolean isMethodBeatClass;

        protected TraceMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className,
                                     boolean hasWindowFocusMethod, boolean isMethodBeatClass) {
            super(api, mv, access, name, desc);
            TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);
            this.methodName = traceMethod.getMethodName();
            this.isMethodBeatClass = isMethodBeatClass;
            this.hasWindowFocusMethod = hasWindowFocusMethod;
            this.className = className;
            this.name = name;
        }

        @Override
        protected void onMethodEnter(a) {
            TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
            if(traceMethod ! =null) {
                // Add logic to function entry;
                // onWindowFocusChange is not handled separately, and there is a problem with Activity subclasses that have already overridden onWindowFocusChange?
                traceMethodCount.incrementAndGet();
                mv.visitLdcInsn(traceMethod.id);
                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)) {
                        // the onWindowFocusChange method adds logic with method ID = -1traceWindowFocusChangeMethod(mv); }}// Add logic at function exit
                traceMethodCount.incrementAndGet();
                mv.visitLdcInsn(traceMethod.id);
                mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o"."(I)V".false); }}}Copy the code

For Activity subclasses that do not reproduce the onWindowFocusChange method, insert an onWindowFocusChange method

  private void insertWindowFocusChangeMethod(ClassVisitor cv) {
        MethodVisitor methodVisitor = cv.visitMethod(Opcodes.ACC_PUBLIC, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD,
                TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS, null.null);
        methodVisitor.visitCode();
        methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        methodVisitor.visitVarInsn(Opcodes.ILOAD, 1);
        methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, TraceBuildConstants.MATRIX_TRACE_ACTIVITY_CLASS, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD,
                TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS, false);
        traceWindowFocusChangeMethod(methodVisitor);
        methodVisitor.visitInsn(Opcodes.RETURN);
        methodVisitor.visitMaxs(2.2);
        methodVisitor.visitEnd();

    }
Copy the code

At this point, the logic of piling at compile time ends; During the runtime, when a method is abnormal, a method ID will be reported, and the backend will track down the problematic method through the mapping between method ID and Method name as shown in the following figure

Slow function detection

Purpose: To detect slow functions that affect main thread execution.

Methodbeat. I (int methodId) and methodBeat. o(int methodId) are added before and after each method is executed at compile time. MethodId is generated at compile time and is a dead constant at run time. By doing this at compile time, you can sense the entry and exit actions for each specific method. Let’s look at the internal implementations of these two methods

/**
     * hook method when it's called in.
     *
     * @param methodId
     */
    public static void i(int methodId) {
        if (isBackground) {
            return; }... isRealTrace =true;
        if (isCreated && Thread.currentThread() == sMainThread) {
           ...
        } else if(! isCreated && Thread.currentThread() == sMainThread && sBuffer ! =null) {.. }}/**
     * hook method when it's called out.
     *
     * @param methodId
     */
    public static void o(int methodId) {
        if (isBackground || null == sBuffer) {
            return;
        }
        if (isCreated && Thread.currentThread() == sMainThread) {
            ...
        } else if (!isCreated && Thread.currentThread() == sMainThread) {
            ...
        }
    }
Copy the code

Counts the entry and exit of methods executed on the main thread while the application is in the foreground. This information is eventually stored in MethodBeat’s Buffer. When the main thread has a suspected slow function, the Buffer data is read, the possible slow function is analyzed, and the JSON data is reported to the back end (the back end converts methodId into a specific method declaration).

There are actually two suspected slow functions: one is a frame drop scenario, and the other is a scenario like ANR where the main thread blocks UI drawing for a long time.

  • The scene of dropping frames

Internal FrameBeat class implements the Choreographer FrameCallback, can sense of each frame drawing time. Determine whether a slow function occurs by the time difference between the two frames.

  @Override
    public void doFrame(long lastFrameNanos, long frameNanos) {
        if (isIgnoreFrame) {
            mActivityCreatedInfoMap.clear();
            setIgnoreFrame(false);
            getMethodBeat().resetIndex();
            return;
        }

        int index = getMethodBeat().getCurIndex();
        // Determine if there is a slow function
        if (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();
        mLazyScheduler.setUp(this.false);

    }
Copy the code
  • The main thread blocks UI drawing for a long time

There is a HandlerThread in LazyScheduler, and calling the lazyScheduler.setup method sends a 5s delay message to the HandlerThread’s MQ. If anR-like scenarios do not occur, cancel this message in the doFrame callback of each frame and send a new message with a delay of 5s (normally messages are not executed); If an ANR-like situation occurs and doFrame is not called back, the 5s delayed message is executed and will be called back to the onTimeExpire method

  @Override
    public void onTimeExpire(a) {
        // 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);
        // There are slow functions
        handleBuffer(Type.ANR, 0, getMethodBeat().getCurIndex() - 1, getMethodBeat().getBuffer(), null, Constants.DEFAULT_ANR, happenedAnrTime, -1);
    }
Copy the code

When a slow function is detected, the analysis of the slow function is completed in the background thread

 private final class AnalyseTask implements Runnable {

        private final long[] buffer;
        private final AnalyseExtraInfo analyseExtraInfo;

        private AnalyseTask(long[] buffer, AnalyseExtraInfo analyseExtraInfo) {
            this.buffer = buffer;
            this.analyseExtraInfo = analyseExtraInfo;
        }

        private long getTime(long trueId) {
            return trueId & 0x7FFFFFFFFFFL;
        }

        private int getMethodId(long trueId) {
            return (int) ((trueId >> 43) & 0xFFFFFL);
        }

        private boolean isIn(long trueId) {
            return ((trueId >> 63) & 0x1) = =1;
        }

        @Override
        public void run(a) {
            analyse(buffer);
        }

        private void analyse(long[] buffer) {...// Analysis logic is mainly to find the most time-consuming method

}
Copy the code

FPS testing

Objective: To detect the number of FPS during drawing.

Get the ViewTreeObserver of the DectorView to sense the start of the UI drawing

    private void addDrawListener(final Activity activity) {
        activity.getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run(a) {
                activity.getWindow().getDecorView().getViewTreeObserver().removeOnDrawListener(FPSTracer.this);
                activity.getWindow().getDecorView().getViewTreeObserver().addOnDrawListener(FPSTracer.this); }}); }@Override
    public void onDraw(a) {
        isDrawing = true;
    }

Copy the code

Through the Choreographer FrameCallback, perceive the UI end of drawing

  @Override
    public void doFrame(long lastFrameNanos, long frameNanos) {
        if(! isInvalid && isDrawing && isEnterAnimationComplete() && mTraceConfig.isTargetScene(getScene())) { handleDoFrame(lastFrameNanos, frameNanos, getScene()); } isDrawing =false;
    }
Copy the code

Theoretically, the user is more concerned about the lag caused by the low FPS during the drawing process (when the UI is still, the user will not feel the low FPS).

In the doFrame method, the data for each frame is recorded, with the scene field identifying a page

  @Override
    public void onChange(final Activity activity, final Fragment fragment) {
        this.mScene = TraceConfig.getSceneForString(activity, fragment);
    }
Copy the code

OnChange default implementation is through the Application of ActivityLifecycleCallbacks callback perceive the change of the Activity

@Override
    public void onActivityResumed(final Activity activity) {...if(! activityHash.equals(mCurActivityHash)) {for (IObserver listener : mObservers) {
                listener.onChange(activity, null); } mCurActivityHash = activityHash; }... }Copy the code

By default, FPS data is analyzed every 2 minutes (in the foreground case). The background polling thread stops when the background is cut.


    /** * report FPS */
    private void doReport(a) {...// Data analysis logic is readable
    }
Copy the code

Caton detection

Objective: To detect the lag in UI drawing.

Stall detection is similar to FPS detection. It determines whether stall occurs in the ‘doFrame callback’ of each frame, and sends data to the background analysis thread for processing if stall occurs.

  @Override
    public void doFrame(final long lastFrameNanos, final long frameNanos) {
        if(! isDrawing) {return;
        }
        isDrawing = false;
        final int droppedCount = (int) ((frameNanos - lastFrameNanos) / REFRESH_RATE_MS);
        if (droppedCount > 1) {
            for (final IDoFrameListener listener : mDoFrameListenerList) {
                listener.doFrameSync(lastFrameNanos, frameNanos, getScene(), droppedCount);
                if (null! = listener.getHandler()) { listener.getHandler().post(new Runnable() {
                        @Override
                        public void run(a) {
                            listener.getHandler().post(newAsyncDoFrameTask(listener, lastFrameNanos, frameNanos, getScene(), droppedCount)); }}); }}Copy the code

Start the test

Objective: Check startup time

When the application starts, it hooks the ActivityThread class directly

public class Hacker {
    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;

    public static void hackSysHandlerCallback(a) {
        try {
            // The time this class is loaded is considered to be the start time of the entire AppsApplicationCreateBeginTime = 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

Callback, the agent’s original handler. Callback, senses when Application onCreate ends

public class HackCallback implements Handler.Callback {
    private static final String TAG = "Matrix.HackCallback";
    private static final int LAUNCH_ACTIVITY = 100;
    private static final int ENTER_ANIMATION_COMPLETE = 149;
    private static final int CREATE_SERVICE = 114;
    private static final int RECEIVER = 113;
    private static boolean isCreated = false;

    private final Handler.Callback mOriginalCallback;

    public HackCallback(Handler.Callback callback) {
        this.mOriginalCallback = callback;
    }

    @Override
    public boolean handleMessage(Message msg) {
// MatrixLog.i(TAG, "[handleMessage] msg.what:%s begin:%s", msg.what, System.currentTimeMillis());
        if (msg.what == LAUNCH_ACTIVITY) {
            Hacker.isEnterAnimationComplete = false;
        } else if (msg.what == ENTER_ANIMATION_COMPLETE) {
            Hacker.isEnterAnimationComplete = true;
        }
        if(! isCreated) {if (msg.what == LAUNCH_ACTIVITY || msg.what == CREATE_SERVICE || msg.what == RECEIVER) {
                // Send a message to start the Activity, which is considered the end time of Application onCreate
                Hacker.sApplicationCreateEndTime = System.currentTimeMillis();
                Hacker.sApplicationCreateEndMethodIndex = MethodBeat.getCurIndex();
                Hacker.sApplicationCreateScene = msg.what;
                isCreated = true; }}if (null == mOriginalCallback) {
            return false;
        }
        returnmOriginalCallback.handleMessage(msg); }}Copy the code

Record the onCreate time of the first Activity

 @Override
    public void onActivityCreated(Activity activity) {
        super.onActivityCreated(activity);
        if (isFirstActivityCreate && mFirstActivityMap.isEmpty()) {
            String activityName = activity.getComponentName().getClassName();
            mFirstActivityIndex = getMethodBeat().getCurIndex();
            mFirstActivityName = activityName;
            mFirstActivityMap.put(activityName, System.currentTimeMillis());
            MatrixLog.i(TAG, "[onActivityCreated] first activity:%s index:%s", mFirstActivityName, mFirstActivityIndex);
            getMethodBeat().lockBuffer(true); }}Copy the code

Record when the Activity gets focus (at compile time, insert methodbeat.at in the Activity subclass’s onWindowFocusChange method)

 public static void at(Activity activity, boolean isFocus) {
        MatrixLog.i(TAG, "[AT] Activity: %s, isCreated: %b sListener Size: %d, isFocus: %b",
                activity.getClass().getSimpleName(), isCreated, sListeners.size(), isFocus);
        if (isCreated && Thread.currentThread() == sMainThread) {
            for (IMethodBeatListener listener : sListeners) {
                listener.onActivityEntered(activity, isFocus, sIndex - 1, sBuffer); }}}Copy the code

The startup phase is considered to end when the Activity gains focus (if there is a SplashActivity, record the time when the Activity gains focus)

    @Override
    public void onActivityEntered(Activity activity, boolean isFocus, int nowIndex, long[] buffer) {... Start data analysis}Copy the code

conclusion

Matrix Trace detection makes clever use of compile-time bytecode pegging technology to optimize detection means of FPS, lag and startup on mobile terminals. With Matrix Trace, developers can optimize at the method level.