最近一直在做agp8.0+的插件适配,涉及到不少知识点,踩到不少坑,特此记录下
gradle和gradle-api的区别
我们在插件开发的时候 如果你看官方的demo 你会发现他们现在都是给你gradle-api 这个依赖,但实际开发的时候我们会发现还会直接用gradle 依赖比较好
简单来说gradle-api 是官方给你的一个简易依赖,对外暴露的api更少,但是更加稳定,agp本身的api变化 会在这个gradle-api依赖中抹平(是不是有一点像booster做的事?)
但是这个gradle-api 对外暴露的api太少了,相信我 至少短期内你不会想直接用gradle-api依赖的
建议插件开发时 直接使用gradle的依赖
任务的输入和输出
查询agp中某个task的具体实现
./gradlew help --task <taskName>
例如:
./gradlew help --task dexBuilderDefaultNewSignDebug
这样可以方便的让我们找到task对应的代码实现位置在哪里 方便定位问题
任务可以没有输入 但是不能没有输出
输入类型和输出类型 不是11对应的
仅支持输入,不支持输出的类型
- String或者任何实现了JDK中 序列化接口Ser的类
- Java的ClassPath
仅支持输出 不支持输入的类型
- Map<String,File>
- Iterable
一些重要且特殊的注解说明
- @input 一般string类型或者序列化的java类就用这个,很多人会把这个和internal作用搞混
- @internal 既不是输入属性也不是输出属性, 也不影响updateToWhen的判定
- @PathSensitive 搭配一些input注解使用 告诉gradle 的 输入文件 路径变化的敏感等级用的,一共是4个级别 (经常用的就这2个,还有2个就不写了)absoulte 任何路径更改 都会触发update,none 忽略所有路径变更 只考虑内容
- @optional 这个就是和部分注解搭配使用时,跳过文件检测,不传也可以正确执行task
- @Incremental 搭配 inputfiles和inputDir 使用,可以通过filechanges 来获取文件变化的细节
任务的状态
- executed 执行
- up-to-date 没有执行 一般是输入输出没改变,大家增量构建时 很多任务都是这个状态 剩下的 from-cache skipped no-source 也是代表任务不执行的状态,只是原因不同
FileCollection和FileTree的区别
更多细节参见这里 简单来说就是FileCollection没有保存文件的树形结构,而FileTree保存了
Artifacts API
这个api 在agp8.0以后 会变得非常重要,在8.0之前 我们做agp插件开发的时候 通常的逻辑是是 先找到agp默认的2个任务 比如 a和c
然后在ac之间插入你自定义的任务b,具体插入方式为 c.dependsOn(b) b.dependsOn(a)
然而这套机制在agp8.0之后 完全失效了, 你在onVarint回调里 是没办法拿到agp的默认任务的 这样就是导致了 你无法插入任务到agp中
val androidComponents = target.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants {
}
目前还不知道怎么绕过这套机制,如果有知道的可以评论区留言。
谷歌在8.0以后推荐的方式 是希望你利用Artifacts API 来完成你的任务编排
8.0之后 如何修改manifest文件?
首先还是定义一下我们的task
abstract class DeleteSmsPermissionForNewSignAppTask : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val mergedManifest:RegularFileProperty
@get:OutputFile
abstract val updatedManifest: RegularFileProperty
@TaskAction
fun taskAction() {
val file = mergedManifest.get().asFile
val document = SAXReader().read(file)
// 随便更改你的xml 就可以了
OutputFormat.createPrettyPrint().apply {
encoding = "UTF-8"
XMLWriter(OutputStreamWriter(FileOutputStream(updatedManifest.get().asFile)), this).apply {
write(document)
flush()
close()
}
}
}
}
最关键的就是如何构建你的task关系,在agp8.0之前我们需要找到mergeManifest 这个task 然后插入 agp8.0之后 会简单很多
variant.artifacts.use(modifyTask)
.wiredWithFiles(
DeleteSmsPermissionForNewSignAppTask::mergedManifest,
DeleteSmsPermissionForNewSignAppTask::updatedManifest
).toTransform(SingleArtifact.MERGED_MANIFEST)
SingleArtifact
可以看下 有多少种单个的Arifact 可以供我们使用
- AAR 可获取AAR
- APK 可获取APK
- Bundle 可获取AAB
- MERGED_MANIFEST 可获取合并后的manifest文件
其他类型 大家有兴趣看看注释即可
其他类型的Artifact
这个我们一般不会用到它,大家知道有这么个东西即可
下面这个就不一样了
这个ScopedArtifact 在 8.0以后的字节码 插件中会扮演重要地位
这个我们后面再说,大家只要谨记,agp8.0以后 你要想插入任务到agp之中 暂时只能支持这几种了。 确实很不方便,比如8.0之前 你要做图片压缩的时候 你插入一个任务到mergeRes之后就可以了
但是现在没有Resources 这个 artifact 这就导致 我们无法插入任务。
aritfact 处理的几种形式
varint.artifacts.use(xxxTask).wiredWithFiles/wireDirectories()/wiredWith()
.toTransform/toCreate/toAppendTo
具体你最终用到的artifact 支持哪种 wired 取决于 源码中实现的接口
遗留问题
artifact 目前还没支持 mergeResources 这个任务, 这会导致 在8.0+中 你如果想做编译期对图片资源压缩之类的任务就没办法做了
有知道的可以评论区留言说下方案
8.0+中的 字节码处理
asm
整体来说 asm的字节码修改 代码写起来 比 8.0之前要简单方便很多,只有当你处理类似像arouter这样的代码的时候会麻烦一些(需要先全盘扫描再做处理),路由插件的asm 8.0 适配如何做 可以看我之前的文章即可。
这里介绍下普通字节码修改的的写法
variant.instrumentation.transformClassesWith(
OkHttpClassVisitorFactory::class.java,
InstrumentationScope.ALL
) {}
abstract class OkHttpClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {
override fun createClassVisitor(classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor {
if (classContext.currentClassData.className != "okhttp3.OkHttpClient") {
return nextClassVisitor
}
return object : ClassNode(ASM7) {
override fun visitEnd() {
super.visitEnd()
methods?.find {
it.name == "<init>" && it.desc != "()V"
}.let {
it?.instructions
?.iterator()
?.asIterable()
?.filterIsInstance(FieldInsnNode::class.java)
?.filter { fieldInsnNode ->
fieldInsnNode.opcode == PUTFIELD &&
fieldInsnNode.owner == "okhttp3/OkHttpClient" &&
fieldInsnNode.name == "networkInterceptors" &&
fieldInsnNode.desc == "Ljava/util/List;"
}?.forEach { fieldInsnNode ->
it.instructions.insert(fieldInsnNode, createOkHttpClientInsnList())
}
}
accept(nextClassVisitor)
}
}
}
override fun isInstrumentable(classData: ClassData): Boolean {
return true
}
private fun createOkHttpClientInsnList(): InsnList {
return with(InsnList()) {
// 插入application 拦截器
add(VarInsnNode(ALOAD, 0))
add(
MethodInsnNode(
INVOKESTATIC,
"com/xxx/xxxx/hook/OkHttpHook",
"addInterceptor",
"(Lokhttp3/OkHttpClient;)V",
false,
),
)
this
}
}
fun <T> Iterator<T>.asIterable(): Iterable<T> = Iterable { this }
}
javaassist
js修改字节码会复杂一些,可以参考下面的写法
这个例子稍微复杂一点 其实主要也是想办法把android.jar 加入到js的环境中,否则部分字节码修改会失败
val taskProvider = target.project.tasks.register<ModifyGlideClassesTask>("${variant.name}ModifyGlideClasses") {
// 必须把android.jar 加入到 ClassPool 中
bootClasspath = androidComponents.sdkComponents.bootClasspath
}
variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
.use(taskProvider)
.toTransform(
ScopedArtifact.CLASSES,
ModifyGlideClassesTask::allJars,
ModifyGlideClassesTask::allDirectories,
ModifyGlideClassesTask::output
)
/**
* 修改glide的 gifdrawable了
* gifdrawable 对draw方法进行try catch
*/
abstract class ModifyGlideClassesTask : DefaultTask() {
@get:Internal
abstract var bootClasspath: Provider<List<RegularFile>>
@get:InputFiles
abstract val allJars: ListProperty<RegularFile>
@get:InputFiles
abstract val allDirectories: ListProperty<Directory>
@get:OutputFile
abstract val output: RegularFileProperty
@Internal
val jarPaths = mutableSetOf<String>()
companion object {
const val GLIDE_HOOK_SWITCH = "image.helper.glide.gifdrawble.HookSwitch"
//GifDrawable的全名 包含他所属的包名
const val GLIDE_GIF_CLASS_NAME = "com.bumptech.glide.load.resource.gif.GifDrawable"
//要修改的是GifDrawable的 draw方法
const val METHOD_NAME_DRAW = "draw"
//新方法命名为tryDraw
const val NEW_METHOD_NAME_TRY_DRAW = "tryDraw"
}
@TaskAction
fun taskAction() {
val pool = ClassPool(ClassPool.getDefault())
// (allJars.get() + allDirectories.get()).map { it.asFile }.forEach {
// pool.appendClassPath(it.canonicalPath)
// }
// 不要遗漏添加android.jar 到ClassPool中
bootClasspath.get().map(RegularFile::getAsFile).forEach {
pool.appendClassPath(it.canonicalPath)
}
val jarOutput = JarOutputStream(
BufferedOutputStream(
FileOutputStream(
output.get().asFile
)
)
)
// copy classes from jar files without modification
allJars.get().forEach outer@{ file ->
val jarFile = JarFile(file.asFile)
jarFile.entries().iterator().forEach { jarEntry ->
if (jarEntry.name == "com/bumptech/glide/load/resource/gif/GifDrawable.class") {
val jarInputStream = jarFile.getInputStream(jarEntry)
val klass = pool.makeClass(jarInputStream)
// 能走到这 就证明找到 GifDrawable这个class了 ,此时我们直接取获取draw方法
val drawMethod: CtMethod = klass.getDeclaredMethod(METHOD_NAME_DRAW)
println("HelpThirdClassTransform :找到draw方法了")
//复制一个名为try-draw的新方法
val newMethod: CtMethod = CtNewMethod.copy(drawMethod, NEW_METHOD_NAME_TRY_DRAW, klass, null)
klass.addMethod(newMethod)
//这里就将原来的draw方法的内容 替换成 调用我们的tryDraw方法 并在调用tryDraw的地方 加上try catch代码块
val sb = StringBuffer()
sb.append("{try{")
sb.append(NEW_METHOD_NAME_TRY_DRAW)
//这里要传递canvas这个参数 注意javaassist的写法
sb.append("($1)")
sb.append(";}catch(Exception e){ android.util.Log.e("GifDrawable", "draw", e);}")
sb.append("}")
//改写我们的draw方法
drawMethod.setBody(sb.toString())
jarInputStream.close()
jarOutput.writeEntity(jarEntry.name, klass.toBytecode())
return@forEach
}
jarOutput.writeEntity(jarEntry.name, jarFile.getInputStream(jarEntry))
}
jarFile.close()
}
// Iterating through class files from directories
// Looking for SomeSource.class to add generated interface and instrument with additional output in
// toString methods (in our case it's just System.out)
allDirectories.get().forEach { directory ->
directory.asFile.walk().forEach { file ->
if (file.isFile) {
// if class is not SomeSource.class - just copy it to output without modification
val relativePath = directory.asFile.toURI().relativize(file.toURI()).getPath()
jarOutput.writeEntity(relativePath.replace(File.separatorChar, '/'), file.inputStream())
}
}
}
jarOutput.close()
}
// writeEntity methods check if the file has name that already exists in output jar
private fun JarOutputStream.writeEntity(name: String, inputStream: InputStream) {
// check for duplication name first
if (jarPaths.contains(name)) {
printDuplicatedMessage(name)
} else {
putNextEntry(JarEntry(name))
inputStream.copyTo(this)
closeEntry()
jarPaths.add(name)
}
}
private fun JarOutputStream.writeEntity(relativePath: String, byteArray: ByteArray) {
// check for duplication name first
if (jarPaths.contains(relativePath)) {
printDuplicatedMessage(relativePath)
} else {
putNextEntry(JarEntry(relativePath))
write(byteArray)
closeEntry()
jarPaths.add(relativePath)
}
}
private fun printDuplicatedMessage(name: String) =
println("Cannot add ${name}, because output Jar already has file with the same name.")
}