利用字节码插桩技术实现路由框架生成汇总映射表
实现 Transform
实现类的拷贝
-
buildSrc build.gradle 新增配置
// 声明仓库地址 repositories { jcenter() google() } // 声明依赖的包 dependencies { implementation gradleApi() implementation localGroovy() implementation 'com.android.tools.build:gradle:4.1.1' }
-
新建 RouterMappingTransform.groovy 类 实现类的拷贝逻辑
class RouterMappingTransform extends Transform { /** * 当前 Transform 名称 * @return */ @Override String getName() { return "RouterMappingTransform" } /** * 告知编译期,当前 Transform 需要消费的输入类型 * @return */ @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } /** * 告知编译期,当前 Transform 需要搜集的范围 * @return */ @Override Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } /** * 是否支持增量 * @return */ @Override boolean isIncremental() { return false } /** * 所有的 class 收集好之后,会被打包传入此方法 * @param transformInvocation * @throws TransformException * @throws InterruptedException * @throws IOException */ @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { // 1. 遍历所有的 Input // 2. 对 Input 进行二次处理 // 3. 将 Input 拷贝到目标目录 transformInvocation.inputs.each { // 把文件夹类型的输入,拷贝到目标目录 it.directoryInputs.each {directoryInput -> def destDir = transformInvocation.outputProvider.getContentLocation( directoryInput.name,directoryInput.contentTypes,directoryInput.scopes, Format.DIRECTORY ) FileUtils.copyDirectory(directoryInput.file,destDir) } // 把 jar 类型的输入,拷贝到目标目录 it.jarInputs.each {jarInput -> def jarDestDir = transformInvocation.outputProvider.getContentLocation( jarInput.name,jarInput.contentTypes,jarInput.scopes, Format.JAR ) FileUtils.copyFile(jarInput.file,jarDestDir) } } } }
-
在 RouterPlugin 中注入 Transform
class RouterPlugin implements Plugin<Project> { /** * 实现 apply 方法,注入插件的逻辑 * @param project */ @Override void apply(Project project) { // 注册 Transform if (project.plugins.hasPlugin(AppPlugin)) { AppExtension extension = project.extensions.getByType(AppExtension) Transform transform = new RouterMappingTransform() extension.registerTransform(transform) } } }
收集目标类
-
创建子工程 biz-reading
plugins { id 'com.android.library' id 'kotlin-android' id 'kotlin-android-extensions' id 'kotlin-kapt' id 'com.imooc.router' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'com.google.android.material:material:1.1.0' testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' // 引入自定义注解 implementation project(':router-annotations') kapt project(':router-processor') }
-
新建 ReadingActivity
@Destination( url = "router://reading", description = "阅读" ) class ReadingActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_reading) } }
-
主工程 build.gradle 引入 biz-reading
implementation project(':biz-reading')
-
buildSrc 工程新建 RouterMappingCollector 完成映射表类名的收集
class RouterMappingCollect { private static final String PACKAGE_NAME = "com/imooc/router/mapping" private static final String CLASS_NAME_PREFIX = "RouterMapping_" private static final String CLASS_NAME_SUFFIX = ".class" private final Set<String> mappingClassNames = new HashSet<>() /** * 获取收集好的映射表类名 * @return */ Set<String> getMappingClassName() { return mappingClassNames } /** * 收集 class 文件或者 class 文件目录中的映射表类 * @param classFile */ void collect(File classFile){ if (classFile == null || !classFile.exists()) return if (classFile.isFile()){ 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,"") mappingClassName.add(className) } }else{ classFile.listFiles().each { collect(it) } } } /** * 收集 jar 包中的目标类 * @param classFile */ void collectFormJarFile(File classFile){ java.util.Enumeration enumeration = new JarFile(classFile).entries() while (enumeration.hasMoreElements()){ JarEntry jarEntry = enumeration.nextElement() String entryName = jarEntry.name if (entryName.contains(PACKAGE_NAME) && entryName.contains(CLASS_NAME_PREFIX) && entryName.contains(CLASS_NAME_SUFFIX)){ String className = entryName .replace(PACKAGE_NAME,"") .replace("/","") .replace(CLASS_NAME_SUFFIX,"") mappingClassName.add(className) } } } }
-
在 RouterMappingTransform 中调用前面定义的 RouterMappingCollect
/** * 所有的 class 收集好之后,会被打包传入此方法 * @param transformInvocation * @throws TransformException * @throws InterruptedException * @throws IOException */ @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { // 1. 遍历所有的 Input // 2. 对 Input 进行二次处理 // 3. 将 Input 拷贝到目标目录 RouterMappingCollect collect = new RouterMappingCollect() transformInvocation.inputs.each { // 把文件夹类型的输入,拷贝到目标目录 it.directoryInputs.each {directoryInput -> def destDir = transformInvocation.outputProvider.getContentLocation( directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY ) collect.collect(directoryInput.file) FileUtils.copyDirectory(directoryInput.file,destDir) } // 把 jar 类型的输入,拷贝到目标目录 it.jarInputs.each {jarInput -> def jarDestDir = transformInvocation.outputProvider.getContentLocation( jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR ) collect.collectFormJarFile(jarInput.file) FileUtils.copyFile(jarInput.file,jarDestDir) } } }
生成汇总映射表
-
编写生成字节码的逻辑
class RouterMappingByteCodeBuilder implements Opcodes { public static final String CLASS_NAME = "com/imooc/router/mapping/generated/RouterMapping" static byte[] get(Set<String> allMappingNames) { // 1. 创建一个类 // 2. 创建构造方法 // 3. 创建get 方法 // i. 创建Map // ii. 添加所有映射表的内容 // iii. 返回Map ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS) // 实例化一个类 cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, CLASS_NAME, null, "java/lang/Object", null) // 创建构造方法 MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null) mv.visitCode() mv.visitVarInsn(Opcodes.ALOAD,0) mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false) mv.visitInsn(Opcodes.RETURN) mv.visitMaxs(1,1) mv.visitEnd() // 创建 get 方法 cw.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) // 存储创建的 HashMap 实例 // 向 Map 中 逐个塞入所有映射表的内容 allMappingNames.each { mv.visitVarInsn(ALOAD,0) mv.visitMethodInsn(INVOKESTATIC, "com/imooc/router/mapping/$it", "get", "()Ljava/util/Map;", false) mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "putAll", "(Ljava/util/Map;)V", true) } // 返回 Map mv.visitVarInsn(ALOAD,0) mv.visitInsn(ARETURN) mv.visitMaxs(2,2) mv.visitEnd() return cw.toByteArray() } }
-
将生成的字节码,写入到本地文件
RouterMappingTransform
if (mappingJarFile.getParentFile().exists()) { mappingJarFile.getParentFile().mkdirs() } if(mappingJarFile.exists()){ mappingJarFile.delete() } // 将生成的字节码,写入本地文件 java.io.FileOutputStream fos = new FileOutputStream(mappingJarFile) JarOutputStream jarOutputStream = new JarOutputStream(fos) ZipEntry zipEntry = new ZipEntry(RouterMappingByteCodeBuilder.CLASS_NAME + ".class") jarOutputStream.putNextEntry(zipEntry) jarOutputStream.write( RouterMappingByteCodeBuilder.get(collect.mappingClassName) ) jarOutputStream.closeEntry() jarOutputStream.close() fos.close()
-
执行构建编译,查看生成的字节码文件
gradle :app:assembleDebug