Android Transform

0 阅读7分钟

1. Android Transform介绍

1.1 APK构建过程

共分为两个过程:编译、打包

  • 编译过程:编译的内容包括工程的文件以及依赖的各种库文件,编译的输出包括dex文件和编译后的资源文件

  • 打包过程:配合Keystore对第一步的输出进行签名对齐,生成最终的apk文件

下面这张图对上面的步骤以及每步用到的工具进行了细分:

  • Java编译器对工程本身的java代码进行编译

    • 输入:app的源代码、由资源文件生成的R文件(aapt工具),以及有aidl文件生成的java接口文件(aidl工具)

    • 产出:.class文件

  • dex工具对class文件和依赖的三方库文件,生成Delvik虚拟机可执行的.dex文件,可能有一个或多个,包含了所有的class信息,包括项目自身的class和依赖的class

    • 输入:.class文件、依赖的三方库文件

    • 产出:.dex文件

  • apkbuilder工具将.dex文件和编译后的资源文件生成未经签名对齐的apk文件。

    • 输入:.dex文件和编译后的资源文件(一是由aapt编译产生的编译后的资源文件,二是依赖的三方库里的资源文件)

    • 产出:未经签名的.apk文件

  • 分别由Jarsigner和zipalign对apk文件进行签名和对齐,生成最终的apk文件

1.2 Transform

介绍

从android-build-tool:gradle:1.5开始,gradle插件包含了一个叫Transform的API,这个API允许第三方插件在class文件转为为dex文件前操作编译好的class文件,这个API的目标就是简化class文件的自定义的操作而不用对Task进行处理。

作用域

.class编译后,打包成.dex前,对.class和resource进行再处理

作用

在【生成class文件之后,dex文件之前】 拦截,可获取当前应用程序中所有的.class文件,再借助ASM等库,遍历这些.class文件中所有方法,再根据一定的条件找到需要的目标方法,最后进行修改并保存,插入目标代码。

1.3 类介绍

Transform

public class BytecodeTransform extends Transform {

    //指明 Transform 的名字
    @Override
    public String getName() {
        return "customTransform";
    }

    //指明 Transform 处理的输入类型
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return Collections.singleton(QualifiedContent.DefaultContentType.CLASSES);
    }

    // Transform 输入文件所属的范围
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return EnumSet.of(
                QualifiedContent.Scope.PROJECT,         // 主工程代码
                QualifiedContent.Scope.SUB_PROJECTS,    // 子模块代码
                QualifiedContent.Scope.EXTERNAL_LIBRARIES // 第三方依赖库
        );
    }

    // 是否支持增量编译
    @Override
    public boolean isIncremental() {
        return true;
    }

    @Override
    public void transform(TransformInvocation invocation) {
        // 核心处理逻辑
        for (TransformInput input : invocation.getInputs()) {
            // 处理JAR输入
            for (JarInput jarInput : input.getJarInputs()) {
                System.out.println("处理JAR: " + jarInput.getName());
            }
            // 处理目录输入
            for (DirectoryInput dirInput : input.getDirectoryInputs()) {
                System.out.println("处理目录: " + dirInput.getName());
            }
        }
    }
}

getName

指明 Transform 的名字,对应该 Transform 所代表的 Task 名称,例如当返回值为 InjectTransform 时,编译后可以看到名为transformClassesWithInjectTransformForxxx 的 task

getInputTypes

指明 Transform 处理的输入类型

旧版使用的是TransformManager 中定义的类型,AGP=7.4.2,TransformManager已经被移除,可使用QualifiedContent进行替换

//Class文件
QualifiedContent.DefaultContentType.CLASSES
//RESOURCES文件
QualifiedContent.DefaultContentType.RESOURCES

getScopes

指明 Transform 输入文件所属的范围, 因为 gradle 是支持多工程编译的

enum Scope implements ScopeType {
   /** 仅包含项目(模块)内容 */
    PROJECT(0x01),
    /** 仅包含子项目(其他模块) */
    SUB_PROJECTS(0x04),
    /** 仅包含外部库 */
    EXTERNAL_LIBRARIES(0x10),
    /** 当前构建变体所测试的代码,包括其依赖项 */
    TESTED_CODE(0x20),
    /** 仅为 “提供”(provided)类型的本地或远程依赖项 */
    PROVIDED_ONLY(0x40),

