Matrix源碼分析————Trace Canary

概述

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

代碼框架分析

Trace Canary經過字節碼插樁的方式在編譯期預埋了方法進入、方法退出的埋點。運行期,慢函數檢測、FPS檢測、卡頓檢測、啓動檢測使用這些埋點信息排查具體哪一個函數致使的異常。android

編譯期方法插樁代碼分析

代碼插樁的總體流程如上圖。在打包過程當中,hook生成Dex的Task任務,添加方法插樁的邏輯。咱們的hook點是在Proguard以後,Class已經被混淆了,因此須要考慮類混淆的問題。ios

插樁代碼邏輯大體分爲三步:git

  • hook原有的Task,執行本身的MatrixTraceTransform,並在最後執行原邏輯github

  • 在方法插樁以前先要讀取ClassMapping文件,獲取混淆前方法、混淆後方法的映射關係並存儲在MappingCollector中。json

  • 以後遍歷全部Dir、Jar中的Class文件,實際代碼執行的時候遍歷了兩次。後端

    • 第一次遍歷Class,獲取全部待插樁的Method信息,並將信息輸出到methodMap文件中;
    • 第二次遍歷Class,利用ASM執行Method插樁邏輯。

hook原生打包流程

將實際執行的Transform換成了MatrixTraceTransformapi

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信息app

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的映射關係,追查到有問題的方法

慢函數檢測

目的:檢測影響主線程執行的慢函數。

上文講述了在編譯期,會對每一個方法的執行體先後添加上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,開發人員能夠從方法級別來作優化。

相關文章
相關標籤/搜索