Transform 是AGP官方提供的接口,在 class->dex 的阶段提供一个时机,可以让我们对字节码文件做修改,或者动态添加一个新的类
添加依赖
因为是安卓官方提供的能力,需要在 buildSrc 的 build.gradle 中 添加 google() 仓库和安卓构建工具的依赖
1 2 3 4 5 6 7 8 9 10 11 12 13
| repositories { jcenter() google() mavenCentral()
}
dependencies { implementation gradleApi() implementation localGroovy() implementation "com.android.tools.build:gradle:3.5.3"
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| class RouterTransform extends Transform {
@Override String getName() { return "RouterMappingTransform" }
@Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS }
@Override Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT }
/** * 是否支持增量 * @return */ @Override boolean isIncremental() { return false }
/** * 实现 * @param transformInvocation * @throws TransformException * @throws InterruptedException * @throws IOException */ @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation)
println("transform start")
def collector = new RouterMappingCollector()
// 遍历所有的输入 transformInvocation.inputs.each { // 把 文件夹 类型的输入,拷贝到目标目录 it.directoryInputs.each { directoryInput -> def destDir = transformInvocation.outputProvider .getContentLocation( directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
collector.collect(directoryInput.file) FileUtils.copyDirectory(directoryInput.file, destDir) }
// 把 JAR 类型的输入,拷贝到目标目录 it.jarInputs.each { jarInput -> def dest = transformInvocation.outputProvider .getContentLocation( jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR) collector.collectFromJarFile(jarInput.file) println("transform jar input path ${jarInput.file.absolutePath}") println("transform jar output path ${dest.absolutePath}") FileUtils.copyFile(jarInput.file, dest) } }
File mappingJarFile = transformInvocation.outputProvider. getContentLocation( "router_mapping", getOutputTypes(), getScopes(), Format.JAR)
println("${getName()} mappingJarFile = $mappingJarFile")
if (mappingJarFile.getParentFile().exists()) { mappingJarFile.getParentFile().mkdirs() }
if (mappingJarFile.exists()) { mappingJarFile.delete() }
// 将生成的字节码,写入本地文件 FileOutputStream fos = new FileOutputStream(mappingJarFile) JarOutputStream jarOutputStream = new JarOutputStream(fos) // CLASS_NAME = "com/imooc/router/mapping/generated/RouterMapping" ZipEntry zipEntry = new ZipEntry(RouterMappingByteCodeBuilder.CLASS_NAME + ".class") jarOutputStream.putNextEntry(zipEntry) println("transform collect class ${collector.mappingClassName}") jarOutputStream.write( // 写入字节码 RouterMappingByteCodeBuilder.get(collector.mappingClassName) ) jarOutputStream.closeEntry() jarOutputStream.close() fos.close()
println("transform end") } }
|
transformInvocation.inputs.each {
表示遍历所有输入
it.directoryInputs.each { directoryInput ->
表示遍历所有目录文件输入,directoryInput 是class文件的目录
it.jarInputs.each { jarInput ->
遍历所有jar包输入, jarInput 是jar包
transformInvocation.outputProvider.getContentLocation(
获取具体的输入
collector.collect(directoryInput.file)
这里是一个自定义逻辑,表示从 class文件 或 jar包 中找到生成的 RouterMapping 路由映射类。
FileUtils.copyFile(jarInput.file, dest)
拷贝文件到目标目录, 最终transform执行之后会在 /…项目目录/app/build/intermediates/transforms 中生成一个RouterMappingTransform目录。 需要吧class文件和jar包都拷贝过去。有意思是,我原本以为我们工程自己的代码应该都是 class文件,都在 it.directoryInputs.each { directoryInput ->
这个输入里。 但是后来发现,只有 app工程是 class 文件,子工程打包成了 jar 包,和其他第三方库放在了一起
后面的代码都是动态生成了一个集合了所有子工程路由注册的路由类,使用asm写入了字节码
即使我们没有对文件做任何处理,我们仍然需要把输入文件拷贝到目标目录下,否则下一个Task就没有TansformInput,如果我们没有将input目录复制到output指定目录,会导致最后的打包的apk缺少class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class RouterPlugin2 implements Plugin<Project> {
String TAG = "RouterPlugin2"
@Override void apply(Project project) {
if (project.plugins.hasPlugin(AppPlugin)) { def extension = project.extensions.getByType(AppExtension) extension.registerTransform(new RouterTransform()) } } }
|
插件也是需要注册的。。。在resource目录的方式注册。 不写了,网上搜一下吧。
使用我们的 Router
使用反射实例化我们的类即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| try { val clazz = Class.forName(GENERATED_MAPPING) val method = clazz.getMethod("get") val allMapping = method.invoke(null) as Map<String, String>
if (allMapping?.size > 0) { Log.i(TAG, "init: get all mapping:") allMapping.onEach { Log.i(TAG, " ${it.key} -> ${it.value}") } mapping.putAll(allMapping) }
} catch (e: Throwable) { Log.e(TAG, "init: error while init router : $e") }
|