    /*** 仅包含该项目的本地依赖项(本地的 JAR 包) 
    @已过时 本地依赖项现在按 “外部库” 进行处理*/
    @Deprecated
    PROJECT_LOCAL_DEPS(0x02),
    /**仅包含子项目的本地依赖项(本地的 JAR 包)
    @已过时 现在本地依赖项将作为 (外部库)来处理*/
    @Deprecated
    SUB_PROJECTS_LOCAL_DEPS(0x08);

    private final int value;

    Scope(int value) {
        this.value = value;
    }

    @Override
    public int getValue() {
        return value;
    }
}

isIncremental

是否支持增量编译

TransformInvocation

@Deprecated
public interface TransformInvocation {

    // 上下文
    @NonNull
    Context getContext();

     // transform 的输入/输出
    @NonNull
    Collection<TransformInput> getInputs();

    // 返回不被这个 transformation 消费的 input
    @NonNull Collection<TransformInput> getReferencedInputs();
    /**
    返回自上次以来二级文件的变更列表。只有本转换能够以增量方式处理的二级文件才会包含在这个变更集中。
    @return 影响到 {@link SecondaryInput}(二级输入)的变更列表
    */
    @NonNull Collection<SecondaryInput> getSecondaryInputs();

    // 返回允许创建内容的 output provider
    @Nullable
    TransformOutputProvider getOutputProvider();

    /**
    指示转换执行是否为增量式的。
    @return 如果是增量式调用则返回 true,否则返回 false。
    */
    boolean isIncremental();
}

TransformInput

@Deprecated
public interface TransformInput {

    // 表示 Jar 包
    @NonNull
    Collection<JarInput> getJarInputs();

    //返回一个目录输入的集合
    @NonNull
    Collection<DirectoryInput> getDirectoryInputs();
}

TransformOutputProvider

@Deprecated
public interface TransformOutputProvider {

    /**
    删除所有内容。这在非增量模式下运行时很有用。
    @throws IOException 如果删除输出操作失败,则抛出此异常。
    */
    void deleteAll() throws IOException;

    // 根据 name、ContentType、QualifiedContent.Scope 返回对应的文件(jar / directory)
    @NonNull
    File getContentLocation(
            @NonNull String name,
            @NonNull Set<QualifiedContent.ContentType> types,
            @NonNull Set<? super QualifiedContent.Scope> scopes,
            @NonNull Format format);
}

2. Gradle自定义插件

2.1 方法一:build.gradle

class MyGradlePlugin : Plugin<Project> {
    override fun apply(target: Project) {
        println("Hello, Gradle!")
    }
}

apply<MyGradlePlugin>()

2.2 方法二:buildSrc

新建文件及目录

不要使用android studio中自带的创建模块的功能

新建buildSrc文件夹,目录层级如下

buildSrc/
  ├── build.gradle.kts (或 build.gradle)
  └── src/
      └── main/
          ├── groovy/ (或 java/)
          └── resources/
              └── META-INF/
                  └── gradle-plugins/
                      └── com.example.plugin.properties

配置build.gradle文件

plugins {
    id 'groovy'
    id 'maven-publish'
}

dependencies {
    implementation gradleApi()      // 引入 Gradle API
    implementation localGroovy()    // Groovy SDK
}

repositories {
    mavenCentral()
    google()
}

编写插件类

package com.example

import org.gradle.api.Plugin
import org.gradle.api.Project

class CustomPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.tasks.register('customTask') {
            doLast {
                println("Hello from Custom Plugin!")
            }
        }
    }
}

声明插件ID

在resources/META-INF/gradle-plugin下创建.properties文件

implementation-class=com.example.MyCustomPlugin

使用

// 方式一:传统语法
apply plugin: 'com.example.plugin'

// 方式二:plugins DSL(需兼容配置)
plugins {
    id 'com.example.plugin'
}

执行任务

./gradlew customTask

2.3 方法三:独立项目

新建模块

构建目录层级

src/main
├── groovy    # 插件源码目录
└── resources/META-INF/gradle-plugins  # 插件声明文件

配置插件类

package com.example

import org.gradle.api.Plugin
import org.gradle.api.Project

class MyCustomPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.tasks.register('customTask') {
            doLast {
                println("Hello from Custom Plugin!")
            }
        }
    }
}

