前言
Android Transform Api 是 Android Gradle Plugin 中提供的一个hook api,该api提供了在 class 转换为 dex 前的加工处理入口,通过该api,我们可以对执行class内容进行更改调整。
Transform 的基本认识
Transform 实际是一个普通的回调入口,该回调将根据 Transform 所返回的各个配置,在 class 转换为 dex 前,根据配置内容进行调用,并将结果通过 transform(Context, Collection<TransformInput>, Collection<TransformInput>, TransformOutputProvider, boolean)【过期】
或 transform(TransformInvocation)
进行返回。
配置
在 Transform 中有几个必须实现的内容
class ClassCollectorTransform extends Transform{
@Override
String getName() {
return null
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return null
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return null
}
@Override
boolean isIncremental() {
return false
}
}
方法名 | 功能 | 备注 |
---|---|---|
getName | Transform Task 命名,该返回名不是作为最后的命名,任务会自动补全该名字 | |
getInputType() | Transform 需要输入的内容类型 | |
getScopes | Transform 搜索范围类型 | |
isIncemental | 是否支持增量编译 |
主要参数说明:
InputType
该参数指名需要操作的内容,主要:
- CLASSES Java代码
- RESOURCES Java Resource 资源
除此之外官方提供了一些其他操作内容,但是受限于前置任务的产出物,并不能完全适用于当前阶段的 transform(指定操作内容必须是已产生的产出物)
- DEX
- NATIVE_LIBS
- CLASSES_ENHANCED
- DATA_BINDING
- JAVA_SOURCES
- DEX_ARCHIVE
Scopes
该参数指明需要搜索的目标范围,主要提供以下几种内容:
- PROJECT
当前项目内容
- SUB_PROJECTS
子项目内容
- EXTERNAL_LIBRARIES
外部依赖库
- TESTED_CODE
测试代码
- PROVIDED_ONLY
provider 方式的本地或者远程依赖
- PROJECT_LOCAL_DEPS
项目本地依赖(local jars)
- SUB_PROJECTS_LOCAL_DEPS
子项目的本地依赖(local jars)
根据以上参数,基本上我们通过使用 InputType + Scope 能实现对应目标内容的输入源的控制。
使用
配置完所需要的配置后,接下来就是对所输出的内容进行转换的过程。
在旧版中,内容是被输出到:
transform(Context contenxt, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental)
方法中
这里我们以新版为例:
transform(TransformInvocation transformInvocation)
TransformInvocation 只是对旧版的封装,及其增加一些补充内容,我们只对目前比较常用的一些参数进行描述。
Context getContext()
返回任务上下文,实质该Context为 TransformTask 的父类,TransformTask 是继承自 DefaultTask 的任务子类
Collection<TransformInput> getInputs()
返回InputType Scopes所要求的内容,TransformInput 提供了两种结果输出,一种是jar,一种是 File(被DirectoryInput包装了) 输出,这两种内容我们都需要进行处理。
TransformOutputProvider getOutputProvider()
TransformOutputProvider 是官方提供的输出代理类,通过该工具类,我们可以将需要指定输出的内容输出到所要求的目录下。
下面用一个例子来简单操作下:
class ExampleTransform extends Transform {
@Override
String getName() {
return "Example"
}
@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 {
super.transform(transformInvocation)
println "begin transform"
transformInvocation.getInputs().each { TransformInput input ->
input.directoryInputs.each { DirectoryInput directoryInput->
File outputFile = transformInvocation.getOutputProvider().getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, outputFile)
println "directory input >>> ${directoryInput.file.getAbsolutePath()}"
println "directory output >>> ${outputFile.getAbsolutePath()} \n"
}
input.jarInputs.each { JarInput jarInput->
String jarName = jarInput.name
File outputFile = transformInvocation.getOutputProvider().getContentLocation(jarName,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, outputFile)
println "jar input >>> ${jarInput.file.getAbsolutePath()}"
println "jar output >>> ${outputFile.getAbsolutePath()}\n"
}
}
println "end transform"
}
}
输出内容:
...
:app:mergeDebugAssets
:app:transformClassesWithExampleForDebug
begin transform
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/appcompat-v7-27.1.1.aar/7228f687c4d3d50971fb2c4144b818e5/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/0.jar
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.2.aar/1ffd866ac236943344a2db8900ec9ff0/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/1.jar
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/support-fragment-27.1.1.aar/366533887dab6e0bdc7b5f9565c8df3f/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/2.jar
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/animated-vector-drawable-27.1.1.aar/9aa09e1b40578aaf51e0fe3c26b1cf2b/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/3.jar
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/support-core-ui-27.1.1.aar/fbf121a4aa433efd0847730e6a71dfa6/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/4.jar
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/support-core-utils-27.1.1.aar/249c37a99952d8b3446a0f10707ef631/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/5.jar
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/support-vector-drawable-27.1.1.aar/0e11925044cb0cd7243a520ef57b6b65/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/6.jar
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/support-compat-27.1.1.aar/89e06865e27940d83a4ef0b84e7a40ec/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/7.jar
jar input >>> /Users/XX/.gradle/caches/modules-2/files-2.1/com.android.support/support-annotations/27.1.1/39ded76b5e1ce1c5b2688e1d25cdc20ecee32007/support-annotations-27.1.1.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/8.jar
jar input >>> /Users/XX/.gradle/caches/modules-2/files-2.1/com.android.support.constraint/constraint-layout-solver/1.1.2/bfc967828daffc35ba01c9ee204d98b664930a0f/constraint-layout-solver-1.1.2.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/9.jar
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/livedata-core-1.1.0.aar/63c28b9d84c74954069ca32d59a8948a/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/10.jar
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/viewmodel-1.1.0.aar/e7d22623b4477da4befa3300011bae7f/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/11.jar
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/runtime-1.1.0.aar/ddae57800275e6f4fbfcddbc1689ac1c/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/12.jar
jar input >>> /Users/XX/.gradle/caches/modules-2/files-2.1/android.arch.lifecycle/common/1.1.0/edf3f7bfb84a7521d0599efa3b0113a0ee90f85/common-1.1.0.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/13.jar
jar input >>> /Users/XX/.gradle/caches/transforms-1/files-1.1/runtime-1.1.0.aar/6a2bd34a44e70a5ec1df7445a52ad01e/jars/classes.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/14.jar
jar input >>> /Users/XX/.gradle/caches/modules-2/files-2.1/android.arch.core/common/1.1.0/8007981f7d7540d89cd18471b8e5dcd2b4f99167/common-1.1.0.jar
jar output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/15.jar
directory input >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/classes/debug
directory output >>> /Users/XX/Desktop/Git/transformExample/app/build/intermediates/transforms/Example/debug/16
end transform
...
在以上例子中我们并未对拦截到的class内容进行任何操作,仅仅只是将它们进行转移,这里需要注意的是,我们对于拦截的内容,需要手动转移到指定的 getContentLocation
地址下,不能随意进行存储,否则将不能正常处理转换后的内容。
另外根据所指定的产物内容的不同,getContentLocation
所存放的地址也不一样。
这里值得注意的是,如果我们忽略了部分代码的转移,那么在最后的包中,将不再有该代码文件的存在。
TransformManager 提供了较多常用的封装工具,可以参考这里面的API进行调用
总结
结合以上的内容,已经可以初步获取到所需要产物的输入内容,并能将所输出的内容转移到对应的目录下。在这个流程中,我们可以任意对输入内容进行修改后再进行转移,以达到所需要的代码调整的目的。
当然对应晦涩难懂的Class 字节码,我们不会直接对字节流进行读取后操作,而是借助于字节码工具,在进行加工处理。
比较出名的字节码操作库有javassist、asm、aspectJ等