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"
}
...