概述
又开始了一个新的系列,这个系列学习Gradle,目标就是彻底理解Gradle,主要还是做下自己理解的笔记,防止忘记
Gradle学习系列(五):Gradle Transform
简介
Google从 Android Gradle 1.5.0开始提供了Transform API,Gradle Transform是Android 官方提供给开发者在项目构建阶段(class->dex期间)用来修改.class文件的一套标准API,把class文件转换为字节码,然后操作字节码,目前比较常用的就是,字节码插桩技术
Android的打包过程
通过上图我们可以知道,我们需要在红色箭头处通过Transform API拿到应用程序的class文件,然后借助AMS之类的库,对class文件中的方法进行遍历,然后找到我们需要改动的方法,修改目标方法,插入我们的代码保存,这就是字节码插桩技术
Transform相关方法介绍
实现一个Transform需要创建一个Gradle插件,至于插件相关可以看我之前的文章Gradle学习系列(三):Gradle插件
,这里就直接从Transform开始讲了
class RenxhTransform 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
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
}
}
可以看到如果实现一个Transform主要有四个方法,下面依次讲一下这四个方法
getName
返回transform的名称,一个应用内可以有多个transform,因此需要一个名称进行标识,方便后面调用
那么最终的名字是怎么组成的呢?
在gradle plugin 的源码中有一个TransformManager
的类,他的主要作用就是管理所有的Transform子类,里面一个方法getTaskNamePrefix
就是名字的规则
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
开头,然后拼接ContentType
这个下面详细讲,ContentType之间用add拼接,然后加上with
最后拼接getName
返回的name
getInputTypes
获取输入类型,ContentType表示类型,我们看下源码
enum DefaultContentType implements ContentType {
/**
* The content is compiled Java code. This can be in a Jar file or in a folder. If
* in a folder, it is expected to in sub-folders matching package names.
*/
CLASSES(0x01),
/** The content is standard Java resources. */
RESOURCES(0x02);
private final int value;
DefaultContentType(int value) {
this.value = value;
}
@Override
public int getValue() {
return value;
}
}
这里表示输入类型包括俩种,CLASSES 和 RESOURCES
分别代表java的class文件和资源文件
getScopes
这个是指Transform需要处理那些输入文件,官方文档一共有7中范围
- EXTERNAL_LIBRARIES : 只有外部库
- PROJECT : 只有项目内容
- PROJECT_LOCAL_DEPS : 只有项目的本地依赖(本地jar)
- PROVIDED_ONLY : 只提供本地或远程依赖项
- SUB_PROJECTS : 只有子项目
- SUB_PROJECTS_LOCAL_DEPS: 只有子项目的本地依赖项(本地jar)
- TESTED_CODE :由当前变量(包括依赖项)测试的代码
看下源码
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),
/** Code that is being tested by the current variant, including dependencies */
TESTED_CODE(0x20),
/** Local or remote dependencies that are provided-only */
PROVIDED_ONLY(0x40),
/**
* Only the project's local dependencies (local jars)
*
* @deprecated local dependencies are Now processed as {@link #EXTERNAL_LIBRARIES}
*/
@Deprecated
PROJECT_LOCAL_DEPS(0x02),
/**
* Only the sub-projects's local dependencies (local jars).
*
* @deprecated local dependencies are Now processed as {@link #EXTERNAL_LIBRARIES}
*/
@Deprecated
SUB_PROJECTS_LOCAL_DEPS(0x08);
private final int value;
Scope(int value) {
this.value = value;
}
@Override
public int getValue() {
return value;
}
}
如果范围越小,我们需要处理文件就越少,处理的速度就会越快
isIncremental
表示是否支持增量编译,一个自定义的Transform在可能的情况下,支持增量编译,可以节省一些编译的时间和资源
transform
transform方法的参数TransformInvocation是一个接口,提供一些关于输入的基本信息,利用这些接口就可以获得编译流程中的class文件进行操作
从上图可以看到,其实整体的处理流程还是很简单的,就是从TransformInvocation获取输入,然后按照class文件夹和jar集合进行遍历,拿到所有的class文件,进行处理
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
printcopyRight()
TransformOutputProvider transformOutputProvider = transformInvocation.getoutputProvider()
List<TransformInput> inputs= transformInvocation.getInputs()
transformInvocation.getInputs().each { TransformInput transformInput ->
transformInput.jarInputs.each { JarInput jarInput ->
println("jar=" + jarInput.name)
}
transformInput.directoryInputs.each { DirectoryInput directoryInput ->
directoryInput.getFile().eachFile { File file ->
printFile(file)
}
}
}
}
TransformInput
- DirectoryInput集合
指源码方式参与项目编译的所有目录结构以及其中的源码文件 - JarInput集合
是指以jar包的形式参与项目编译的所有的本地jar包和远程jar包(包括aar)
TransformOutputProvider
实战
第一步
首先我们要创建一个插件项目,至于插件相关可以看我之前的文章Gradle学习系列(三):Gradle插件
与上次相比,这次多了引入了 implementation 'com.android.tools.build:gradle:3.4.1'
因为这次需要用到android plugin中的api
第二步
创建文件RenxhTransform.groovy继承Transform
package com.renxh.cusplugin
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.gradle.api.Project
class RenxhTransform extends Transform {
Project mProject
RenxhTransform(Project project) {
this.mProject = project;
}
@Override
String getName() {
return "RenxhTransform"
}
/**
* 需要处理的数据类型,有两种枚举类型
* CLASSES 代表处理的 java 的 class 文件,RESOURCES 代表要处理 java 的资源
* @return
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
* 1. EXTERNAL_LIBRARIES 只有外部库
* 2. PROJECT 只有项目内容
* 3. PROJECT_LOCAL_DEPS 只有项目的本地依赖(本地jar)
* 4. PROVIDED_ONLY 只提供本地或远程依赖项
* 5. SUB_PROJECTS 只有子项目。
* 6. SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)。
* 7. TESTED_CODE 由当前变量(包括依赖项)测试的代码
* @return
*/
@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)
printcopyRight()
TransformOutputProvider transformOutputProvider = transformInvocation.getoutputProvider()
transformInvocation.getInputs().each { TransformInput transformInput ->
transformInput.jarInputs.each { JarInput jarInput ->
println("jar=" + jarInput.name)
processJarInput(jarInput, transformOutputProvider)
}
transformInput.directoryInputs.each { DirectoryInput directoryInput ->
if (directoryInput.file.isDirectory()) {
FileUtils.getAllFiles(directoryInput.file).each { File file ->
println(file.name)
}
}
processDirectoryInputs(directoryInput, transformOutputProvider)
}
}
}
static void printFile(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles()
files.each { File file1 ->
if (file1.isDirectory()) {
printFile(file1)
} else {
println("File = " + file.name)
}
}
} else {
println("File = " + file.name)
}
}
static void printcopyRight() {
println()
println("******************************************************************************")
println("****** ******")
println("****** 欢迎使用 RenxhTransform 编译插件 ******")
println("****** ******")
println("******************************************************************************")
println()
}
static void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
File dest = outputProvider.getContentLocation(
jarInput.getFile().getAbsolutePath(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR)
// to do some transform
// 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.copyFile(jarInput.getFile(),dest)
}
static void processDirectoryInputs(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY)
// 建立文件夹
FileUtils.mkdirs(dest)
// to do some transform
// 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.copyDirectory(directoryInput.getFile(),dest)
}
}
可以看到其实最主要的代码在transform()
方法中,上面主要是打印了我们自定义的文本,然后分别遍历directory和jar
包然后打印出他们的名字,这里没有对文件进行处理,最后把输入文件拷贝到目标目录下,需要注意的是即使我们没有对文件做任何处理,我们仍然需要把输入文件拷贝到目标目录下,否则下一个Task就没有TansformInput,如果我们将input目录复制到output指定目录,会导致最后的打包的apk缺少class
第三步
class CustomPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
AppExtension appExtension = project.getExtensions().findByType(AppExtension.class)
appExtension.registerTransform(new RenxhTransform(project))
}
app 的 build.gradle 里我们通常会采用插件 apply plugin: ‘com.android.application’ ,而在 library module 中则采用插件 apply plugin: ‘com.android.library’,我们看下Gradle的源码com.android.application
插件对应的实现类是AppPlugin
类
AppPlugin
中添加Extension
就是AppExtension
,源码如下,主要看createExtension
public class AppPlugin extends BasePlugin implements Plugin<Project> {
@Inject
public AppPlugin(Instantiator instantiator, ToolingModelBuilderRegistry registry) {
super(instantiator, registry);
}
protected BaseExtension createExtension(Project project, ProjectOptions projectOptions, Instantiator instantiator, AndroidBuilder androidBuilder, SdkHandler sdkHandler, NamedDomainObjectContainer<BuildType> buildTypeContainer, NamedDomainObjectContainer<ProductFlavor> productFlavorContainer, NamedDomainObjectContainer<SigningConfig> signingConfigContainer, NamedDomainObjectContainer<BaseVariantOutput> buildOutputs, ExtraModelInfo extraModelInfo) {
return (BaseExtension)project.getExtensions().create("android", AppExtension.class, new Object[]{project, projectOptions, instantiator, androidBuilder, sdkHandler, buildTypeContainer, productFlavorContainer, signingConfigContainer, buildOutputs, extraModelInfo});
}
public void apply(Project project) {
super.apply(project);
}
//省略...
}
这里添加了android
的Extension
实现类是AppExtension
,我们的transform最后添加到了AppExtension
的基类BaseExtension
的容器中了
看下效果
执行 终端执行./gradlew app:assemble
命令
已经达到想要的效果
参考
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。