奇门遁甲之Transform API
函数插桩技术是可以提高开发者开发效能的有力工具。常用的组合是TransformApi+ ASM,在打包apk的过程中,对特定的类最修改,偷梁换柱,以满足我们的一些特殊需要,如全局监控网络、计算方法耗时、组件化中的路由收集,自动加埋点等。
但是使用中对Transform API 的理解一直不是很到位,如Transform是在apk打包apk的哪个环节生效,Transfrom的边界是哪里?
本文就上面这些问题 做一些梳理和总结。
一、Transform API 的常规用法
Transform API 是gradle 1.5.0 开始引入的,它允许第三方插件在编译后的类文件转换为dex文件之前做处理操作. Transform API 可以让我们聚焦在如何对输入的类文件进行处理,而不用关系AppPlugin的编译流程。
1.1、注册Transform
使用Transform 只需要注册一个Plugin,在Apply方法中,在AppExtension对象上调用registerTransform() 将自定义Transform添加进去就可以了。
class TrPlugin : Plugin<Project> {
override fun apply(project: Project) {
var isApp =
project.plugins.hasPlugin(AppPlugin::class.java) //是否引入了com.android.application 插件
TrLogger.setLogger(project.logger)
if (isApp) {
//注册Transform
val android = target.extensions.findByType(AppExtension::class.java)
android?.registerTransform(CostTransform())
}
}
}
AppExtension 是实际上对应build.gradle中的android{}标签
AppExtension 集成自BaseExtension,可见注册Transform仅是将Transform对象 加入到了AppExtension的transforms容器中.
1.2、TransForm 的主要API
Transform的常用API 如下
public abstract class Transform {
public abstract String getName();
public abstract Set<ContentType> getInputTypes();
public abstract Set<? super Scope> getScopes();
public abstract boolean isIncremental();
fun transform(transformInvocation:TransformInvocation)
}
1.2.1、name :Transform的唯一名称。
Transform 最终会被封装成一个TransformTask,TransformTask的名称并不与Transform完全一致。
TransformTask的名称格式如下:
transform+"InputType"+With+"TransformName"+For+"BuildType"
static String getTaskNamePrefix(Transform transform) {
StringBuilder sb = new StringBuilder(100);
sb.append("transform");
sb.append((String)transform.getInputTypes().stream().map((inputType) -> {
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, inputType.name());
}).sorted().collect(Collectors.joining("And"))).append("With").append(StringHelper.capitalize(transform.getName())).append("For");
return sb.toString();
}
如自定义的Transform 名称是Cost,对应的TransformTask的名称可能为:transformClassesWithCostForDebug
1.2.2、getInputTypes:transform 要处理的数据类型
我们可以用的只有两个:
-
CLASSES 表示要处理编译后的字节码,CLASSES已经包含了class文件和jar文件
-
RESOURCES 表示的是啥???? 没搞清楚,此处待定。
1.2.3、getScopes 表示transform 的作用域
type | Des |
---|---|
PROJECT | 只处理当前项目 |
SUB_PROJECTS | 只处理子项目 |
PROJECT_LOCAL_DEPS | 只处理当前项目的本地依赖,例如jar, aar |
EXTERNAL_LIBRARIES | 只处理外部的依赖库 |
PROVIDED_ONLY | 只处理本地或远程以provided形式引入的依赖库 |
TESTED_CODE | 测试代码 |
1.2.4、tranform方法
transform() 是Tranform进行数据处理的地方。
imageTransForm是链式调用的,如上图所示,TransformB的输入 是TransformA的输出,TransformB的输出同时也是TransformC的输入。
所以Transform.transform()方法 即使任何功能不实现,也需要完成一个将文件从input目录拷贝到output目录的动作,否则下一个Transform将会丢失待处理的文件(class或jar)。
加入Transform的名字为Cost,项目编译之后,在build/intermediates/transform目录下就会出现Cost目录,该目录就是Cost Transform的输出目录,同时也是下一级Tranform的输入目录。
image
Cost目录目录之下还会生成一个content.json ,类似一个文件清单的样子。
[{
"name": "org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.72_7b6c9b0015ab57b3a6475f5627bb94c0",
"index": 0,
"scopes": ["EXTERNAL_LIBRARIES"],
"types": ["CLASSES"],
"format": "JAR",
"present": true
}, {
"name": "androidx.core:core-ktx:1.3.2_004fa720a5219b591486b877aa0fab1c",
"index": 1,
"scopes": ["EXTERNAL_LIBRARIES"],
"types": ["CLASSES"],
"format": "JAR",
"present": true
}
...
]
在完成class和jar文件从input目录拷贝到output的基础之上,可以完成一些额外的处理操作,如利用ASM 对特定类进行修改。
override fun transform(transformInvocation: TransformInvocation?) {
//TransformInput 包含两个类型的输入:jar文件和文件夹
transformInvocation?.inputs?.forEach { input ->
//jar输入,它代表着以jar包方式参与项目编译的所有本地jar包或远程jar包,
input.jarInputs.forEach { jarInput ->
//输入文件名
val destName = jarInput.name.let {
//jar文件去掉.jar后缀
if (it.endsWith(".jar")) it.substring(0, it.length - 4) else it
}
//确定输出文件名
val finalDestName = "${destName}_${DigestUtils.md5Hex(jarInput.file.absolutePath)}"
//确定输出文件
val destFile = transformInvocation.outputProvider.getContentLocation(
finalDestName,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR
)
//(1) 此处完成对jar文件的额外处理
//通用操作,将jar文件 从输入copy到输出目的地
FileUtils.copyFile(jarInput.file, destFile)
}
//目录输入,它代表着以源码方式参与项目编译的所有目录结构及其目录下的源码文件
input.directoryInputs.forEach { directoryInput ->
//(2)此处可以完成对class文件的额外处理操作
//确定输出文件des
val dest: File = transformInvocation.getOutputProvider().getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY
)
//完成从source到dest的拷贝操作
TrLogger.e("DirectoryInput:${directoryInput.file.absolutePath},dest:${dest.absolutePath}")
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
}
二、Tranform 是如何工作的
2.1、AppPlugin
Android项目build.gradle中通常会引入application插件
apply plugin: 'com.android.application'
com.android.application 其实是Gradle内置的一个用于构建apk的gradle插件,对应AppPlugin.class,它负责完成apk整个构建过程。
2.2、Extention
Plugin的入口函数Apply()接收一个参数Project,Project代表运行该插件的项目.
project.extensions.getByType(AppExtension::class.java)
Project中可以注册一些可以供用户个性化配置的信息,称作Extention,通过Extension用户向Plugin插件传递参数。
Extention通过Project.ExtensionContainer进行维护,支持通过名称、类名查找,支持新增Extention
public interface Project extends Comparable<Project>, ExtensionAware, PluginAware {
ExtensionContainer getExtensions();
}
AppExtension是AppPlugin会默认创建的一个Extension
//注册AppExtention,取名为android
project.getExtensions()
.create(
"android",//指定extension的名称
AppExtention(),
project,
projectOptions,
globalScope,
sdkHandler,
buildTypeContainer,
productFlavorContainer,
signingConfigContainer,
buildOutputs,
sourceSetManager,
extraModelInfo,
isBaseApplication);
AppExtension 对我们其实并不陌生,它实际上就是build.gradle中的android
android {
compileSdkVersion 29
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "com.sogou.iot.trplugin"
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
通过AppExtension 我们可以告诉AppPlugin,我们所使用的compileSdkVersion、buildToolsVersion、buildTypes、Flavor 等等。
此外我们在自定义Plugin中,也可以为Project注册Extention,来供用户传递参数。
注册Extension
class TrPlugin : Plugin<Project> {
override fun apply(project: Project) {
var isApp =
project.plugins.hasPlugin(AppPlugin::class.java) //是否引入了com.android.application 插件
TrLogger.setLogger(project.logger)
if (isApp) {
//注册Extension
project.extensions.create(PluginHolder.componentExt, ComponentExtension::class.java)
project.extensions.getByType(AppExtension::class.java)?.apply {
registerTransform(CostTransform())
//扫描,收集类信息
}
}
}
}
open class ComponentExtension {
//待搜集的接口类
var matchInterfaceType: String = ""
//Container容器类
var matchInjectManagerType: String = ""
//容器类的
var matchInjectManagerInjectMethod: String = ""
var openLog:Boolean = false
var logLevel:Int = TrLogger.LogLevelDebug
}
build.gradle 设置componentExt参数
componentExt{
matchInterfaceType = "com.sogou.iot.trplugin.IComponent"
matchInjectManagerType = "com.sogou.iot.trplugin.ComponentManager"
matchInjectManagerInjectMethod = "initComponet"
openLog = true
logLevel = TrLogger.LogLevelDebug
}
插件中提取参数
(project?.extensions?.getByName(componentExt) as ComponentExtension).openLog
讲了这么多,Extension和Transform有什么关系呢?上面有点扯远了,下面进入正题。
AppExtension继承BaseExtension,BaseExtension中有一个Transform类型的数组
public abstract class BaseExtension implements AndroidConfig {
private final List<Transform> transforms = Lists.newArrayList();
}
所以registerTransform()注册Transform 实际上是把Tranform对象加入到transforms数组中,以便后面构建TransformTask时使用。
## BaseExtension.class
public void registerTransform(@NonNull Transform transform, Object... dependencies) {
transforms.add(transform);
transformDependencies.add(Arrays.asList(dependencies));
}
2.3、自定义TransformTask的边界在哪里
上面提到一个问题:Transfrom 工作在apk构架的哪一环节?它的边界再哪里?
我们看一下Apk的整体构建流程:
image
我们自定义的Transform 是在javac将java文件编译成.cass文件之后。也就是上图中第4步dex的过程。
dex过程内部又包含了一些列的TrasformTask,完整TransformTask链如下图所示:
dex完整流程
- jacoco 是用于统计代码覆盖率的Task,在isTestCoverageEnabled= true时会加入jacoco Task
- desuger 脱糖处理,将Java8 的特性语法糖(如lamda表达式) 替换为java 7中的标准语言,以实现对仅支持java7的编译工具的兼容.
android.enableD8.desugaring = false时,会加入desuger TranfromTask
- MergeJavaRes:合并资源,处理lib/目录下的aar和so文件
Transform to merge all the Java resources.
- 自定义Transfrom,假设加入了两个自定义的Transform:Cost和Scan
- MergeClass:将class文件合并成jar
A transform that takes the FULL_PROJECT's CLASSES streams and merges the class files into a single jar.
- AdvancedProfiling 可选
- Proguard:混淆和去除无用代码
- PreColdSwap:可选,好像和InstanceRun有关,没弄太明白。
/**
* Task to disable execution of the InstantRun slicer, dexer and packager when they are not needed.
*
* <p>The next time they run they will pick up all intermediate changes.
*
* <p>With multi apk (N or above device) resources are packaged in the main split APK. However when
* a warm swap is possible, it is not necessary to produce immediately the new main SPLIT since the
* runtime use directly the resources.ap_ file. However, as soon as an incompatible change forcing a
* cold swap is triggered, the main APK must be rebuilt (even if the resources were changed in a
* previous build).
*/
- D8MainDexList 可选,通过D8计算哪些类应该加入到主Dex中
Calculate the main dex list using D8.
- Dex: 将class文件生成dex文件
- ResourcesShrinker:资源压缩,可选。
- DexSplitter:拆分Dex为多个,可选。
/**
* Transform that splits dex files depending on their feature sources
*/
可以看到Dex的过程有非常多的系统Transform非常复杂,这些Transform有些是在特定条件才添加进Transfrom链中的。
假设我们自定义的Transform 处理的InputType 为TransformManager.CONTENT_CLASS (class和jar),去掉哪些可有可无的系统Transform流程就会清晰很多。如下:
dex简化流程
假设我们定义了两个Transform(Cost和Scan),那么
- Cost Transform的上一个Task为Javac,Cost的input目录为app/build/intermediates/javac,Cost的输出目录为app/build/intermediates/Cost
- Scan Transform的上一个Task为Cost,Scan的输入为app/build/intermediates/Cost,Scan的输出为app/build/intermediates/Scan
2.4 源码
上面Transform的构建流程,可以参考AppPlugin和TaskManager
AppPlugin启动时调用apply()方法,主要做了三件事情:
// 配置项目,设置构建回调
this::configureProject
// 配置Extension
this::configureExtension
// 创建任务
this::createTasks
- configureProject 做的事情,主要是进行版本有效性的判断,创建了 AndroidBuilder 对象,并设置了构建流程的回调来处理依赖和dex的加载和缓存清理
- configureExtension 方法的作用,主要是创建 AppExtention扩展对象, 创建taskManager。
- createTasks 主要是通过taskManager等工具,构建编译任务。
TaskManager是构建任务Task的大管家,它负责组织编译任务,其中就包括Transform的任务链。
##TaskMapager.java
protected void createCompileTask(@NonNull VariantScope variantScope) {
//(1)创建Javac Task
TaskProvider<? extends JavaCompile> javacTask = createJavacTask(variantScope);
addJavacClassesStream(variantScope);
setJavaCompilerTask(javacTask, variantScope);
//(2)构建Javac之后的TransformTask任务链
createPostCompilationTasks(variantScope);
}
- 在添加javac Task任务之后,调用了createPostCompilationTasks()方法
- 了createPostCompilationTasks()中完成了Transform任务链的构建
2.5 小结
TransformTask位于Javac Task之后,主要职责是完成生成dex文件。TransformTask会组成一个"Dex任务链"。自定义的Transform 会插入到任务链的最前面,而DexTransform位于"Dex任务链"的末尾。
所以自定义的Transform 仅能在javac 将java文件编译成class文件之后,在class文件转换成dex之前做一些class处理操作,其输入是class和jar,输出也是class和jar。
其他
相关知识点:
三、Transform的优化
Tranform的增量编译和并发编译可以参照 一起玩转Android项目中的字节码 一文
四、其他
4.1、常用名字解释
- D8相关 用于替代dx工具的, 职责是 将class文件转化成dex文件
- Proguard 压缩与优化(minification、shrinking、optimization)部分的替代品,依然使用与Proguard一样的keep规则。
4.2、如何查看gradle Task执行时间
./gradlew clean assembleDebug --profile
profile参数 可以查看Gradle 编译各阶段 各任务的耗时,并生成一个Profile网页
See the profiling report at: file:///Users/feifei/Desktop/TM/Demo/TrPlugin/build/reports/profile/profile-2021-01-26-10-10-14.html
image
4.3、Transform+ASM实践
可参照trplugin:
利用函数插桩实现了自定计算方法耗时,自动收集组件信息等功能。
五、参考文章
https://juejin.cn/post/6844903829671002126