Matrix原始碼分析————Trace Canary

肥寶發表於2019-04-27

概述

年前,微信開源了Matrix專案,提供了Android、ios的APM實現方案。對於Android端實現,主要包括APK CheckerResource CanaryTrace CanarySQLite LintIO Canary五部分。本文主要介紹Trace Canary的原始碼實現,其他部分的原始碼分析將在後續推出。

程式碼框架分析

Trace Canary通過位元組碼插樁的方式在編譯期預埋了方法進入、方法退出的埋點。執行期,慢函式檢測、FPS檢測、卡頓檢測、啟動檢測使用這些埋點資訊排查具體哪個函式導致的異常。

Matrix原始碼分析————Trace Canary

編譯期方法插樁程式碼分析

Matrix原始碼分析————Trace Canary

程式碼插樁的整體流程如上圖。在打包過程中,hook生成Dex的Task任務,新增方法插樁的邏輯。我們的hook點是在Proguard之後,Class已經被混淆了,所以需要考慮類混淆的問題。

插樁程式碼邏輯大致分為三步:

  • hook原有的Task,執行自己的MatrixTraceTransform,並在最後執行原邏輯

  • 在方法插樁之前先要讀取ClassMapping檔案,獲取混淆前方法、混淆後方法的對映關係並儲存在MappingCollector中。

  • 之後遍歷所有Dir、Jar中的Class檔案,實際程式碼執行的時候遍歷了兩次。

    • 第一次遍歷Class,獲取所有待插樁的Method資訊,並將資訊輸出到methodMap檔案中;
    • 第二次遍歷Class,利用ASM執行Method插樁邏輯。

hook原生打包流程

將實際執行的Transform換成了MatrixTraceTransform

public static void inject(Project project, def variant) {
        //獲取Matrix trace的gradle配置引數
        def configuration = project.matrix.trace
        //hook的Task名
        String hackTransformTaskName = getTransformTaskName(
                configuration.hasProperty('customDexTransformName') ? configuration.customDexTransformName : "",
                "",variant.name
        )
        //同上
        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()) {
                    //找到需要hook的Task名稱
                    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)
                        //反射替換成MatrixTraceTransform,並將原transform傳入,最後執行原transform邏輯
                        field.set(task, new MatrixTraceTransform(project, variant, task.transform))
                        project.logger.warn("transform class after hook: " + task.transform.getClass())
                        break
                    }
                }
            }
        })
    }
複製程式碼

MatrixTraceTransform主要邏輯在transform方法中

@Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        long start = System.currentTimeMillis()
        //是否增量編譯
        final boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental()
        //transform的結果,重定向輸出到這個目錄
        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())
        //獲取Class混淆的mapping資訊,儲存到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 ->
                //收集、重定向目錄中的class
                collectAndIdentifyDir(scrInputMap, dirInput, rootOutput, isIncremental)
            }
            input.jarInputs.each { JarInput jarInput ->
                if (jarInput.getStatus() != Status.REMOVED) {
                    //收集、重定向jar包中的class
                    collectAndIdentifyJar(jarInputMap, scrInputMap, jarInput, rootOutput, isIncremental)
                }
            }
        }
        //收集需要插樁的方法資訊,每個插樁資訊封裝成TraceMethod物件
        MethodCollector methodCollector = new MethodCollector(traceConfig, mappingCollector)
        HashMap<String, TraceMethod> collectedMethodMap = methodCollector.collect(scrInputMap.keySet().toList(), jarInputMap.keySet().toList())
       //執行插樁邏輯,在需要插樁方法的入口、出口新增MethodBeat的i/o邏輯
        MethodTracer methodTracer = new MethodTracer(traceConfig, collectedMethodMap, methodCollector.getCollectedClassExtendMap())
        methodTracer.trace(scrInputMap, jarInputMap)
        //執行原transform的邏輯;預設transformClassesWithDexBuilderForDebug這個task會將Class轉換成Dex
        origTransform.transform(transformInvocation)
        Log.i("Matrix." + getName(), "[transform] cost time: %dms", System.currentTimeMillis() - start)
    }
複製程式碼

