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

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

一.字节码插桩是什么

字节码,字节码插桩。

1.2 场景

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

1.3 技术原理

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

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

二.页面路由自动注册

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

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

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

在这里插入图片描述

2.1新建Transform

添加依赖 ,插件版本

在这里插入图片描述

新建一个groovy的类

在这里插入图片描述

代码

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)
	
    }

接下来我们实现这个方法

在这里插入图片描述

代码

 @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类型目录

在这里插入图片描述

打开我们的插件,注册Transform

在这里插入图片描述

运行一下

在这里插入图片描述

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

在这里插入图片描述

2.2收集类信息

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

在这里插入图片描述

依赖这个模块

在这里插入图片描述

settings下面也要添加依赖

在这里插入图片描述

子工程添加相关插件

在这里插入图片描述

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

在这里插入图片描述

添加一个页面,模拟

在这里插入图片描述

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

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

在这里插入图片描述

查看我们的工程,生效

在这里插入图片描述

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

在这里插入图片描述

2.3生成映射表

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

在这里插入图片描述

匹配到对应的类,

在这里插入图片描述

匹配信息

在这里插入图片描述

代码

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中,采集我们的信息

在这里插入图片描述

文件夹和jar包中的

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

子工程模块

在这里插入图片描述

app模块

在这里插入图片描述

三.汇总所有的映射表

3.1目标类的结构

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

3.2安装引入ASM插件

asm插件,修改字节码

在这里插入图片描述

插件使用

在这里插入图片描述

生成对应的ASM接口

在这里插入图片描述

拷贝到项目根目录下

在这里插入图片描述

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,
                "<init>", "()V", null, null)
        mv.visitCode()
        mv.visitVarInsn(ALOAD, 0)
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object",
                "<init>", "()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<Ljava/lang/String;Ljava/lang/String;>;",
                null)
        mv.visitCode()
        mv.visitTypeInsn(NEW, "java/util/HashMap")
        mv.visitInsn(DUP)
        mv.visitMethodInsn(INVOKESPECIAL, "java/util/HashMap",
                "<init>", "()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中字节码写入本地文件

添加这些代码

在这里插入图片描述

运行看我们的日志

在这里插入图片描述

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

在这里插入图片描述

解压后的内容

在这里插入图片描述

确认过后没问题

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

  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值