Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。

文章目录

  • Gradle开发,字节码插桩,路由组件自动注册
    • 一.字节码插桩是什么
      • 1.2 场景
      • 1.3 技术原理
    • 二.页面路由自动注册
      • 2.1新建Transform
      • 2.2收集类信息
      • 2.3生成映射表
    • 三.汇总所有的映射表
      • 3.1目标类的结构
      • 3.2安装引入ASM插件
      • 3.3生成字节码的逻辑
      • 3.4transform中字节码写入本地文件

Gradle开发,字节码插桩,路由组件自动注册

一.字节码插桩是什么

字节码,字节码插桩。

1.2 场景

监控方法耗时,代码插入,代码替换。

1.3 技术原理

.java文件编译后变成.class文件,然后再被编译为.dex文件,最终打包形成apk文件。

在.class转化为.dex文件之前,在我们的插件中搞一个Transform,拿到.class的集合,对它们修改,解析,它们是二进制文件,借助ASM工具。

二.页面路由自动注册

对于一个URL,根据映射关系表,来打开特定页面的组件。

一个apk中可能会有多个映射表,因为组件化开发或者依赖某些子工程也会生成对应的映射表。运行期间注册到内存

目标是收集每个模块工程中build中生成的映射表类

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第1张图片

2.1新建Transform

添加依赖 ,插件版本

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第2张图片

新建一个groovy的类

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第3张图片

代码

package com.qfhqfh.plugin

import com.android.build.api.transform.Context
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.io.FileUtils
import org.gradle.api.Project

import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry

class RouterMappingTransform extends Transform {
    Project project

    RouterMappingTransform(Project project) {
        this.project = project
    }
    // Transform 的名称
    @Override
    String getName() {
        return "RouterMappingTransform"
    }
    //告知编译器,Transform需要消费的输入类型
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }
    //Transform需要收集的范围
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }
    //是否支持增量
    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs,
                   TransformOutputProvider outputProvider,
                   boolean isIncremental) throws IOException, TransformException, InterruptedException {
        super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
	
    }

接下来我们实现这个方法

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第4张图片

代码

 @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs,
                   TransformOutputProvider outputProvider,
                   boolean isIncremental) throws IOException, TransformException, InterruptedException {
        super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
//        project.logger.warn("register RouterMappingTransform")
//        RouterMappingCollector collector = new RouterMappingCollector()
		// 1. 遍历所有的Input
        // 2. 对Input进行二次处理
        // 3. 将Input拷贝到目标目录
        // 遍历所有的输入
        inputs.each { TransformInput input ->
            input.jarInputs.each { JarInput jarInput ->
//                println "jarInput.file.absolutePath:" + jarInput.file.absolutePath
                scanJar(jarInput.file)
                File src = jarInput.file
                File dest = getDestFile(jarInput, outputProvider)
//                collector.collectFromJarFile(jarInput.file)
                FileUtils.copyFile(src, dest)
            }
            // 把 文件夹 类型的输入,拷贝到目标目录
            input.directoryInputs.each { DirectoryInput directoryInput ->
                File dest = outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
//                collector.collect(directoryInput.file)

                FileUtils.copyDirectory(directoryInput.file, dest)
            }
        }
    }

拷贝目录,文件夹类型和jar类型目录

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第5张图片

打开我们的插件,注册Transform

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第6张图片

运行一下

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第7张图片

出现这个目录说明生效,说明被纳入了apk的编译过程。

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第8张图片

2.2收集类信息

模拟多个子工程的效果,选择Android library,新建module

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第9张图片

依赖这个模块

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第10张图片

settings下面也要添加依赖

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第11张图片

子工程添加相关插件

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第12张图片

添加注解处理器和注解子工程

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第13张图片

添加一个页面,模拟

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第14张图片

运行一下康康会不会创建出映射表类

在此之前我们先加一句代码,防止子工程也去执行下面的代码,只需要app模块执行即可。

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第15张图片

查看我们的工程,生效

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第16张图片

康康我们的app模块,也生效了

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第17张图片

2.3生成映射表

在我们的插件工程中新建一个类,收集类信息

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第18张图片

匹配到对应的类,

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第19张图片

匹配信息

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第20张图片

代码

package com.qfhqfh.plugin

import java.util.jar.JarEntry
import java.util.jar.JarFile

class RouterMappingCollector {
    private final Set<String> mappingClassNames = new HashSet<>()

    private static final String PACKAGE_NAME = 'com\\qfh\\common\\mapping'
    private static final String CLASS_NAME_PREFIX = 'RouterMapping'
    private static final String CLASS_NAME_SUFFIX = '.class'

