通过 Android 的打包流程可知,Android 除了将 Java 源文件编译成 class 文件之外,还需要将 class 文件转换为 dex 文件,而 Gradle 就为这个过程提供了一个注入节点,允许在转换为 dex 前修改编译生成的 class 文件,因此 Transform 针对的是编译后的产物 。
Gradle 编译的执行单元是 Plugin,因此 Transform 的执行也需要借助 Plugin;将 Transform 注册到 Plugin 后,当 Plugin 被执行时就会根据注册的顺序执行 Transform 任务。
(1)创建一个 Transform 类并继承自 Gradle Transform:
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 class DemoTransform extends Transform { @Override String getName() { return "DemoTransform" } @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } @Override Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } @Override boolean isIncremental() { return false } @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { } }
(2)创建一个 Plugin 并注册 Transform 任务(Plugin 的注册方式可参考上篇文章):
1 2 3 4 5 6 7 8 class DemoPlugin implements Plugin <Project > { @Override void apply(Project project) { DemoTransform demoTransform = new DemoTransform() project.android.registerTransform(demoTransform) } }
Transform 的工作流程简单来说就是以下几步:
(1)通过包名筛选符合条件的 Class 文件,其中 Class 有两种可能的文件来源:
(2)通过读取 Class 文件包含的类信息(例如接口、注解等)进一步筛选符合条件的 Class 文件;
(3)对最终符合条件的 Class 做处理(修改字节码、插桩等);
(4)将产物拷贝至 Transform 的输出目录,作为下一个 Transform 的输入;
2.2.1 过滤Class包名 Gradle 处理的 Class 文件有两种可能的来源:
Java 文件源码编译生成的 ClassDir 下的 Class 文件;
Jar、AAR 依赖包中已经集成的 Class 文件;
TransformInput
提供了分别读取两种来源的 Class 的接口。此外上文提到 Transform 可以指定是否开启增量模式,增量模式可以通过忽略未发生变更的文件来优化编译速度。因此可以在 JarInput
和 DirectoryInput
两个接口中先分别判断一下文件状态:
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 @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super .transform(transformInvocation) if (!transformInvocation.isIncremental()) { transformInvocation.outputProvider.deleteAll() } transformInvocation.inputs.each { -> it.jarInputs.each { JarInput eachJarInput -> Status jarClassStatus = eachJarInput.getStatus(); ...... } it.directoryInputs.each { DirectoryInput eachDirectoryInput -> Map<File, Status> classStatusMap = eachDirectoryInput.getChangedFiles(); } } } public enum Status { NOTCHANGED, ADDED, CHANGED, REMOVED; }
2.2.2 过滤Class类信息 由于 Transform 是以文件形式接收 Class 输入,因此第一层过滤只能通过文件路径反推包名来大致过滤包名符合条件的 Class 文件,但要确定一个 Class 是否为 Transform 的处理目标,还需要读取 Class 文件中包含的类信息,例如接口、注解等,此时需要借助 ClassWriter、ClassReader 等 ASM 工具。
2.2.3 读写Class字节码 借助 ASM 工具可以读写文件形式的 Class。
由于 Transform 的输入和输出是「流式」的,每个 Transform 的输出即作为下一个 Transform 的输入,所以为了让下一个 Transform 仍有机会处理所有的类(而不论当前 Transform 是否已经处理过),通常会将每个原始输入(包含 Jar 或 ClassDir)都拷贝至输出目录下。
当然,Transform 也可以根据是否增/删/改了某个 Class 的情况自由调整拷贝的内容,只需切记 Transform 的输出将作为下一个 Transform 的输入 即可。
3.1 从Jar文件中筛选Class 筛选 Jar 文件实际上就是将 Jar 解包后读取其中的 Class。
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 @Override void transform(TransformInvocation transformInvocation) { super .transform(transformInvocation) transformInvocation.inputs.each { -> it.jarInputs.each { JarInput eachJarInput -> File srcJarFile = eachJarInput.file File targetJarFile = getTargetJarFile(transformInvocation, eachJarInput) if (isJarInvalid(eachJarInput)) { continue ; } enumerateJarEntryFromJarFile(srcJarFile) FileUtils.copyFile(jarSrcFile, jarTargetFile) } it.directoryInputs.each { DirectoryInput eachDirectoryInput -> ...... } } } private static File getTargetJarFile(TransformInvocation transformInvocation, JarInput jarInput) { String srcJarFileName = jarInput.name if (srcJarFileName.toLowerCase().endsWith(".jar" )) { srcJarFileName = srcJarFileName.substring(0 , srcJarFileName.length() - 4 ) } String srcJarFileMd5 = DigestUtils.md5Hex(jarInput.file.absolutePath) String targetJarFileName = srcJarFileName + '_' + srcJarFileMd5 return transformInvocation .outputProvider .getContentLocation(targetJarFileName, jarInput.contentTypes, jarInput.scopes, Format.JAR) } private static boolean isJarInvalid(JarInput jarInput) { return (pathName.contains("com.android.support" ) || pathName.contains("/android/m2repository" ) || pathName.contains("androidx." )) } private static void enumerateJarEntryFromJarFile(File srcJarFile) { def jarFile = new JarFile(jarSrcFile) Enumeration allElementsInJar = jarFile.entries() while (allElementsInJar != null && allElementsInJar.hasMoreElements()) { JarEntry eachJarEntry = (JarEntry) allElementsInJar.nextElement() if (eachJarEntry.getName().startsWith("priv/demo" ) && eachJarEntry.getName().toLowerCase().endsWith(".class" )) { } } jarFile.close() }
3.2 从编译目录中筛选Class 筛选 Class 目录实际上就是遍历目录中包含的每个 Class 文件:
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 @Override void transform(TransformInvocation transformInvocation) { super .transform(transformInvocation) transformInvocation.inputs.each { -> it.jarInputs.each { JarInput eachJarInput -> ...... } it.directoryInputs.each { DirectoryInput eachDirectoryInput -> File srcClassDir = directoryInput.file File targetClassDir = getTargetClassDirectoryFile(transformInvocation, eachDirectoryInput) enumerateClassFromClassDir(srcClassDir) FileUtils.copyDirectory(srcClassDir, targetClassDir) } } } private static File getTargetClassDirectoryFile(TransformInvocation transformInvocation, DirectoryInput directoryInput) { return transformInvocation .outputProvider .getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) } private static void enumerateClassFromClassDir(File srcClassDir) { srcClassDir.eachFileRecurse { File eachClassFile -> if (!eachClassFile.isFile()) { continue ; } String eachClassShortPath = getClassShortPathFromFile(srcClassDir, eachClassFile) if (eachClassShortPath.startsWith("priv/demo" ) && eachClassShortPath.endsWith(".class" )) { } } } private static String getClassShortPathFromFile(File srcClassDir, File srcClassFile) { String srcClassDirPath = srcClassDir.absolutePath if (!srcClassDirPath.endsWith(File.separator)) { srcClassDirPath += File.separator } String classShortPath = srcClassFile.absolutePath.replace(srcClassDirPath, '' ) if (File.separator != '/' ) { classShortPath = classShortPath.replace("\\\\" , "/" ) } return classShortPath; }
3.3 判断Class文件的类信息 上文在筛选 Jar 及 ClassDir 时,实际上都是通过 enumerateXXX
读取了文件/目录内的每个元素,但:
Jar 文件遍历的目标是 JarEntry
;
ClassDir 遍历的目标是 Class 对应的 File
;
本质上它们都是文件类型,而仅仅判断包名通常并不足以满足条件,有时还需要读取文件获取与 Class 相关的信息(例如接口、注解等)进一步判断,此时可以通过 ClassVisitor 读取:
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 class ClassScannerClassVisitor extends ClassVisitor { static scanFromFileInputStream(InputStream classFileInputStream) { ClassReader classReader = new ClassReader(classFileInputStream) ClassWriter classWriter = new ClassWriter(classReader, 0 ) ClassScannerClassVisitor scannerClassVisitor = new ClassScannerClassVisitor(Opcodes.ASM5, classWriter) classReader.accept(scannerClassVisitor, ClassReader.EXPAND_FRAMES) } ClassScannerClassVisitor(int api, ClassVisitor classVisitor) { super (api, classVisitor) } @Override void visit(int version, int access, String className, String signature, String superName, String[] interfaces) { super .visit(version, access, className, signature, superName, interfaces) interfaces.each { interfaceName -> } } } private static void scanClassFromJarEntry(File srcJarFile, JarEntry jarEntry) { InputStream srcJarFileInputStream = srcJarFile.getInputStream(jarEntry) ClassScannerClassVisitor.scanFromFileInputStream(srcJarFileInputStream) srcJarFileInputStream.close() } private static void scanClassFromClassFile(File classFile) { FileInputStream classFileInputStream = new FileInputStream(classFile) ClassScannerClassVisitor.scanFromFileInputStream(classFileInputStream) classFileInputStream.close() }
参考文献