现在有很多的框架用到APT的技术,可以很好的解耦,实现编译期生成文件或者修改class文件,实现插桩的功能,Android提供了Transform的接口,在编译期间可以拿到所有编译后的class文件和jar包(包括aar包)。 美团开源的WMRouter中就用到这项技术,在编译期找到所有的ServiceInit_XXX.class文件(包括本地主工程、子工程和依赖的aar/jar包),结合ASM生成ServiceLoaderInit.class文件,在框架运行时进行异步加载所有的Service实现。 今天重点在分析里面的Transform技术,ServiceInit_XXX.class等文件什么功能不会做介绍,这个以后的系列文章会进行阐述。
1. Transform插件开发流程
Transform其实也是可以理解成一个gradle task,是Google写的可以在这个task里面拿到安装包的所有class文件。 为了编译期间能执行该task,需要编写一个gradle plugin(这方面可以参考我之前的文章 一步步自定义Gradle插件),并且在我们的app工程的build.gradle中apply这个插件,看下实现步骤和代码。
第一步,先建一个Java Library工程,最后整体结构如下
其中META-INF中的文件名是插件的名称,在app build.gradle中需要apply:
apply plugin: 'WMRouter'
WMRouter {
enableDebug = true // 调试开关
enableLog = true
}
看下WMRouter.properties文件的内容:
implementation-class=com.sankuai.waimai.router.plugin.WMRouterPlugin
上面就是插件的入口WMRouterPlugin,将本文的主角WMRouterTransform注册到project中,这样编译时才会被执行。
public class WMRouterPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
WMRouterExtension extension = project.getExtensions()
.create(Const.NAME, WMRouterExtension.class);
WMRouterLogger.info("register transform");
project.getExtensions().findByType(BaseExtension.class)
.registerTransform(new WMRouterTransform());
project.afterEvaluate(p -> WMRouterLogger.setConfig(extension));
}
}
2. Transform源码
先看下其父类Transform的源码,需要实现下面四个抽象方法:
public abstract class Transform {
/**
* Returns the unique name of the transform.
*
* <p>This is associated with the type of work that the transform does. It does not have to be
* unique per variant.
*/
@NonNull
public abstract String getName();
/**
* Returns the type(s) of data that is consumed by the Transform. This may be more than
* one type.
*
* <strong>This must be of type {@link QualifiedContent.DefaultContentType}</strong>
*/
@NonNull
public abstract Set<ContentType> getInputTypes();
/**
* Returns the scope(s) of the Transform. This indicates which scopes the transform consumes.
*/
@NonNull
public abstract Set<? super Scope> getScopes();
/**
* Returns whether the Transform can perform incremental work.
*
* <p>If it does, then the TransformInput may contain a list of changed/removed/added files, unless
* something else triggers a non incremental run.
*/
public abstract boolean isIncremental();
/**
* @deprecated replaced by {@link #transform(TransformInvocation)}.
*/
@Deprecated
@SuppressWarnings("UnusedParameters")
public void transform(
@NonNull Context context,
@NonNull Collection<TransformInput> inputs,
@NonNull Collection<TransformInput> referencedInputs,
@Nullable TransformOutputProvider outputProvider,
boolean isIncremental) throws IOException, TransformException, InterruptedException {
}
}
解释如下:
- getName就是这个task的名字,在编译时可以看到,比如打debug包是有下面的task::demoapp:transformClassesWithWMRouterForDebug
- getInputTypes,声明我们感兴趣的文件类型,这里就是class,还可以有jar,dex, resource
public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
public static final Set<ContentType> CONTENT_NATIVE_LIBS =
ImmutableSet.of(NATIVE_LIBS);
public static final Set<ContentType> CONTENT_DEX = ImmutableSet.of(ExtendedContentType.DEX);
public static final Set<ContentType> CONTENT_DEX_WITH_RESOURCES =
ImmutableSet.of(ExtendedContentType.DEX, RESOURCES);
public static final Set<ContentType> DATA_BINDING_BASE_CLASS_LOG_ARTIFACT =
ImmutableSet.of(ExtendedContentType.DATA_BINDING_BASE_CLASS_LOG);
- getScopes, 声明该task作用的范围,一般常用的是这些,我们这里声明作用范围包括主工程,子工程和外部依赖包
enum Scope implements ScopeType {
/** Only the project (module) content */
PROJECT(0x01),
/** Only the sub-projects (other modules) */
SUB_PROJECTS(0x04),
/** Only the external libraries */
EXTERNAL_LIBRARIES(0x10),
...
}
public static final Set<Scope> SCOPE_FULL_PROJECT =
Sets.immutableEnumSet(
Scope.PROJECT,
Scope.SUB_PROJECTS,
Scope.EXTERNAL_LIBRARIES);
- isIncremental,是否支持增量编译,这里一般返回false
- 主要工作在最后一个方法transformz中,通过入参TransformInvocation可以拿到工程文件。下面就具体看下WMRouterTransform的该方法实现。
3. WMRouterTransform
目的是扫描com.sankuai.waimai.router.generated.service目录下的class文件,这些class文件也是通过编译时生成,这个本文先不介绍。
扫描到的文件名称保存到initClasses容器下。
invocation有两种输入类型,一种是依赖包jarInput(包括aar),一种是目录directoryInput
// WMRouterTransform.java
@Override
public void transform(TransformInvocation invocation) {
WMRouterLogger.info(TRANSFORM + "start...");
long ms = System.currentTimeMillis();
Set<String> initClasses = Collections.newSetFromMap(new ConcurrentHashMap<>());
for (TransformInput input : invocation.getInputs()) {
input.getJarInputs().parallelStream().forEach(jarInput -> {
File src = jarInput.getFile();
File dst = invocation.getOutputProvider().getContentLocation(
jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(),
Format.JAR);
try {
scanJarFile(src, initClasses);
FileUtils.copyFile(src, dst);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
input.getDirectoryInputs().parallelStream().forEach(directoryInput -> {
File src = directoryInput.getFile();
File dst = invocation.getOutputProvider().getContentLocation(
directoryInput.getName(), directoryInput.getContentTypes(),
directoryInput.getScopes(), Format.DIRECTORY);
try {
scanDir(src, initClasses);
FileUtils.copyDirectory(src, dst);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
File dest = invocation.getOutputProvider().getContentLocation(
"WMRouter", TransformManager.CONTENT_CLASS,
ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY);
generateServiceInitClass(dest.getAbsolutePath(), initClasses);
WMRouterLogger.info(TRANSFORM + "cost %s ms", System.currentTimeMillis() - ms);
}
打个断点看了下jarInput打出来的组成,该jarInput是依赖的子工程(scopes:SUB_PROJECT) demolib2(name=:demolib2),里面的文件类型是CLASSES
而编译后的文件包在/demolib2/build/intermediates/intermediate-jars/debug/classes.jar
看下目录截图
这里面就能拿到子工程demolib2中的指定文件,再接着源码往下看.
src就是上面编译后的classes.jar,而每次transform修改后的文件要复制到指定输出目录,否则下一个transform或者task就拿不到文件。
而这里dst就是输出目录,在上面的jarinput中可以看到dst=''WMRouter/demoapp/build/intermediates/transforms/WMRouter/debug/17.jar''
// WMRouterTransform.java
input.getJarInputs().parallelStream().forEach(jarInput -> {
File src = jarInput.getFile();
File dst = invocation.getOutputProvider().getContentLocation(
jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(),
Format.JAR);
try {
scanJarFile(src, initClasses);
FileUtils.copyFile(src, dst);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
再看下scanJarFile(src, initClasses)
:
在子工程demolib2的目标目录com.sankuai.waimai.router.generated.service
目录下有两个ServiceInit_xxx
文件,把这两个文件添加到initClasses保存。
而对于工程里的另外一个子工程demolib1也是同样的逻辑,输出目录在
dst=''WMRouter/demoapp/build/intermediates/transforms/WMRouter/debug/18.jar''
再来看下主工程的目录文件扫描过程,结合图片会比较好理解,是directoryInput打出来的组成,该directoryInput是主工程工程(scopes:PROJECT),里面的文件类型是CLASSES
input.getDirectoryInputs().parallelStream().forEach(directoryInput -> {
File src = directoryInput.getFile();
File dst = invocation.getOutputProvider().getContentLocation(
directoryInput.getName(), directoryInput.getContentTypes(),
directoryInput.getScopes(), Format.DIRECTORY);
try {
scanDir(src, initClasses);
FileUtils.copyDirectory(src, dst);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
源路径src=WMRouter/demoapp/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes
,输出路径和前面两个子工程在同一个目录下,包名不一样是22.jar
看下源路径的截图:
再回到源码,循环扫描完成后就是需要生成目标文件了,生成的目标文件位置在dest="WMRouter/demoapp/build/intermediates/transforms/WMRouter/debug/23",文件名是ServiceLoaderInit.class
File dest = invocation.getOutputProvider().getContentLocation(
"WMRouter", TransformManager.CONTENT_CLASS,
ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY);
generateServiceInitClass(dest.getAbsolutePath(), initClasses);
WMRouterLogger.info(TRANSFORM + "cost %s ms", System.currentTimeMillis() - ms);
打开看一下历经千辛万苦生产的目标文件长啥样,就是主工程,子工程,依赖包下的所有复合指定包名和后缀的ServiceInit_xxx文件。
package com.sankuai.waimai.router.generated;
import xxx
public class ServiceLoaderInit {
public static void init() {
ServiceInit_aea7f96d0419b507d9b0ef471913b2f5.init();
ServiceInit_f3649d9f5ff15a62b844e64ca8434259.init();
ServiceInit_eb71854fbd69455ef4e0aa026c2e9881.init();
ServiceInit_b57118238b4f9112ddd862e55789c834.init();
ServiceInit_f1e07218f6691f962a9f674eb5b4b8bd.init();
ServiceInit_4268a3e74040533ba48f2e1679155468.init();
ServiceInit_e694d982fb5d7a3a8c6b7085829e74a6.init();
ServiceInit_ee5f6404731417fe1433da40fd3c9708.init();
ServiceInit_9482ef47a8cf887ff1dc4bf705d5fc0a.init();
ServiceInit_36ed390bf4b81a8381d45028b37cc645.init();
}
}
再看下生成文件的操作generateServiceInitClass(dest.getAbsolutePath(), initClasses);
4. ASM
先上源码,
首先构造一个ClassWriter,通过它后面写入到文件
再通过ClassVisitor来构造类ServiceLoaderInit,父类默认Object
通过MethodVisitor来构造init方法,方法体里面内容通过遍历classes调用visitMethodInsn生成
最后通过FileOutputStream写出到文件ServiceLoaderInit.class
private void generateServiceInitClass(String directory, Set<String> classes) {
if (classes.isEmpty()) {
WMRouterLogger.info(GENERATE_INIT + "skipped, no service found");
return;
}
try {
WMRouterLogger.info(GENERATE_INIT + "start...");
long ms = System.currentTimeMillis();
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, writer) {
};
String className = Const.SERVICE_LOADER_INIT.replace('.', '/');
cv.visit(50, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,
Const.INIT_METHOD, "()V", null, null);
mv.visitCode();
for (String clazz : classes) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, clazz.replace('.', '/'),
"init",
"()V",
false);
}
mv.visitMaxs(0, 0);
mv.visitInsn(Opcodes.RETURN);
mv.visitEnd();
cv.visitEnd();
File dest = new File(directory, className + SdkConstants.DOT_CLASS);
dest.getParentFile().mkdirs();
new FileOutputStream(dest).write(writer.toByteArray());
WMRouterLogger.info(GENERATE_INIT + "cost %s ms", System.currentTimeMillis() - ms);
} catch (IOException e) {
WMRouterLogger.fatal(e);
}
}
5.总结
在整个WMRouterTransform中都有通过日志来打印一些关键节点,看下整个编译过程的日志。
首先是在plugin入口调用的[WMRouter] register transform,
接下来会分别编译子工程router->demolib2->demokotlin->demolib1->demoapp 的compileDebugJavaWithJavac,这里会得到class文件
最后执行:demoapp:transformClassesWithWMRouterForDebug,分别找到10个ServiceInitClass
文件,
生成ServiceLoaderInit.class
文件耗时19ms,整个Transform过程耗时188ms
最后通过:demoapp:transformClassesWithDexBuilderForDebug打dex包
Executing tasks: [:demoapp:assembleDebug] in project
[WMRouter] register transform
...
:router:compileDebugJavaWithJavac UP-TO-DATE
:router:processDebugJavaRes NO-SOURCE
:router:transformClassesAndResourcesWithPrepareIntermediateJarsForDebug UP-TO-DATE
...
:demolib2:compileDebugJavaWithJavac
:demolib2:processDebugJavaRes NO-SOURCE
:demolib2:transformClassesAndResourcesWithPrepareIntermediateJarsForDebug
:demokotlin:kaptGenerateStubsDebugKotlin
:demokotlin:kaptDebugKotlin
:demokotlin:compileDebugKotlin
:demokotlin:prepareLintJar UP-TO-DATE
:demokotlin:generateDebugSources UP-TO-DATE
:demokotlin:javaPreCompileDebug
:demokotlin:compileDebugJavaWithJavac
:demokotlin:processDebugJavaRes NO-SOURCE
:demokotlin:transformClassesAndResourcesWithPrepareIntermediateJarsForDebug
:demolib1:generateDebugBuildConfig
...
:demolib1:compileDebugJavaWithJavac
:demolib1:processDebugJavaRes NO-SOURCE
:demolib1:transformClassesAndResourcesWithPrepareIntermediateJarsForDebug UP-TO-DATE
:demoapp:javaPreCompileDebug UP-TO-DATE
...
:demoapp:compileDebugJavaWithJavac
...
:demoapp:transformClassesWithWMRouterForDebug
[WMRouter] Transform: start...
[WMRouter] find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_9482ef47a8cf887ff1dc4bf705d5fc0a
[WMRouter] find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_4268a3e74040533ba48f2e1679155468
[WMRouter] find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_ee5f6404731417fe1433da40fd3c9708
[WMRouter] find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_f1e07218f6691f962a9f674eb5b4b8bd
[WMRouter] find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_aea7f96d0419b507d9b0ef471913b2f5
[WMRouter] find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_f3649d9f5ff15a62b844e64ca8434259
[WMRouter] find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_e694d982fb5d7a3a8c6b7085829e74a6
[WMRouter] find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_eb71854fbd69455ef4e0aa026c2e9881
[WMRouter] find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_36ed390bf4b81a8381d45028b37cc645
[WMRouter] find ServiceInitClass: com.sankuai.waimai.router.generated.service.ServiceInit_b57118238b4f9112ddd862e55789c834
[WMRouter] GenerateInit: start...
[WMRouter] GenerateInit: cost 19 ms
[WMRouter] Transform: cost 188 ms
:demoapp:transformClassesWithDexBuilderForDebug
...
BUILD SUCCESSFUL in 21s
107 actionable tasks: 40 executed, 67 up-to-date
w: Detected multiple Kotlin daemon sessions at build/kotlin/sessions
通过上面所有几个步骤就把WMRouter中的Transform技术给说清楚了,没错就是为了生成一个文件费这么大周章。但是也有好处,主app没有强依赖子工程或者依赖包的具体服务,这个技术也可以用于主app分发生命周期,只需要在Application的生命周期中调用生成文件的方法即可。另外其实通过传统的ServiceLoader也可以,但是有一个缺点就是需要运行时去IO读取文件再反射构造调用,而通过这种方式编译期生成文件就可以避免掉IO读取接口文件的步骤,性能是比较好的。