收集Dir中的Class資訊

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()
        }
        //增量編譯
        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()
                    //增量編譯模式下之前的build輸出已經重定向到dirOutput;替換成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:
                            //新增、修改的Class檔案,此次需要掃描
                            dirInputMap.put(changedFileInput, changedFileOutput)
                            break
                        case Status.REMOVED:
                            //刪除的Class檔案,將檔案直接刪除
                            changedFileOutput.delete()
                            break
                    }
                    obfuscatedChangedFiles.put(changedFileOutput, status)
                }
                replaceChangedFile(input, obfuscatedChangedFiles)
            }
        } else {
            //全量編譯模式下,所有的Class檔案都需要掃描
            dirInputMap.put(dirInput, dirOutput)
        }
        //反射input,將dirOutput設定為其輸出目錄
        replaceFile(input, dirOutput)
    }
複製程式碼

反射替換輸出目錄的程式碼:

  protected void replaceFile(QualifiedContent input, File newFile) {
        final Field fileField = ReflectUtil.getDeclaredFieldRecursive(input.getClass(), 'file')
        fileField.set(input, newFile
    }
複製程式碼

類似的,收集Jar中的Class資訊


   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 {
            ...
            //這部分程式碼可忽略,微信AutoDex自定義的檔案結構
        }

        replaceFile(input, jarOutput)
    }
複製程式碼

第一次遍歷Class,收集待插樁method

總體流程都在collect方法中

public HashMap collect(List<File> srcFolderList, List<File> dependencyJarList) {
        mTraceConfig.parseBlackFile(mMappingCollector);
        //獲取base模組已經收集到的待插樁方法
        File originMethodMapFile = new File(mTraceConfig.getBaseMethodMap());
        getMethodFromBaseMethod(originMethodMapFile);
        
        Log.i(TAG, "[collect] %s method from %s", mCollectedMethodMap.size(), mTraceConfig.getBaseMethodMap());
        //轉換為混淆後的方法名
        retraceMethodMap(mMappingCollector, mCollectedMethodMap);
        //僅收集目錄、jar包中的class資訊
        collectMethodFromSrc(srcFolderList, true);
        collectMethodFromJar(dependencyJarList, true);
        //收集目錄、jar包中的method資訊
        collectMethodFromSrc(srcFolderList, false);
        collectMethodFromJar(dependencyJarList, false);
        Log.i(TAG, "[collect] incrementCount:%s ignoreMethodCount:%s", mIncrementCount, mIgnoreCount);
        //儲存待插樁的方法資訊到檔案
        saveCollectedMethod(mMappingCollector);
        //儲存不需要插樁的方法資訊到檔案(包括黑名單中的方法)
        saveIgnoreCollectedMethod(mMappingCollector);
        //返回待插樁的方法集合
        return mCollectedMethodMap;

    }
複製程式碼

收集method資訊的邏輯類似,以下面程式碼為例(位元組碼相關操作使用了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) {
                    //僅收集Class資訊
                    visitor = new SingleTraceClassAdapter(Opcodes.ASM5, classWriter);
                } else {
                    //收集Method資訊
                    visitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
                }
                classReader.accept(visitor, 0);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    is.close();
                } catch (Exception e) {
                    // ignore
                }
            }
        }
    }
複製程式碼

個人感覺SingleTraceClassAdapter好像是多餘的,一個TraceClassAdapter可以搞定收集Class、Method的資訊

    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;
            }
            //儲存一個 類->父類 的map(用於查詢Activity的子類)
            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) {
                    //該方法是否與onWindowFocusChange方法的簽名一致(該類中是否複寫了onWindowFocusChange方法,Activity不用考慮Class混淆)
                    hasWindowFocusMethod = mTraceConfig.isWindowFocusChangeMethod(name, desc);
                }
                //CollectMethodNode中執行method收集操作
                return new CollectMethodNode(className, access, name, desc, signature, exceptions);
            }
        }

        @Override
        public void visitEnd() {
            super.visitEnd();
            // collect Activity#onWindowFocusChange
            //onWindowFocusChange方法統一給一個-1的方法id
            TraceMethod traceMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
                    TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
            //沒有過複寫onWindowFocusChange,後續會在該類中插入一個onWindowFocusChange方法,此處先記錄一下這個會被插樁的方法
            if (!hasWindowFocusMethod && mTraceConfig.isActivityOrSubClass(className, mCollectedClassExtendMap) && mTraceConfig.isNeedTrace(traceMethod.className, mMappingCollector)) {
                mCollectedMethodMap.put(traceMethod.getMethodName(), traceMethod);
            }
        }

    }
