其实Transform API
在一个android工程的打包流程中做用很是大, 像是咱们熟知的混淆处理, 类文件转dex文件的处理, 都是经过Transform API
去完成的. 本篇内容主要围绕Transform
作展开:html
Transform API
的使用及原理ASM
使用技巧Transform API
在应用工程上的使用摸索自从1.5.0-beta1
版本开始, android gradle插件就包含了一个Transform API
, 它容许第三方插件在编译后的类文件转换为dex文件以前作处理操做. 而使用Transform API
, 咱们彻底能够不用去关注相关task的生成与执行流程, 它让咱们能够只聚焦在如何对输入的类文件进行处理java
Transform的注册和使用很是易懂, 在咱们自定义的plugin内, 咱们能够经过android.registerTransform(theTransform)
或者android.registerTransform(theTransform, dependencies).
就能够进行注册.android
class DemoPlugin: Plugin<Project> {
override fun apply(target: Project) {
val android = target.extensions.findByType(BaseExtension::class.java)
android?.registerTransform(DemoTransform())
}
}
复制代码
而咱们自定义的Transform
继承于com.android.build.api.transform.Transform
, 具体咱们能够看javaDoc, 如下代码是比较常见的transform处理模板git
class DemoTransform: Transform() {
/** * transform 名字 */
override fun getName(): String = "DemoTransform"
/** * 输入文件的类型 * 可供咱们去处理的有两种类型, 分别是编译后的java代码, 以及资源文件(非res下文件, 而是assests内的资源) */
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_CLASS
/** * 是否支持增量 * 若是支持增量执行, 则变化输入内容可能包含 修改/删除/添加 文件的列表 */
override fun isIncremental(): Boolean = false
/** * 指定做用范围 */
override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.SCOPE_FULL_PROJECT
/** * transform的执行主函数 */
override fun transform(transformInvocation: TransformInvocation?) {
transformInvocation?.inputs?.forEach {
// 输入源为文件夹类型
it.directoryInputs.forEach {directoryInput->
with(directoryInput){
// TODO 针对文件夹进行字节码操做
val dest = transformInvocation.outputProvider.getContentLocation(
name,
contentTypes,
scopes,
Format.DIRECTORY
)
file.copyTo(dest)
}
}
// 输入源为jar包类型
it.jarInputs.forEach { jarInput->
with(jarInput){
// TODO 针对Jar文件进行相关处理
val dest = transformInvocation.outputProvider.getContentLocation(
name,
contentTypes,
scopes,
Format.JAR
)
file.copyTo(dest)
}
}
}
}
}
复制代码
每个Transform
都声明它的做用域, 做用对象以及具体的操做以及操做后输出的内容.github
经过Transform#getScopes
方法咱们能够声明自定义的transform的做用域, 指定做用域包括以下几种算法
QualifiedContent.Scope | |
---|---|
EXTERNAL_LIBRARIES | 只包含外部库 |
PROJECT | 只做用于project自己内容 |
PROVIDED_ONLY | 支持compileOnly的远程依赖 |
SUB_PROJECTS | 子模块内容 |
TESTED_CODE | 当前变体测试的代码以及包括测试的依赖项 |
经过Transform#getInputTypes
咱们能够声明其的做用对象, 咱们能够指定的做用对象只包括两种api
QualifiedContent.ContentType | |
---|---|
CLASSES | Java代码编译后的内容, 包括文件夹以及Jar包内的编译后的类文件 |
RESOURCES | 基于资源获取到的内容 |
TransformManager
整合了部分经常使用的Scope以及Content集合, 若是是application
注册的transform, 一般状况下, 咱们通常指定TransformManager.SCOPE_FULL_PROJECT
;若是是library
注册的transform, 咱们只能指定TransformManager.PROJECT_ONLY
, 咱们能够在LibraryTaskManager#createTasksForVariantScope
中看到相关的限制报错代码浏览器
Sets.SetView<? super Scope> difference =
Sets.difference(transform.getScopes(), TransformManager.PROJECT_ONLY);
if (!difference.isEmpty()) {
String scopes = difference.toString();
globalScope
.getAndroidBuilder()
.getIssueReporter()
.reportError(
Type.GENERIC,
new EvalIssueException(
String.format(
"Transforms with scopes '%s' cannot be applied to library projects.",
scopes)));
}
复制代码
而做用对象咱们主要经常使用到的是TransformManager.CONTENT_CLASS
网络
咱们经过实现Transform#transform
方法来处理咱们的中间转换过程, 而中间相关信息都是经过TransformInvocation
对象来传递app
public interface TransformInvocation {
/** * transform的上下文 */
@NonNull
Context getContext();
/** * 返回transform的输入源 */
@NonNull
Collection<TransformInput> getInputs();
/** * 返回引用型输入源 */
@NonNull Collection<TransformInput> getReferencedInputs();
/** * 额外输入源 */
@NonNull Collection<SecondaryInput> getSecondaryInputs();
/** * 输出源 */
@Nullable
TransformOutputProvider getOutputProvider();
/** * 是否增量 */
boolean isIncremental();
}
复制代码
关于输入源, 咱们能够大体分为消费型和引用型和额外的输入源
消费型
就是咱们须要进行transform操做的, 这类对象在处理后咱们必须指定输出传给下一级, 咱们主要经过getInputs()
获取进行消费的输入源, 而在进行变换后, 咱们也必须经过设置getInputTypes()
和getScopes()
来指定输出源传输给下个transform.getReferencedScopes()
指定咱们的引用型输入源的做用域后, 咱们能够经过TransformInvocation#getReferencedInputs()
获取引用型输入源ProGuardTransform
中, 就会指定建立mapping.txt传给下一级; 一样像是DexMergerTransform
, 若是打开了multiDex
功能, 则会将maindexlist.txt文件传给下一级咱们已经大体了解它是如何使用的, 如今看下他的原理(本篇源码基于gradle插件3.3.2
版本)在去年AppPlugin源码解析中, 咱们粗略了解了android的com.android.application
以及com.android.library
两个插件都继承于BasePlugin
, 而他们的主要执行顺序能够分为三个步骤
在BaseExtension
内部维护了一个transforms
集合对象, android.registerTransform(theTransform)
实际上就是将咱们自定义的transform实例新增到这个列表对象中. 在3.3.2
的源码中, 也能够这样理解. 在BasePlugin#createAndroidTasks
中, 咱们经过VariantManager#createAndroidTasks
建立各个变体的相关编译任务, 最终经过TaskManager#createTasksForVariantScope
(application
插件最终实现方法在TaskManager#createPostCompilationTasks
中, 而library
插件最终实现方法在LibraryTaskManager#createTasksForVariantScope
中)方法中获取BaseExtension
中维护的transforms
对象, 经过TransformManager#addTransform
将对应的transform对象转换为task, 注册在TaskFactory
中.这里关于一系列Transform Task
的执行流程, 咱们能够选择看下application
内的相关transform流程, 因为篇幅缘由, 能够自行去看相关源码, 这里的transform task流程分别是从Desugar->MergeJavaRes->自定义的transform->MergeClasses->Shrinker(包括ResourcesShrinker和DexSplitter和Proguard)->MultiDex->BundleMultiDex->Dex->ResourcesShrinker->DexSplitter, 由此调用链, 咱们也能够看出在处理类文件的时候, 是不须要去考虑混淆的处理的.
TransformManager
管理了项目对应变体的全部Transform
对象, 它的内部维护了一个TransformStream
集合对象streams
, 每当新增一个transform, 对应的transform会消费掉对应的流, 然后将处理后的流添加会streams
内
public class TransformManager extends FilterableStreamCollection{
private final List<TransformStream> streams = Lists.newArrayList();
}
复制代码
咱们能够看下它的核心方法addTransform
@NonNull
public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(
@NonNull TaskFactory taskFactory,
@NonNull TransformVariantScope scope,
@NonNull T transform,
@Nullable PreConfigAction preConfigAction,
@Nullable TaskConfigAction<TransformTask> configAction,
@Nullable TaskProviderCallback<TransformTask> providerCallback) {
...
List<TransformStream> inputStreams = Lists.newArrayList();
// transform task的命名规则定义
String taskName = scope.getTaskName(getTaskNamePrefix(transform));
// 获取引用型流
List<TransformStream> referencedStreams = grabReferencedStreams(transform);
// 找到输入流, 并计算经过transform的输出流
IntermediateStream outputStream = findTransformStreams(
transform,
scope,
inputStreams,
taskName,
scope.getGlobalScope().getBuildDir());
// 省略代码是用来校验输入流和引用流是否为空, 理论上不可能为空, 若是为空, 则说明中间有个transform的转换处理有问题
...
transforms.add(transform);
// transform task的建立
return Optional.of(
taskFactory.register(
new TransformTask.CreationAction<>(
scope.getFullVariantName(),
taskName,
transform,
inputStreams,
referencedStreams,
outputStream,
recorder),
preConfigAction,
configAction,
providerCallback));
}
复制代码
在TransformManager
中添加一个Transform
管理, 流程可分为如下几步
static String getTaskNamePrefix(@NonNull Transform transform) {
StringBuilder sb = new StringBuilder(100);
sb.append("transform");
sb.append(
transform
.getInputTypes()
.stream()
.map(
inputType ->
CaseFormat.UPPER_UNDERSCORE.to(
CaseFormat.UPPER_CAMEL, inputType.name()))
.sorted() // Keep the order stable.
.collect(Collectors.joining("And")));
sb.append("With");
StringHelper.appendCapitalized(sb, transform.getName());
sb.append("For");
return sb.toString();
}
复制代码
从上面代码, 咱们能够看到新建的transform task的命名规则能够理解为transform${inputType1.name}And${inputType2.name}With${transform.name}For${variantName}
, 对应的咱们也能够经过已生成的transform task来验证
streams
做用域和做用类型的交集来获取对应的流, 将其定义为咱们须要的引用型流
private List<TransformStream> grabReferencedStreams(@NonNull Transform transform) {
Set<? super Scope> requestedScopes = transform.getReferencedScopes();
...
List<TransformStream> streamMatches = Lists.newArrayListWithExpectedSize(streams.size());
Set<ContentType> requestedTypes = transform.getInputTypes();
for (TransformStream stream : streams) {
Set<ContentType> availableTypes = stream.getContentTypes();
Set<? super Scope> availableScopes = stream.getScopes();
Set<ContentType> commonTypes = Sets.intersection(requestedTypes,
availableTypes);
Set<? super Scope> commonScopes = Sets.intersection(requestedScopes, availableScopes);
if (!commonTypes.isEmpty() && !commonScopes.isEmpty()) {
streamMatches.add(stream);
}
}
return streamMatches;
}
复制代码
private IntermediateStream findTransformStreams( @NonNull Transform transform, @NonNull TransformVariantScope scope, @NonNull List<TransformStream> inputStreams, @NonNull String taskName, @NonNull File buildDir) {
Set<? super Scope> requestedScopes = transform.getScopes();
...
Set<ContentType> requestedTypes = transform.getInputTypes();
// 获取消费型输入流
// 并将streams中移除对应的消费型输入流
consumeStreams(requestedScopes, requestedTypes, inputStreams);
// 建立输出流
Set<ContentType> outputTypes = transform.getOutputTypes();
// 建立输出流转换的文件相关路径
File outRootFolder =
FileUtils.join(
buildDir,
StringHelper.toStrings(
AndroidProject.FD_INTERMEDIATES,
FD_TRANSFORMS,
transform.getName(),
scope.getDirectorySegments()));
// 输出流的建立
IntermediateStream outputStream =
IntermediateStream.builder(
project,
transform.getName() + "-" + scope.getFullVariantName(),
taskName)
.addContentTypes(outputTypes)
.addScopes(requestedScopes)
.setRootLocation(outRootFolder)
.build();
streams.add(outputStream);
return outputStream;
}
复制代码
如何触发到咱们实现的Transform#transform
方法, 就在TransformTask
对应的TaskAction中执行
void transform(final IncrementalTaskInputs incrementalTaskInputs) throws IOException, TransformException, InterruptedException {
final ReferenceHolder<List<TransformInput>> consumedInputs = ReferenceHolder.empty();
final ReferenceHolder<List<TransformInput>> referencedInputs = ReferenceHolder.empty();
final ReferenceHolder<Boolean> isIncremental = ReferenceHolder.empty();
final ReferenceHolder<Collection<SecondaryInput>> changedSecondaryInputs =
ReferenceHolder.empty();
isIncremental.setValue(transform.isIncremental() && incrementalTaskInputs.isIncremental());
GradleTransformExecution preExecutionInfo =
GradleTransformExecution.newBuilder()
.setType(AnalyticsUtil.getTransformType(transform.getClass()).getNumber())
.setIsIncremental(isIncremental.getValue())
.build();
// 一些增量模式下的处理, 包括在增量模式下, 判断输入流(引用型和消费型)的变化
...
GradleTransformExecution executionInfo =
preExecutionInfo.toBuilder().setIsIncremental(isIncremental.getValue()).build();
...
transform.transform(
new TransformInvocationBuilder(TransformTask.this)
.addInputs(consumedInputs.getValue())
.addReferencedInputs(referencedInputs.getValue())
.addSecondaryInputs(changedSecondaryInputs.getValue())
.addOutputProvider(
outputStream != null
? outputStream.asOutput(
isIncremental.getValue())
: null)
.setIncrementalMode(isIncremental.getValue())
.build());
if (outputStream != null) {
outputStream.save();
}
}
复制代码
经过上文的介绍, 咱们如今应该知道了自定义的Transform执行的时序, 位置, 以及相关原理. 那么, 咱们如今已经拿到了编译后的全部字节码, 咱们要怎么去处理呢? 咱们能够了解下ASM
想要处理字节码, 常见的框架有AspectJ, Javasist, ASM. 关于框架的选型网上相关的文章仍是比较多的, 从处理速度以及内存占用率上, ASM明显优于其余两个框架.本篇主要着眼于ASM的使用.
ASM
是一个通用的Java字节码操做和分析框架。它能够用于修改现有类或直接以二进制形式动态生成类. ASM
提供了一些常见的字节码转换和分析算法,能够从中构建自定义复杂转换和代码分析工具. ASM库提供了两个用于生成和转换编译类的API:Core API
提供基于事件的类表示,而Tree API
提供基于对象的表示。因为基于事件的API(Core API)不须要在内存中存储一个表示该类的对象数, 因此从执行速度和内存占用上来讲, 它比基于对象的API(Tree API)更优.而后从使用场景上来讲, 基于事件的API使用会比基于对象的API使用更为困难, 譬如当咱们须要针对某个对象进行调整的时候.因为一个类只能被一种API管理, 因此咱们应该要区分场景选取使用对应的API
ASM的使用须要必定的学习成本, 咱们能够经过使用ASM Bytecode Outline
插件辅助了解, 对应插件在AS中的插件浏览器就能够找到
@RouteModule
public class ASMTest {
}
复制代码
Transform API
在应用工程方面的摸索使用Transform API
在组件化工程中有不少应用方向, 目前咱们项目中在自开发的路由框架中, 经过其去作了模块的自动化静态注册, 同时考虑到路由经过协议文档维护的不肯定性(页面路由地址的维护不及时致使对应开发没法及时更新对应代码), 咱们作了路由的常量管理, 首先经过扫描整个工程项目代码收集路由信息, 创建符合必定规则的路由原始基础信息文件, 经过variant#registerJavaGeneratingTask
注册 经过对应原始信息文件生成对应常量Java文件下沉在基础通用组件中的task, 这样上层依赖于这个基础组件的项目均可以经过直接调用常量来使用路由.在各组件代码隔离的状况下, 能够经过由组件aar传递原始信息文件, 仍然走上面的步骤生成对应的常量表, 而存在的类重复的问题, 经过自定义Transform
处理合并
在应用工程中, 咱们一般有关于网络监控,应用性能检测(包括页面加载时间, 甚至包括各个方法调用所耗时间, 可能存在超过阈值须要警告)的需求, 这些需求咱们都不可能嵌入在业务代码中, 都是能够基于Transform API
进行处理. 而针对于埋点, 咱们也能够经过Transform
实现自动化埋点的功能, 经过ASM Core
和ASM Tree
将尽量多的字段信息造成记录传递, 这里有些咱们项目中已经实现了, 有一些则是咱们须要去优化或者去实现的.
关于结合Transform+ASM
的使用, 我写了个一个小Demo, 包括了如何处理支持增量功能时的转换, 如何使用ASM Core Api
和ASM Tree Api
, 作了必定的封装, 能够参阅