字节码,字节码插桩。
监控方法耗时,代码插入,代码替换。
.java文件编译后变成.class文件,然后再被编译为.dex文件,最终打包形成apk文件。
在.class转化为.dex文件之前,在我们的插件中搞一个Transform,拿到.class的集合,对它们修改,解析,它们是二进制文件,借助ASM工具。
对于一个URL,根据映射关系表,来打开特定页面的组件。
一个apk中可能会有多个映射表,因为组件化开发或者依赖某些子工程也会生成对应的映射表。运行期间注册到内存
目标是收集每个模块工程中build中生成的映射表类
添加依赖 ,插件版本
新建一个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的编译过程。
模拟多个子工程的效果,选择Android library,新建module
依赖这个模块
settings下面也要添加依赖
子工程添加相关插件
添加注解处理器和注解子工程
添加一个页面,模拟
运行一下康康会不会创建出映射表类
在此之前我们先加一句代码,防止子工程也去执行下面的代码,只需要app模块执行即可。
查看我们的工程,生效
康康我们的app模块,也生效了
在我们的插件工程中新建一个类,收集类信息
匹配到对应的类,
匹配信息
代码
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模块
演示一下,app下新建一个包
把收集到的映射表类放到map中,我们的目标是把这个类的代码通过字节码插桩的方法插入到编译过程中,在transform。
路由字节码创建者,新建一个类
asm插件,修改字节码
插件使用
生成对应的ASM接口
拷贝到项目根目录下
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) 方法。
添加这些代码
运行看我们的日志
解压一下这个jar包康康里面的内容
解压后的内容
确认过后没问题
字节码自动收集映射表功能验证完毕,汇总映射表。