Matrix源码分析————Trace Canary

栏目: IOS · Android · 发布时间: 5年前

内容简介:年前,微信开源了Trace Canary通过字节码插桩的方式在编译期预埋了方法进入、方法退出的埋点。运行期,慢函数检测、FPS检测、卡顿检测、启动检测使用这些埋点信息排查具体哪个函数导致的异常。代码插桩的整体流程如上图。在打包过程中,hook生成Dex的Task任务,添加方法插桩的逻辑。我们的hook点是在Proguard之后,Class已经被混淆了,所以需要考虑类混淆的问题。

年前,微信开源了 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,开发人员可以从方法级别来做优化。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

游戏的设计与开发

游戏的设计与开发

次世代工作室 / 人民交通出版社 / 2003-12 / 50.00元

一起来看看 《游戏的设计与开发》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

URL 编码/解码
URL 编码/解码

URL 编码/解码