    //获取收集好的映射表类名
    Set<String> getMappingClassName() {
        return mappingClassNames;
    }
    //收集class文件或者class文件目录中的映射表类。
    void collect(File classFile) {
        if (classFile == null || !classFile.exists()) return
        if (classFile.isFile()) {
            println "qfh classFile.path = ${classFile.path}"
            println "qfh  PACKAGE_NAME ${classFile.absolutePath.contains(PACKAGE_NAME)}"
            println "qfh  classFile.name ${classFile.name}"
            println "qfh  CLASS_NAME_PREFIX ${classFile.name.startsWith(CLASS_NAME_PREFIX)}"
            println "qfh  CLASS_NAME_SUFFIX ${classFile.name.endsWith(CLASS_NAME_SUFFIX)}"
            println "qfh  total ${classFile.absolutePath.contains(PACKAGE_NAME) && classFile.name.startsWith(CLASS_NAME_PREFIX) && classFile.name.endsWith(CLASS_NAME_SUFFIX)}"
            if (classFile.absolutePath.contains(PACKAGE_NAME)
                    && classFile.name.startsWith(CLASS_NAME_PREFIX)
                    && classFile.name.endsWith(CLASS_NAME_SUFFIX)) {
                String className =
                        classFile.name.replace(CLASS_NAME_SUFFIX, "")
                mappingClassNames.add(className)
            }
        } else {
            classFile.listFiles().each {
                collect(it)
            }
        }
    }
    //收集JAR包中的目标类
    void collectFromJarFile(File jarFile) {
        println "qfh jarFile = $jarFile"
        Enumeration enumeration = new JarFile(jarFile).entries()
        String PACKAGE_NAME_JAR_FILE = PACKAGE_NAME.replace("\\","/")
        while (enumeration.hasMoreElements()) {
            JarEntry jarEntry = (JarEntry) enumeration.nextElement()
            String entryName = jarEntry.getName()
            println "qfh entryName = $entryName"
            println "qfh PACKAGE_NAME_JAR_FILE = ${entryName.contains(PACKAGE_NAME_JAR_FILE)}"
            println "qfh CLASS_NAME_PREFIX = ${entryName.contains(CLASS_NAME_PREFIX)}"
            println "qfh CLASS_NAME_SUFFIX = ${entryName.contains(CLASS_NAME_SUFFIX)}"
            if (entryName.contains(PACKAGE_NAME_JAR_FILE)
                    && entryName.contains(CLASS_NAME_PREFIX)
                    && entryName.contains(CLASS_NAME_SUFFIX)) {
                String className = entryName
                        .replace(PACKAGE_NAME_JAR_FILE, "")
                        .replace("/", "")
                        .replace(CLASS_NAME_SUFFIX, "")
                println "qfhqfh className = $className"
                mappingClassNames.add(className)
            }
        }

    }
}

在transform中,采集我们的信息

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第21张图片

文件夹和jar包中的

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第22张图片

最后我们加一行日志观察是否收集到类的信息

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第23张图片

运行观看日志,确实收集到了我们app模块和子工程模块中的类的页面映射表的class信息。

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第24张图片

查看我们的app模块,子工程模块验证一下

子工程模块

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第25张图片

app模块

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第26张图片

三.汇总所有的映射表

3.1目标类的结构

演示一下,app下新建一个包

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第27张图片

把收集到的映射表类放到map中,我们的目标是把这个类的代码通过字节码插桩的方法插入到编译过程中,在transform。

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第28张图片

路由字节码创建者,新建一个类

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第29张图片

3.2安装引入ASM插件

asm插件,修改字节码

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第30张图片

插件使用

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第31张图片

生成对应的ASM接口

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第32张图片

拷贝到项目根目录下

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第33张图片

3.3生成字节码的逻辑

package com.qfhqfh.plugin

import org.objectweb.asm.ClassWriter
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes

class RouterMappingByteCodeBuilder implements Opcodes {
    public static final String CLASS_NAME =
            "com/qfh/router/mapping/generated/RouterMapping"

    static byte[] get(Set<String> names) {
        // 1. 创建一个类
        // 2. 创建构造方法
        // 3. 创建get方法
        //   (1)创建一个Map
        //   (2)塞入所有映射表的内容
        //   (3)返回map
        //        classWriter.visit(V11, ACC_PUBLIC | ACC_SUPER,
        //        "com/example/common/sample/RouterMapping", null, "java/lang/Object", null);
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS)
        writer.visit(V11, ACC_PUBLIC | ACC_SUPER, "com/example/common/sample/RouterMapping", null,
                "java/lang/Object", null)
        // 生成或者编辑方法
        MethodVisitor mv

        // 创建构造方法
        mv = writer.visitMethod(ACC_PUBLIC,
                "", "()V", null, null)
        mv.visitCode()
        mv.visitVarInsn(ALOAD, 0)
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object",
                "", "()V", false)
        mv.visitInsn(RETURN)
        mv.visitMaxs(1, 1)
        mv.visitEnd()

        // 创建get方法
        mv = writer.visitMethod(ACC_PUBLIC + ACC_STATIC,
                "get", "()Ljava/util/Map;",
                "()Ljava/util/Map;",
                null)
        mv.visitCode()
        mv.visitTypeInsn(NEW, "java/util/HashMap")
        mv.visitInsn(DUP)
        mv.visitMethodInsn(INVOKESPECIAL, "java/util/HashMap",
                "", "()V", false)
        mv.visitVarInsn(ASTORE, 0)

        // 向Map中,逐个塞入所有映射表的内容
        names.each {
            mv.visitVarInsn(ALOAD, 0)
            mv.visitMethodInsn(INVOKESTATIC, "com/example/common/sample/$it",
                    "get", "()Ljava/util/Map;",
                    false)
            mv.visitMethodInsn(INVOKEINTERFACE,
                    "java/util/Map", "putAll", "(Ljava/util/Map;)V"
                    , true)
        }
        mv.visitVarInsn(ALOAD, 0)
        mv.visitInsn(ARETURN)
        mv.visitMaxs(2, 2)
        mv.visitEnd()
        return writer.toByteArray()

    }
}

对外提供了一个 static byte[] get(Set names) 方法。

3.4transform中字节码写入本地文件

添加这些代码

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第34张图片

运行看我们的日志

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第35张图片

解压一下这个jar包康康里面的内容

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第36张图片

解压后的内容

Gradle开发(三),字节码插桩,编译期间自动注册收集页面路由信息的映射表类并汇总。_第37张图片

确认过后没问题

字节码自动收集映射表功能验证完毕,汇总映射表。

你可能感兴趣的:(Android进阶训练营,android,学习,架构)