複製程式碼

如果子類Activity複寫了onWindowFocusChange方法,其對應的methodId就不為-1了;這塊邏輯感覺有點問題~~

    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() {
            super.visitEnd();
            TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);

            if ("<init>".equals(name) /*|| "<clinit>".equals(name)*/) {
                isConstructor = true;
            }
            // filter simple methods
            //忽略空方法、get/set方法、沒有區域性變數的簡單方法,
            if ((isEmptyMethod() || isGetSetMethod() || isSingleMethod())
                    && mTraceConfig.isNeedTrace(traceMethod.className, mMappingCollector)) {
                mIgnoreCount++;
                mCollectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
                return;
            }

            //不在黑名單中的方法加入待插樁的集合;在黑名單中的方法加入ignore插樁的集合
            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);
            }

        }
    }
複製程式碼

第二次遍歷Class,執行method插樁邏輯

入口是MethodTracertrace方法

  public void trace(Map<File, File> srcFolderList, Map<File, File> dependencyJarList) {
        traceMethodFromSrc(srcFolderList);
        traceMethodFromJar(dependencyJarList);
    }
複製程式碼

分別對目錄、jar包插樁

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 = new TraceClassAdapter(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 = new ZipEntry(zipEntryName);
                    FileUtil.addZipEntry(zipOutputStream, newZipEntry, byteArrayInputStream);
    ...
    }
複製程式碼

核心邏輯在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;
            //是否是抽象類、介面
            if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
                this.isABSClass = true;
            }
            //是否是MethodBeat類
            if (mTraceConfig.isMethodBeatClass(className, mCollectedClassExtendMap)) {
                isMethodBeatClass = true;
            }
        }

        @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) {
                    //是否是onWindowFocusChange方法
                    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() {
            TraceMethod traceMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
                    TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
            //如果Activity的子類沒有onWindowFocusChange方法,插入一個onWindowFocusChange方法
            if (!hasWindowFocusMethod && mTraceConfig.isActivityOrSubClass(className, mCollectedClassExtendMap)
                    && mCollectedMethodMap.containsKey(traceMethod.getMethodName())) {
                insertWindowFocusChangeMethod(cv);
            }
            super.visitEnd();
        }
    }
複製程式碼

在待插樁方法的入口、出口新增對應邏輯

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() {
            TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
            if (traceMethod != null) {
                //函式入口處新增邏輯;
                //沒有單獨處理onWindowFocusChange,對於已經複寫onWindowFocusChange的Activity子類,會有問題?
                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)) {
                        //onWindowFocusChange方法統一新增method id = -1的邏輯
                        traceWindowFocusChangeMethod(mv);
                    }
                }
                //函式出口處新增邏輯
                traceMethodCount.incrementAndGet();
                mv.visitLdcInsn(traceMethod.id);
                mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
            }
        }
    }
複製程式碼

對於沒有復現onWindowFocusChange方法的Activity子類,插入一個onWindowFocusChange方法

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

    }
複製程式碼

至此,編譯期插樁的邏輯就結束了;在執行期,檢測到某個方法異常時,會上報一個method id,後端通過下圖的method id到method name的對映關係,追查到有問題的方法

Matrix原始碼分析————Trace Canary

慢函式檢測

目的:檢測影響主執行緒執行的慢函式。

上文講述了在編譯期,會對每個方法的執行體前後新增上MethodBeat.i(int methodId)MethodBeat.o(int methodId)的方法呼叫,且methodId是在編譯期生成的,在執行時是一個寫死的常量。通過編譯期的這個操作,就能感知到具體每個方法的進入、退出動作。下面來看下這兩個方法的內部實現

