android app的构建是使用gradle 工具,它提供给了开发者自定义编译期行为的能力。一般情况下,我们在transform阶段进行字节码的修改,插入,删除等操作。通过字节码处理,我们可以完成很多cool的事情,比如根据编译时注解,完成一些特定的操作等。
实现修改字节码的工具有:
javassist (如库 ‘org.javassist:javassist:3.27.0-GA’)
ASM ( 如库’com.android.tools.build:gradle:3.6.4’)
插件开发的一般步骤
- 继承Plugin
class TestPlugin implements Plugin<Project> { @Override void apply(Project project) { System.out.println("========================"); System.out.println("hello TestPlugin") System.out.println("========================") project.extensions.findByType(AppExtension.class).registerTransform(new MyTransform(project));
- 自定义Transfrom
class MyTransform extends Transform { private Project project MyTransform(Project project) { this.project = project } @Override String getName() { return "MyTransform" } @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 { // do something when transform...下面只是一个例子 transformInvocation.inputs.each { input -> // 包含我们手写的 Class 类及 R.class、BuildConfig.class 等 input.directoryInputs.each { directoryInput -> String path = directoryInput.file.absolutePath println("[MyTransform] Begin to inject: $path") // 执行注入逻辑 ========== // inject code ... InjectByJavassit.inject(path, project) // 获取输出目录 def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) println("[MyTransform] Directory output dest: $dest.absolutePath") // 将input的目录复制到output指定目录 FileUtils.copyDirectory(directoryInput.file, dest) } // jar文件,如第三方依赖 input.jarInputs.each { jarInput -> def dest = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR) // FileUtils.copyFile(jarInput.file, dest) } } } }
修改字节码的时候,我们可以使用javassist或者ASM。
- 注册transform
project.extensions.findByType(AppExtension.class).registerTransform(new MyTransform(project));
在buildSrc中,开发并发布插件
插件的功能是,在Activity的onCreate中,编译时统一加上弹toast的功能。
- build.gradle
apply plugin: 'groovy' apply plugin: 'maven-publish' println "debug, buildSrc ..." repositories { google() jcenter() mavenCentral() } allprojects { repositories { google() jcenter() mavenCentral() } } dependencies { implementation gradleApi() implementation localGroovy() implementation 'com.android.tools.build:gradle:3.5.0' implementation 'org.javassist:javassist:3.27.0-GA' }
- 在META-INF.gradle-plugins下新建文件com.test.testplugin.properties
内容为implementation-class=com.test.testplugin.TestPlugin - 在src/main底下,建groovy目录,并建com.test.testplugin包
新建以下类 - TestPlugin.groovy
// package com.test.testplugin import org.gradle.api.Plugin import org.gradle.api.Project import com.android.build.gradle.AppExtension class TestPlugin implements Plugin<Project> { @Override void apply(Project project) { System.out.println("========================"); System.out.println("hello TestPlugin") System.out.println("========================") project.extensions.findByType(AppExtension.class).registerTransform(new MyTransform(project)) project.extensions.create("myExtension", MyExtension) project.task("myExtensionTask").doLast { System.out.println("in myExtensionTask " + project["myExtension"].name + ": " + project["myExtension"].version) } } }
- MyTransform.groovy
package com.test.testplugin import com.android.build.api.transform.* import com.android.build.gradle.internal.pipeline.TransformManager import org.apache.commons.io.FileUtils import org.gradle.api.Project class MyTransform extends Transform { private Project project MyTransform(Project project) { this.project = project } @Override String getName() { return "MyTransform" } @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 { transformInvocation.inputs.each { input -> // 包含我们手写的 Class 类及 R.class、BuildConfig.class 等 input.directoryInputs.each { directoryInput -> String path = directoryInput.file.absolutePath println("[MyTransform] Begin to inject: $path") // 执行注入逻辑 ========== // inject code ... InjectByJavassit.inject(path, project) // 获取输出目录 def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) println("[MyTransform] Directory output dest: $dest.absolutePath") // 将input的目录复制到output指定目录 FileUtils.copyDirectory(directoryInput.file, dest) } // jar文件,如第三方依赖 input.jarInputs.each { jarInput -> def dest = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR) // FileUtils.copyFile(jarInput.file, dest) } } } }
- MyExtension.groovy
package com.test.testplugin class MyExtension { String name = null String version = null }
- InjectByJavassit.groovy
package com.test.testplugin import javassist.ClassPool import javassist.CtClass import javassist.CtMethod import org.gradle.api.Project class InjectByJavassit { static void inject(String path, Project project) { try { File dir = new File(path) if (dir.isDirectory()) { dir.eachFileRecurse { File file -> if (file.name.endsWith('Activity.class')) { doInject(project, file, path) } } } } catch (Exception e) { e.printStackTrace() } } private static void doInjectKai(Project project, File clsFile, String originPath) { println("[Inject] DoInject: $clsFile.absolutePath") String cls = new File(originPath).relativePath(clsFile).replace('/', '.') cls = cls.substring(0, cls.lastIndexOf('.class')) println("[Inject] Cls: $cls") ClassPool pool = ClassPool.getDefault() CtClass ctClass = pool.getCtClass(cls) // 解冻 if (ctClass.isFrozen()) { ctClass.defrost() } // 获取方法 CtMethod ctMethod = ctClass.getDeclaredMethod('splitString') String addLog = 'android.util.Log.e("kaidebug", "This is add by injecting");' ctMethod.insertAfter(addLog) ctClass.writeFile(originPath) ctClass.detach() } private static void doInject(Project project, File clsFile, String originPath) { println("[Inject] DoInject: $clsFile.absolutePath") String cls = new File(originPath).relativePath(clsFile).replace('/', '.') cls = cls.substring(0, cls.lastIndexOf('.class')) println("[Inject] Cls: $cls") ClassPool pool = ClassPool.getDefault() // 加入当前路径 pool.appendClassPath(originPath) // project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类 pool.appendClassPath(project.android.bootClasspath[0].toString()) // 引入android.os.Bundle包,因为onCreate方法参数有Bundle pool.importPackage('android.os.Bundle') CtClass ctClass = pool.getCtClass(cls) // 解冻 if (ctClass.isFrozen()) { ctClass.defrost() } // 获取方法 CtMethod ctMethod = ctClass.getDeclaredMethod('onCreate') String toastStr = 'android.widget.Toast.makeText(this, "This is ' + cls + '", android.widget.Toast.LENGTH_SHORT).show();' // 方法尾插入 ctMethod.insertAfter(toastStr) ctClass.writeFile(originPath) // 释放 ctClass.detach() } }
App 模块中使用插件
在app module中的build.gradle中,使用上面开发的插件
...... apply plugin: 'com.test.testplugin' println "kaidebug, app build.gradle" // 这里为插件传入控制数据,方便插件使用者向插件注入控制量,是灵活性的一种体现 myExtension { name "zhuyunkai" version "1.2.3" } ...