Android Gradle 中的Transform
Posted 好人静
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Gradle 中的Transform相关的知识,希望对你有一定的参考价值。
目录
前言
逐步整理的一系列的总结:
Android 自定义Gradle插件的Extension类(五)
android Gradle 中的Transform(六)
一个APP从代码到最后生成一个可用的apk程序包,其实需要经历编译、打包、apk生成内容以及最后签名的整个过程。前面不管是在 Android 自定义Gradle插件的完整流程(三)中的二 Task,还是在 Android 自定义Task添加到任务队列(四)中定义的二 在自定义插件中添加Task都是添加在APP的编译过程通过添加自定义Task来增加一些任务功能。
那如果需要在class文件转换成dex之前,对class文件进行处理,例如:
- (1)对全局的class进行字节码插桩:例如UI、内存、网络等的性能监控;埋点功能(如用户页面的访问、点击事件、每个方法的耗时统计)
- (3)全局log日志:自动生成TAG,例如默认为当前类的名称;自动添加当前代码的行数
那能不能有方法来解决这个问题呢?这次主要就是探索和了解下这个过程。像web开发中的ORM框架(如MyBatis)都是对.class文件的字节码进行修改。
遗留问题:结合前期看的MyBatis的相关内容,在去详细了解下。
一 APP打包流程
1.概要
一个Android项目从代码到安装到手机上,大致经历编译、打包、apk生成以及签名的过程。通过Android Studio的Build的Analyze APK来查看一个APK的内容如下:
- (1)一个或多个dex文件
- (2)resources.arsc
- (3)未编译的资源文件
- (4)AndroidManifest.xml文件
生成APK文件之后,再通过ADB将签名之后的APK安装到手机上。这里又让自己明白了平时在build.gradle中的signingConfigs{}进行配置,那么通过Android Studio进行运行之后的APK就会带着签名信息。
signingConfigs {
debug {
storeFile file("debug.keystore")
storePassword "xxxx"
keyAlias "xxxx"
keyPassword "xxxx"
}
release {
storeFile file("release.keystore")
storePassword "xxxx"
keyAlias "xxxx"
keyPassword "xxxx"
}
}
遗留问题:再详细的分析下(2)和(3)
2.详细的打包流程
根据官网提供的打包流程图来具体分析一下前面的过程。
- (1) res目录下的可被编译的资源文件(如layout、values等)和AndroidManifest.xml经过aapt工具生成对应的R.java文件和resouce.arsc文件。
- R.java文件是在应用层可以直接通过resource id进行访问;而resouce.arsc是apk在运行时,dalivk虚拟机用来识别的资源表;
- R.java文件只是resource id列表,而resource.arsc文件会将这些resource id进行组装,在apk运行的时候,根据设备的情况采用不同的资源;
- 另一部分不可编译的需要直接通过文件名进行访问的资源文件(assets、res/raw)就会直接通过和.dex文件打包到apk中
- (2)aidl文件通过aidl工具生成对应的java文件。对应的任务队列中的“Task :app:compileDebugAidl NO-SOURCE”;
- (3)将R.java文件、aidl文件以及项目的源代码经过Java编译器编译成.class文件;
- (4)通过dx工具(主要将Java字节码转换成Dalivk字节码、压缩常量池、消除冗余信息)生成运行apk的环境dalivk虚拟机可以执行的.dex文件,这里包括第三方的libraries以及.class文件。
- (5)不可编译的资源文件、.dex文件通过apkbuilder工具直接打包到apk中
- (6)通过jarsinger工具对apk进行签名,通常可根据debug还是release设置两种签名的keystore
- (7)使用zipalign工具对apk中的所有资源文件对齐处理(遗留问题:这个需要了解下)
上面这些过程都是在Android Studio的build窗口中通过task任务来完成对应的工作。而这次探索的Transform就是在第(4)过程将.class文件转换成.dex文件的过程。
二 Android Transform的几个重要方法
Android Gradle提供了Transform来对由.class文件转换成.dex文件进行字节码查找、代码注入等操作。每个Transform都是一个task,TaskManager会将每个Transform在处理完之后会交给下一个Transform。先大体了解一点Transform。
Transform是一个抽象类。所以要想自定义一个Transform,就要继承该类。
public abstract class Transform {
}
主要了解几个重要方法的含义:
1.getName()
返回的内容即为指定该Transform的名字
@Override
public String getName() {
return "HotTransformTask";
}
在任务队列输出的时候如下:
> Task :app:transformClassesWithHotTransformTaskForDebug
而这个Task的名字组成是由“transform+ContentType+With+transform名字+TaskFor+buildType+productFlavor”组成的。 这个transform名字就是我们通过getName()设置的名字。ContentType这个就是下面要介绍的一个方法设置的内容。
2.getInputTypes()
返回的内容即为指定该Transform要处理的数据类型,即该Transform的输入文件的类型
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
在TransformManager中有下面几种类型:
/**要处理的数据类型是java class文件,并且包含jar文件*/
public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
/**要处理的数据类型是jar文件*/
public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
/**要处理的数据类型是java resource文件*/
public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
/**要处理的数据类型是dex文件*/
public static final Set<ContentType> CONTENT_DEX = ImmutableSet.of(ExtendedContentType.DEX);
/**要处理的数据类型是dex文件、java resource文件*/
public static final Set<ContentType> CONTENT_DEX_WITH_RESOURCES =
ImmutableSet.of(ExtendedContentType.DEX, RESOURCES);
一般getInputTypes()返回的是CONTENT_CLASS。
3.getScopes()
返回的内容即指定该Transform的修改input文件的作用域
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
在TransformManager中有下面几种类型:
/**仅仅当前工程*/
public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT);
/**整个项目工程+外部library库*/
public static final Set<ScopeType> SCOPE_FULL_PROJECT =
ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES);
/***/
public static final Set<ScopeType> SCOPE_FULL_WITH_FEATURES =
new ImmutableSet.Builder<ScopeType>()
.addAll(SCOPE_FULL_PROJECT)
.add(InternalScope.FEATURES)
.build();
public static final Set<ScopeType> SCOPE_FEATURES = ImmutableSet.of(InternalScope.FEATURES);
public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS =
ImmutableSet.of(Scope.PROJECT, InternalScope.LOCAL_DEPS);
public static final Set<ScopeType> SCOPE_FULL_PROJECT_WITH_LOCAL_JARS =
new ImmutableSet.Builder<ScopeType>()
.addAll(SCOPE_FULL_PROJECT)
.add(InternalScope.LOCAL_DEPS)
.build();
一般使用的是 SCOPE_FULL_PROJECT。
通过getInputTypes() 和getScopes()就设置好了需要处理的设置为输入的对应的class的字节码,当复写transform()的时候,如果不进行任何处理,那么将无法生成.dex文件,在最后打包之后的apk文件中无.dex文件,如图:
4.getReferencedScopes()
返回的内容即指定该Transform的查看input文件的作用域
@Override
public Set<? super QualifiedContent.Scope> getReferencedScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.EMPTY_SCOPES;
}
getReferencedScopes()区别于getScopes(),复写transform()并不会覆盖Android原来的.class文件转换成dex文件的过程。该方法主要用来该自定义的Transform并不想处理任何input文件的内容,仅仅只是想查看input文件的内容的作用域范围。
从源码注解中可以发现:
所以要实现只查看input文件的内容,设置getReferencedScopes()的作用域范围,同时需要将getScopes()返回一个空集合,如上代码所示。这样在transform()可以查看该.class文件转换成dex文件的过程,不改变原来Android的打包apk的逻辑。
5.isIncremental()
返回的内容即指定该Transform的是否进行增量编译
@Override
public boolean isIncremental() {
return false;
}
当开启增量编译之后,对应着input的四种状态(changed/added/removed/notchanged)要完成不同的操作。当然返回false,则进行全量编译,每次删除上一次编译的内容。
6.transform()
完成字节码的修改、处理等操作。
先了解下怎么查看input的相关内容。后续在详细研究下,下面只是在transform()中输出了input的内容。
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
//如果不带super,就不会生成dex文件
super.transform(transformInvocation);
Collection<TransformInput> inputs = transformInvocation.getReferencedInputs();
for (TransformInput input : inputs) {
//返回的是ImmutableJarInput
for (JarInput jar : input.getJarInputs()) {
SystemOutPrint.println("jar file = " + jar.getFile());
}
//返回的是ImmutableDirectoryInput
for (DirectoryInput directory : input.getDirectoryInputs()) {
SystemOutPrint.println("directory file = " + directory.getFile());
}
}
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
SystemOutPrint.println("output = " + outputProvider);
}
Android Gradle已经将input和output打包成一个TransformInvocation对象,其中可以通过下面的方法获取对应的内容:
public interface TransformInvocation {
/**
* 返回正在处理哪个Context,该Context包含项目名称、路径等信息
*/
Context getContext();
/**
* 返回通过getScope()设置的所有的input
*/
Collection<TransformInput> getInputs();
/**
* 返回通过getReferencedScopes()设置的referenced-only 的input
*/
Collection<TransformInput> getReferencedInputs();
/**
* 返回secondaryInputs
*/
@NonNull Collection<SecondaryInput> getSecondaryInputs();
/**
* 可以设置output的相关内容
*/
TransformOutputProvider getOutputProvider();
/**
* 返回是否是增量编译
*/
boolean isIncremental();
}
- (1)getInputs()和 getReferencedInputs()就是返回的是input的相关内容,被封装为TransformInput,里面包含jar相关的JarInput集合以及和其他文件相关的DirectoryInput集合;
- (2)通过getOutputProvider()来操作output相关内容。output的内容不是任意指定,而是根据input的内容和设置的scopes等有TransformOutputProvider生成。
这样就可以获取到需要处理的.class文件,然后对这些文件进行自定义处理。
7.registerTransform()
注册Transform。
在前面的多次提到,当我们通过继承DefaultTask添加的一个task的时候,都需要通过依赖已有task的方式将task队列中,那么对于自定义Transform同样也是需要进行注册,Android Gradle提供了相应的API进行注册,注册之后自动会插在Transform队列的最前面。
class FirstPluginProject implements Plugin<Project> {
@Override
void apply(Project project) {
......
project.extensions.findByType(BaseExtension.class).registerTransform(new HotTransform())
}
这里findByType()既可以是BaseExtension.class,也可以是AppExtension.class,这个AppExtension继承BaseExtension。
在Android Studio中运行之后,就可以在Build的输出窗口中,输出相应的内容:
> Task :app:validateSigningDebug
> Task :app:mergeDebugResources
> Task :app:writeDebugAppMetadata
> Task :app:writeDebugSigningConfigVersions
> Task :app:processDebugManifestForPackage
> Task :app:mergeDebugNativeLibs
> Task :app:mergeDebugJavaResource
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:processDebugResources
> Task :app:compileDebugJavaWithJavac
> Task :app:compileDebugSources
> Task :app:dexBuilderDebug
> Task :app:mergeLibDexDebug
> Task :app:transformClassesWithHotTransformTaskForDebug
%%%%%%%%% FirstPluginProject %%%%%%%%% context project name = appcontext project name = :app:transformClassesWithHotTransformTaskForDebug , isIncremental = false
%%%%%%%%% FirstPluginProject %%%%%%%%% jar file = /Users/j1/.gradle/caches/transforms-2/files-2.1/acfe271f2f2894a7773f8e05b54882dc/material-1.2.1-runtime.jar
//.......输出内容比较多,省略
%%%%%%%%% FirstPluginProject %%%%%%%%% directory file = /Users/j1/Documents/android/code/AndroidPlugin/app/build/intermediates/javac/debug/classes
> Task :app:mergeProjectDexDebug
> Task :app:mergeExtDexDebug
具体的代码已经上传到至https://github.com/wenjing-bonnie/AndroidPlugin.git的firstplugin目录的相关内容。因为逐渐在这基础上进行迭代,可回退到Gradle_6.0.1该tag下可以查看相关内容。
三 Android Transform高级应用
前面也提到了每个Transform都是一个gradle中task,而Android Gradle中的TaskMananger将每个Transform串联到一起。有两种方式的Transform:
- (1)消费型:当前Transform需要将消费型输出给下一个Transform,每个Transform节点都可以对class进行处理之后在传递给下一个Transform。通过getScopes()设置的就是消费型输入,需要将输出给下一个任务,此时获取的outputProvider不为null;
- (2)引用型:当前Transform可以读取这些输入,而不需要输出给下一个Transform。 通过getReferencedScopes()设置的为引用型输入,此时获取的outputProvider不为null。
前面在二 Android Transform的几个重要方法中提到的例子,仅仅是一个引用型的Transform,只是简单的利用Transform输出了input的内容,那么消费型的Transform才用来进行对.class文件进行修改,那修改之后怎么生成对应的.dex文件,然后打包到apk包里面呢?
对于消费型的Transform,最重要的就是需要将input内容通过FileUtils.copyFile()或FileUtils.copyDirectory()拷贝到output目录中,否则生成的apk会无法安装到手机。
相比较于二 Android Transform的几个重要方法中提到的例子,修改内容如下:
- (1)设置消费型input的作用域
/**
* 需要操作的内容范围
*
* @return
*/
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
//仅仅用来查看input文件
//return TransformManager.EMPTY_SCOPES;
return TransformManager.SCOPE_FULL_PROJECT;
}
/**
* 仅仅用来设置查看input文件的作用域
*/
// @Override
// public Set<? super QualifiedContent.Scope> getReferencedScopes() {
// return TransformManager.SCOPE_FULL_PROJECT;
// }
若不复写 getReferencedScopes(),在父类中默认的已经返回TransformManager.EMPTY_SCOPES。
- (2)重写transform()
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
//如果不带super,就不会生成dex文件
super.transform(transformInvocation);
SystemOutPrint.println("context project name = " + transformInvocation.getContext().getProjectName()
+ "context project name = " + transformInvocation.getContext().getPath()
+ " , isIncremental = " + transformInvocation.isIncremental());
//现在进行处理.class文件:消费型输入,需要输出给下一个任务
Collection<TransformInput> inputs = transformInvocation.getInputs();
//仅仅用来查看input文件:引用型输入,无需输出,此时outputProvider为null
//Collection<TransformInput> inputs = transformInvocation.getReferencedInputs();
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
for (TransformInput input : inputs) {
//返回的是ImmutableJarInput。
for (JarInput jar : input.getJarInputs()) {
SystemOutPrint.println("jar file = " + jar.getFile());
//TODO 在这里增加处理.jar文件的代码
//获取Transforms的输出目录
File dest = outputProvider.getContentLocation(jar.getFile().getAbsolutePath(), jar.getContentTypes(), jar.getScopes(), Format.JAR);
//将修改之后的文件拷贝到对应outputProvider的目录中
FileUtils.copyFile(jar.getFile(), dest);
}
//返回的是ImmutableDirectoryInput
for (DirectoryInput directory : input.getDirectoryInputs()) {
SystemOutPrint.println("directory file = " + directory.getFile());
//TODO 在这里增加处理.class文件的代码
//获取Transforms的输出目录
File dest = outputProvider.getContentLocation(directory.getName(), directory.getContentTypes(), directory.getScopes(), Format.DIRECTORY);
//将修改之后的文件拷贝到对应outputProvider的目录中
FileUtils.copyDirectory(directory.getFile(), dest);
}
}
}
这样就剩下只需要在TODO备注的地方增加对.jar文件或.class文件进行修改 的代码,最后将修改之后的文件拷贝到对应的output的目录中即可。
这里要注意拷贝directory一定要用FileUtils.copyDirectory(),拷贝File一定要用FileUtils.copyFile(),一开始由于将FileUtils.copyDirectory()误用成了FileUtils.copyFile()导致抛出下面异常:
将插件进行打包发布,通过Android Studio 运行项目,发现该apk已经成功运行在手机上。具体的代码已经上传到至https://github.com/wenjing-bonnie/AndroidPlugin.git的firstplugin目录的相关内容。因为逐渐在这基础上进行迭代,可回退到Gradle_6.0.2该tag下可以查看相关内容。
四 总结
这两天终于解决了一开始自己在复写transform(),最后生成的apk无法运行在手机上面的问题了,太开心了。总结下
- 1.一个apk从源码工程到安装到手机,大体会经过编译、打包、apk生成以及签名、adb安装过程;
- 2.一个apk的详细打包流程简单总结:
- (1)资源文件(除去assets以及res/raw)经过aapt工具被成R.java文件,放到resources.arsc文件中,供Dalivk虚拟机识别;
- (2)剩余的资源文件(即assets以及res/raw)会直接最后和.dex文件以及其他java resouce文件被打包到apk中;
- (3).aidl文件会经过aidl工具生成对应的java文件
- (4)R.java文件、源代码以及aidl生成的java文件经过java编译器生成.class文件;
- (5).class文件和第三方的library库以及.class文件经过dex工具被打包成.dex文件,而这个Android Transform的过程就发生在这里;
- (6).dex文件、不可编译的资源文件以及其他资源文件经过apkBuilder被打包到.apk文件中,供Dalivk虚拟机识别运行;
- (7).apk文件经过jarsinger签名、以及zipalign对齐最后生成一个可以运行在手机的apk
- 3.Android Transform其实就是一个task,TransformManger会把所有的Transform串成一个链,一般自定义的Transform会自动添加到Transform链的最前面;
- 4.Transform分为消费型的Transform和引用型的Transform;
- 5.消费型的Transform必须通过getScopes()设置input的作用域以及将对应的input拷贝到output目录中;
- 6.引用型的Transform通过getReferencedScopes()设置的引用input的作用域,并且还得设置getScopes()的返回集合为空,此时output为空,无需执行拷贝操作。
后面尝试下对input文件进行修改,然后给每个页面增加打点事件,加油!!!越来越有意思了
以上是关于Android Gradle 中的Transform的主要内容,如果未能解决你的问题,请参考以下文章
gradle build 未检测到 build.gradle 中的 android ndkVersion
Android Gradle 插件Gradle 依赖管理 ② ( build.gradle 中的 dependencies 依赖配置 | DependencyHandler#add 方法介绍 )
Gradle,Gradle 包装器在 Android Studio 中的不同实例 by ionic cordova
Android Gradle 插件Gradle 构建机制 ① ( 空白工程 Gradle 构建文件 | IntelliJ IDEA 工程构建文件 | Android Studio 工程构建文件 )
android/flutter 中的 settings_aar.gradle 有啥用?
Android Gradle 插件组件化中的 Gradle 构建脚本实现 ⑤ ( 优化 Gradle 构建脚本 | 构建脚本结构 | 闭包定义及用法 | 依赖配置 | android 块配置 )