/**
     * 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) {
            ...
        }
    }
複製程式碼

統計了當應用處於前臺時,在主執行緒執行的方法的進入、退出。這些資訊最後儲存在MethodBeatBuffer中。當主執行緒有疑似慢函式存在時,讀取Buffer的資料,分析可能的慢函式,並上報json資料到後端(後端將methodId轉換為具體的方法宣告)。

疑似發生慢函式的實際有兩個:一個是掉幀的場景,一個是類似ANR這樣長時間主執行緒阻塞UI繪製的場景。

  • 掉幀的場景

內部FrameBeat類實現了Choreographer.FrameCallback,可以感知每一幀的繪製時間。通過前後兩幀的時間差判斷是否有慢函式發生。

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

        int index = getMethodBeat().getCurIndex();
        //判斷是否有慢函式
        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);

    }
複製程式碼
  • 主執行緒長時間阻塞UI繪製的場景

LazyScheduler內有一個HandlerThread,呼叫LazyScheduler.setup方法會向這個HandlerThread的MQ傳送一個延時5s的訊息。若沒有發生類似ANR的場景,在每一幀的doFrame回撥中取消這個訊息,同時傳送一個新的延時5s的訊息(正常情況下訊息是得不到執行的);若發生類似ANR的情況,doFrame沒有被回撥,這個延時5s的訊息得到執行,將回撥到onTimeExpire方法

  @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);
    }
複製程式碼

當檢測到慢函式時,會在後臺執行緒完成慢函式的分析

 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() {
            analyse(buffer);
        }

        private void analyse(long[] buffer) {
            ...
            //分析邏輯主要是找出最耗時的方法,可自行閱讀

}
複製程式碼

FPS檢測

目的:檢測繪製過程中的FPS數量。

獲取DectorView的ViewTreeObserver,感知UI繪製的開始

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

複製程式碼

通過Choreographer.FrameCallback,感知UI繪製的結束

  @Override
    public void doFrame(long lastFrameNanos, long frameNanos) {
        if (!isInvalid && isDrawing && isEnterAnimationComplete() && mTraceConfig.isTargetScene(getScene())) {
            handleDoFrame(lastFrameNanos, frameNanos, getScene());
        }
        isDrawing = false;
    }
複製程式碼

理論上使用者更關心的是繪製過程中FPS過低導致的卡頓(UI靜止的情況下,使用者是感知不到FPS低的)

doFrame方法中,記錄每一幀的資料,其中scene這個欄位標識一個頁面

  @Override
    public void onChange(final Activity activity, final Fragment fragment) {
        this.mScene = TraceConfig.getSceneForString(activity, fragment);
    }
複製程式碼

onChange的預設實現是通過Application的ActivityLifecycleCallbacks回撥感知Activity的變化

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

複製程式碼

FPS資料預設是2分鐘分析一次(前臺情況下),切後臺時後臺輪詢執行緒停止。


    /**
     * report FPS
     */
    private void doReport() {
        ...
        //資料分析邏輯可行閱讀
    }
複製程式碼

卡頓檢測

目的:檢測UI繪製過程中的卡頓情況。

卡頓檢測與FPS檢測類似,在每一幀的`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);
        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() {
                            listener.getHandler().post(new AsyncDoFrameTask(listener,
                                    lastFrameNanos, frameNanos, getScene(), droppedCount));
                        }
                    });

                }
            }
複製程式碼

啟動檢測

目的:檢測啟動階段耗時

應用啟動時,會直接對ActivityThread類hook

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() {
        try {
            //這個類被載入的時間,認為是整個App的啟動開始時間
            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());
        }
    }
}
複製程式碼

代理原有的Handler.Callback,感知Application onCreate的結束時間

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) {
                //傳送啟動Activity等訊息,認為是Application onCreate的結束時間
                Hacker.sApplicationCreateEndTime = System.currentTimeMillis();
                Hacker.sApplicationCreateEndMethodIndex = MethodBeat.getCurIndex();
                Hacker.sApplicationCreateScene = msg.what;
                isCreated = true;
            }
        }
        if (null == mOriginalCallback) {
            return false;
        }
        return mOriginalCallback.handleMessage(msg);
    }
}
複製程式碼

記錄第一個Activity的onCreate時間

 @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);
        }
    }
複製程式碼

記錄Activity獲取焦點的時間(在編譯期,在Activity子類的onWindowFocusChange方法中插入MethodBeat.at方法)

 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);
            }
        }
    }
複製程式碼

當Activity獲取到焦點時,認為啟動階段結束(若有SplashActivity,則記錄下一個Activity獲取焦點的時間)

    @Override
    public void onActivityEntered(Activity activity, boolean isFocus, int nowIndex, long[] buffer) {
       ...啟動資料分析
    }
複製程式碼

總結

Matrix Trace檢測巧妙的利用了編譯期位元組碼插樁技術,優化了移動端的FPS、卡頓、啟動的檢測手段;藉助Matrix Trace,開發人員可以從方法級別來做優化。

相關文章