创建.properties文件

implementation-class=com.example.MyCustomPlugin

配置构建脚本

在自定义gradle模块的build.gradle中添加如下信息

plugins {
    id 'groovy'
    id 'maven-publish'
}

dependencies {
    implementation gradleApi()           // 依赖Gradle API
    implementation localGroovy()         // Groovy SDK
}

// 发布到本地Maven仓库
publishing {
    publications {
        mavenJava(MavenPublication) {
            groupId = 'com.example.plugin'
            artifactId = 'com.example.plugin.gradle.plugin'
            version = '1.0.0'
            from components.java
        }
    }
    repositories {
        maven { url = uri("../myPlugin/repo") }  // 本地仓库路径
    }
}

Gradle要求插件ID与Maven坐标的转换格式:

plugin.id → groupId:plugin.id.gradle.plugin:version

因此如后续在 plugin 模块通过id的方式引用插件

需要注意 artifactId 的命名

发布插件

//执行Gradle任务 生成的插件将存储在指定的目录下
./gradlew publish

在项目build.gradle中插入本地仓库的地址

buildscript {
    repositories {
        maven {
            url uri("file:///D:/ASFiles/GradleLearning/myPlugin/repo")
        }
    }
    dependencies {
        classpath 'com.example.plugin:com.example.plugin.gradle.plugin:1.0.0'
    }
}

在目标模块中使用

//方案1:使用apply plugin
buildscript {
    repositories {
        maven { url = uri("../myPlugin/repo") }
    }
    dependencies {
        classpath 'com.example:myplugin:1.0.0'
    }
}
apply plugin: 'com.example.myplugin'

//方案2:使用plugin id
plugins {
    id 'com.example.plugin'
}

3. 编写transform demo

前提

  1. 本次采用自定义插件方法三

  2. gradle 版本为7.5 AGP版本为7.4.2时,appcompat的版本应该为1.6.1 避免出现编译问题,如下截图

新建BytecodeTransform继承Transform

public class BytecodeTransform extends Transform {

    //指明 Transform 的名字
    @Override
    public String getName() {
        return "customTransform";
    }

    //指明 Transform 处理的输入类型
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return Collections.singleton(QualifiedContent.DefaultContentType.CLASSES);
    }

    // Transform 输入文件所属的范围
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return EnumSet.of(
                QualifiedContent.Scope.PROJECT,         // 主工程代码
                QualifiedContent.Scope.SUB_PROJECTS,    // 子模块代码
                QualifiedContent.Scope.EXTERNAL_LIBRARIES // 第三方依赖库
        );
    }

    // 是否支持增量编译
    @Override
    public boolean isIncremental() {
        return true;
    }

    @Override
    public void transform(TransformInvocation invocation) {
        // 核心处理逻辑
        for (TransformInput input : invocation.getInputs()) {
            // 处理JAR输入
            for (JarInput jarInput : input.getJarInputs()) {
                System.out.println("处理JAR: " + jarInput.getName());
            }
            // 处理目录输入
            for (DirectoryInput dirInput : input.getDirectoryInputs()) {
                System.out.println("处理JAR: " + dirInput.getName());
            }
        }
    }
}

在插件入口引用transform

class MyCustomPlugin implements Plugin<Project> {
    void apply(Project project) {
        AppExtension android = project.extensions.findByType(AppExtension)
        if (android != null) {
            android.registerTransform(new BytecodeTransform())
        }
    }
}

发布插件

//执行Gradle任务 生成的插件将存储在指定的目录下
./gradlew publish

在目标模块中使用

//方案2:使用plugin id
plugins {
    id 'com.example.plugin'
}

运行app

在Build的控制台可以看到打印输出,成功调用transform

或直接运行Transform对应的Task

 ./gradlew transformClassesWithCustomTransformForDebug

可以在控制台看到打印信息,成功引入

4. 补充记录

  1. 是 AGP 8.x 为平滑过渡提供的 临时解决方案,适用于紧急兼容旧插件,但长期需迁移至新 API 以提升构建性能和稳定性。
android {
    useAgp8TransformShim = true // 在 gradle.properties 添加
}
  • gradle7.0+移除了transformManager 使用QualifiedContent代替

juejin.cn/post/688146…

juejin.cn/post/688146…

juejin.cn/post/691448…