一文了解 AGP8 的使用

AGP 是 Android Gradle Plugin 的简称。AGP 的主要主要的用于实现 Android 项目的构建。当我们执行 assemble 命令时,会有如下图的任务执行,这些任务就是 AGP 提供的。

除此之外,AGP 还提供了扩展接口,让我们可以扩展构建流程。这篇文章就将介绍如何使用 AGP。

Android studio 下载agp源码

scss 复制代码
implementation("com.android.tools.build:gradle:8.6.1")

AGP 的基本使用

使用 AGP 有一个基本的结构, 如下代码所示:

kotlin 复制代码
//定义插件
class CustomPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // 在Android应用程序插件上注册回调。
        // 这让 CustomPlugin 无论是应用在Android应用程序插件
        // 之前还是之后都可以正常工作,
        project.plugins.withType(AppPlugin::class.java) {
            // 获取由 Android 应用插件设置的扩展对象
            val androidComponents =
                project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
            androidComponents.onVariants { variant ->
                // 通过variant.sources.* 可以访问各种文件
                // 下面是创建了asset目录,并且在其中创建了txt文件
                variant.sources.assets?.let {
                    val assetCreationTask =
                        project.tasks.register<AssetCreatorTask>("create${variant.name}Asset")

                    it.addGeneratedSourceDirectory(
                        assetCreationTask,
                        AssetCreatorTask::outputDirectory
                    )
                }
            }
        }
    }
}

可以看到,不同于传统的 Gradle Task (关于 Gradle Task 具体可看一文了解Gradle 的Task),AGP的扩展不使用 dependsOn finalizedBy mustRunAfter shouldRunAfter 来显式定义任务的执行顺序;也不需要注册Gradle 生命周期回调(如 afterEvaluate())。而是通过使用 AGP 提供的回调(比如这里的 onVariants)来修改构建过程中创建的特定对象。

AGP 的脚本解析和创建流程

AGP 的脚本解析和创建流程如下所示:(注意,下面的流程都是在Gradle的配置阶段执行的)

  1. DSL 解析(DSL parsing):此时会解析构建脚本,并创建和设置 android 块中各类 Android DSL 对象的属性。下文所述的变体 API 回调也会在此阶段注册。
  2. finalizeDsl() 回调:该回调允许你在 DSL 对象被锁定以用于组件(变体)创建前修改它们。变体构建器(VariantBuilder)对象会基于 DSL 对象中包含的数据创建。
  3. DSL 锁定(DSL locking):此时 DSL 已锁定,无法再修改。
  4. beforeVariants() 回调:在构建的此阶段,你可以访问 VariantBuilder 对象,这些对象决定了将要创建的变体及其属性。例如,你可以通过编程方式禁用某些变体、其测试,或仅为特定变体修改属性值(如 minSdk)。与 finalizeDsl() 类似,你提供的所有值必须在配置阶段解析,且不能依赖外部输入。beforeVariants() 回调执行完成后,VariantBuilder 对象不得再被修改。
  5. 变体创建(Variant creation):此时将创建的组件和产物列表已确定,无法再更改。
  6. onVariants() 回调:调用 onVariants() 时,AGP 将要创建的所有产物已确定,因此你不能再禁用它们。不过,你可以通过为变体对象中的 Property 属性设置值,来修改用于任务的某些参数。由于 Property 值仅在 AGP 任务执行时才会解析,因此你可以安全地将其与自定义任务的提供者(provider)关联 ------ 这些自定义任务将执行所需的计算,包括读取外部输入(如文件或网络数据)。
  7. 变体锁定(Variant locking):此时变体对象已锁定,无法再修改。
  8. 任务创建(Tasks created):变体对象及其 Property 值会被用于创建执行构建所需的任务实例。

上面的内容了解即可,我们主要关心的只有三个回调:finalizeDsl() 回调beforeVariants() 回调onVariants() 回调 。它们的执行时机是每个 build.gradle 脚本都解析完成后,并在 gradle.projectsEvaluated 回调之前。如下图所示:

finalizeDsl() 回调

finalizeDsl() 回调 的作用是对 build.gradle 中的 android{} 属性进行修改。如下所示,使用 finalizeDsl() 回调 创建 finalizeDslTest 的构建类型(buildType),并获取 installation 的相关属性。

csharp 复制代码
android {
    
   buildTypes {// 生产/测试环境配置
        release {// 生产环境
            ...
        }
        debug {// 测试环境
            ...
        }
    }
    
    installation {
        // 设置 adb 操作的超时时间为 5000 毫秒
        timeOutInMs = 5000

        // 配置 APK 安装选项
        installOptions.apply {
            // 允许覆盖已安装的应用
            add("-r")
            // 授予所有运行时权限
            add("-g")
        }
    }
  
}
dart 复制代码
val androidComponents =
    project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
androidComponents.finalizeDsl { extension ->
    // 设置 buildType
    val buildType = extension.buildTypes.maybeCreate("finalizeDslTest")
    buildType.isJniDebuggable = true

    // 获取安装选项的信息
    extension.installation.also {
        println("获取的安装选项为:\n timeOutInMs: ${it.timeOutInMs} \n installOptions: ${it.installOptions.joinToString()}")
    }
}

更多关于android{} 属性的内容可以看一文了解 Android项目中build.gradle中的 android 配置扩展

beforeVariants() 回调

beforeVariants() 回调 的作用是访问和修改 VariantBuilder 的属性。beforeVariants() 回调 的作用其实和 finalizeDsl 类似。因为 VariantBuilder 内部的属性值是来自 android{}的。代码示例如下所示:

kotlin 复制代码
class BeforeVariantsPlugin : Plugin<Project>  {
    override fun apply(target: Project) {
        target.plugins.withType(AppPlugin::class.java) {
            val extensions = target.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
            // 仅包含影响构建流程的配置时属性的应用程序组件的模型。
            // beforeVariants 支持 selector() 函数来筛选,而 finalizeDsl 不支持
            extensions.beforeVariants(extensions.selector().withBuildType("release")) {
                // 和 finalizeDsl 类似,都是对 android {} 里面的属性进行修改
                // 但是beforeVariants修改能力相对于 finalizeDsl 更低,比如 finalizeDsl 可以修噶
                // buildType ,而 beforeVariants 只能获取 buildType,而不能修改
                it.buildType.apply {
                    println("BeforeVariantsPlugin $this")
                }
                it.minSdk = 21
            }
        }
    }
}

onVariants() 回调

onVariants() 回调是最常用的,主要的作用是创建修改任务,来对对构建过程的产生的中间产物或者最终产物进行修改。

kotlin 复制代码
val androidComponents =
    project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
    // 通过variant.sources.* 可以访问各种文件
    // 下面是创建了asset目录,并且在其中创建了txt文件
    variant.sources.assets?.let {
        val assetCreationTask =
            project.tasks.register<AssetCreatorTask>("create${variant.name}Asset")

        it.addGeneratedSourceDirectory(
            assetCreationTask,
            AssetCreatorTask::outputDirectory
        )
    }
}

为什么要创建任务来修改,而不是直接在 onVariants 中直接操作。主要原因是:

  1. onVariants 是在配置阶段调用的,此时中间产物或者最终产物还没有生成
  2. 配置阶段不建议执行耗时操作,比如创建文件、网络请求。应该把这些操作放到执行阶段

Component、Variant 和 Artifact

在 AGP 中,我们需要了解 ComponentVariantArtifact 这三个概念。

  • Artifact: Artifact 是 Task 的产物,比如 class、 manifest 文件、aar等。
  • Variant:是构建插件的主要输出,比如 apk、aar
  • ComponentComponent(组件)可以是 APK、AAR、测试 APK、单元测试、测试工具等,涵盖了所有的构建输出。

它们的关系如下所示:

Artifact 有两个属性,分别是 ArtifactKindCategory。其中 ArtifactKind 用于表示 Task 生成的 Artifact 是文件还是目录;而 Category 则用于表示 Artifact 输出文件的位置。

c 复制代码
/**
 * 定义产物类型的类别。例如,这将用于确定输出文件的位置。
 */
enum class Category {
    /* 源码类产物 */
    SOURCES,
    /* 生成的文件,旨在让用户从 IDE 中可见 */
    GENERATED,
    /* 任务产生的中间文件 */
    INTERMEDIATES,
    /* 输出到 outputs 文件夹的文件。这是构建的结果 */
    OUTPUTS,
    /* 测试和 lint 的报告文件 */
    REPORTS,
    ;
}

其中根据 Task 生成的 Artifact 是一个还是可能多个(如果Artifact种类是文件,那么多个就是多个文件,如果Artifact种类是目录,那么多个是指多个目录),把 Artifact 分成了 SingleArtifactMultipleArtifact

所有的 SingleArtifact 种类如下所示:

kotlin 复制代码
/**
 * APK 文件所在的目录。当从 Android Studio 触发构建以优化测试体验时,
 * 生成的 APK 可能不适合发布到 Google Play 商店。
 */
object APK :
    SingleArtifact<Directory>(DIRECTORY),
    ContainsMany,
    Replaceable,
    Transformable

/**
 * 将用于 APK、Bundle 和 InstantApp 包的合并后的清单文件。
 * 仅在应用以下任一插件的模块中可用:
 *      com.android.application
 *      com.android.dynamic-feature
 *      com.android.library
 *      com.android.test
 *
 * 对于每个模块,单元测试和 Android 测试变体没有可用的清单文件。
 */
object MERGED_MANIFEST :
    SingleArtifact<RegularFile>(FILE, Category.INTERMEDIATES, "AndroidManifest.xml"),
    Replaceable,
    Transformable

object OBFUSCATION_MAPPING_FILE :
    SingleArtifact<RegularFile>(FILE, Category.OUTPUTS, "mapping.txt") {
        override fun getFolderName(): String = "mapping"
    }

/**
 * 可供 Google Play 商店使用的最终 Bundle 文件。
 * 仅对基础模块有效。
 */
object BUNDLE :
    SingleArtifact<RegularFile>(FILE, Category.OUTPUTS),
    Transformable

/**
 * 可供发布的最终 AAR 文件。
 */
object AAR :
    SingleArtifact<RegularFile>(FILE, Category.OUTPUTS),
    Transformable

/**
 * 包含库项目导出的公共资源列表的文件。
 *
 * 每行一个资源,格式为:
 * `<资源类型> <资源名称>`
 *
 * 例如:
 * ```
 * string public_string
 * ```
 *
 * 即使没有资源,此文件也会被创建。
 *
 * 参见 [选择要公开的资源](https://developer.android.com/studio/projects/android-library.html#PrivateResources)。
 */
object PUBLIC_ANDROID_RESOURCES_LIST : SingleArtifact<RegularFile>(FILE)

/**
 * 库依赖项的元数据。
 *
 * 文件格式由 com.android.tools.build.libraries.metadata.AppDependencies 定义,
 * 其稳定性不做保证。
 */
@Incubating
object METADATA_LIBRARY_DEPENDENCIES_REPORT : SingleArtifact<RegularFile>(FILE),
    Replaceable,
    Transformable

/**
 * 将打包到最终 APK 或 Bundle 中的资产。
 *
 * 作为输入时,内容为合并后的资产。
 * 对于 APK,资产在打包前会被压缩。
 *
 * 如需向 [ASSETS] 添加新文件夹,必须使用 [com.android.build.api.variant.Sources.assets]。
 */
@Incubating
object ASSETS :
    SingleArtifact<Directory>(DIRECTORY),
    Replaceable,
    Transformable

/**
 * 包含所有屏幕密度资产的通用 APK。
 * 它未针对特定设备优化,且比普通 APK 大得多。
 * 构建过程会先创建 bundle 文件,再从中生成通用 APK。
 *
 * 使用 [APK_FROM_BUNDLE] 效率不高,因为其体积较大,且需要先创建 Bundle(.aab)文件,
 * 最后从中提取 APK。这些步骤会减慢构建流程。因此,除非你需要检查从 .aab 文件生成的通用 APK,
 * 否则建议优先使用 [APK]。
 */
@Incubating
object APK_FROM_BUNDLE :
    SingleArtifact<RegularFile>(FILE, Category.OUTPUTS),
    Transformable

/**
 * 包含将打包到 APK、AAR 或 Bundle 中的所有原生库(.so 文件)的目录。
 *
 * 此目录中的原生库可能会经过进一步处理(例如剥离调试符号)后再打包。
 */
@Incubating
object MERGED_NATIVE_LIBS : SingleArtifact<Directory>(DIRECTORY)

/**
 * 文本符号输出文件(R.txt),包含资源及其 ID 的列表(包括传递依赖的资源)。
 */
@Incubating
object RUNTIME_SYMBOL_LIST :
    SingleArtifact<RegularFile>(FILE)

所有的 MultipleArtifact 种类如下

markdown 复制代码
/**
 * 包含额外 ProGuard 规则的文本文件,用于确定哪些类会被编译到主 dex 文件中。
 *
 * 若设置了此类文件,其中的规则会与构建系统使用的默认规则结合使用。
 *
 * 从 DSL [com.android.build.api.dsl.VariantDimension.multiDexKeepProguard] 初始化。
 */
object MULTIDEX_KEEP_PROGUARD :
    MultipleArtifact<RegularFile>(FILE, Category.SOURCES),
    Replaceable,
    Transformable

/**
 * 包含原生调试元数据的目录。
 *
 * 若设置了此类目录,扩展名为 .dbg 的调试元数据文件会与提取的调试元数据合并并一起打包。
 */
object NATIVE_DEBUG_METADATA :
    MultipleArtifact<Directory>(DIRECTORY),
    Replaceable,
    Appendable,
    Transformable

/**
 * 包含调试符号表的目录。
 *
 * 若设置了此类目录,扩展名为 .sym 的调试符号表文件会与提取的调试符号表合并并一起打包。
 */
object NATIVE_SYMBOL_TABLES :
    MultipleArtifact<Directory>(DIRECTORY),
    Replaceable,
    Appendable,
    Transformable

我们可以看到 SingleArtifactMultipleArtifact 还实现了不同的接口,其作用分别为:

  • Appendable: 表示可以追加的构件类型。由于追加场景的累加行为,Appendable 必须是 MultipleArtifact。
  • Transformable: 表示可以转换的构件类型
  • Replaceable: 表示可以替换的构件类型。只有 SingleArtifact 可以被替换。如果要替换MultipleArtifact 构件类型,则需要通过将所有输入合并为单个输出来转换它。
  • ContainsMany: 表示一个可能包含零个或多个BuiltArtifact的单个DIRECTORY

AGP 使用示例

创建构建配置

kotlin 复制代码
override fun apply(target: Project) {
    target.plugins.withType(AppPlugin::class.java) {
        val extensions =
            target.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
        extensions.onVariants { variant ->
            modifyBuildConfig(variant)
        }
    }
}

// 修改构建配置
private fun modifyBuildConfig(variant: ApplicationVariant) {
    /**
     * 要想生成 BuildConfig 文件,需要配置 buildFeatures { buildConfig = true }
     * 这样你就可以通过 BuildConfig.name 获取到 value 这个字段的值
     */
    variant.buildConfigFields.put("name", BuildConfigField("String", "\"value\"", "说明"))
    // 创建 R.string.test 资源
    variant.resValues.put(variant.makeResValueKey("string", "test"), ResValue("hhhhhh"))
}

我们可以在 Activity 中使用在 AGP 中创建的构建配置。

kotlin 复制代码
class MainActivity: AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.layout_main)
        println("res: ${resources.getString(R.string.test)}")
        println("buildConfig: ${BuildConfig.name}")
    }

}

修改 AndroidManifest

修改 AndroidManifest 有两种方式,一种是通过占位符替换来修改;另一种是直接对 AndroidManifest 文件进行 IO 操作。

对于第一种方式,我们需要先在 AndroidManifest 文件中定义 ${MyActivityName} 占位符。

ini 复制代码
<application android:label="Minimal" android:theme="@style/Theme.AppCompat">
    <activity android:name="${MyActivityName}"
        android:launchMode="standard"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

然后我们就可以通过根据占位符来修改,占位符会替换成对应的值。代码示例如下:

arduino 复制代码
variant.manifestPlaceholders.put("MyActivityName", "MainActivity")

对于第二种方式,我们需要先定义 ManifestTransformerTask 任务。代码示例如下:

less 复制代码
// 修改 AndroidManifest 的任务  
abstract class ManifestTransformerTask : DefaultTask() {
    @get:Input
    abstract val launchMode: Property<String>

    @get:InputFile
    @get:PathSensitive(PathSensitivity.NONE)
    abstract val mergedManifest: RegularFileProperty

    @get:OutputFile
    abstract val updatedManifest: RegularFileProperty

    @TaskAction
    fun taskAction() {
        println("ManifestTransformerTask taskAction")
        var manifest = mergedManifest.asFile.get().readText()
        manifest = manifest.replace(
            "android:launchMode=\"standard\"",
            "android:launchMode=\"${launchMode.get()}\""
        )
        updatedManifest.get().asFile.writeText(manifest)
    }
}

然后在 ApplicationAndroidComponentsExtension 中注册这个任务,如下所示:

kotlin 复制代码
    override fun apply(target: Project) {
        target.plugins.withType(AppPlugin::class.java) {
            val extensions =
                target.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
            extensions.onVariants { variant ->
                modifyAndroidManifest(target, variant)
            }
        }
    }
    
    // 修改 AndroidManifest
    private fun modifyAndroidManifest(project: Project, variant: ApplicationVariant) {
       
        // 替换 AndroidManifest.xml 文件的内容
        // 由于涉及 IO 操作,而 onVariants 是在 Gradle 的配置阶段,因此建议使用 Task 来执行
        val manifestUpdater =
            project.tasks.register(
                variant.name + "ManifestUpdater",
                ManifestTransformerTask::class.java
            ) {
                it.launchMode.set("singleTask")
            }
        // 更新 AndroidManifest.xml 文件
        variant.artifacts 
            .use(manifestUpdater) 
            .wiredWithFiles(
                ManifestTransformerTask::mergedManifest,
                ManifestTransformerTask::updatedManifest
            ).toTransform(SingleArtifact.MERGED_MANIFEST)
    }

其中 variant.artifacts 是指 variant 可访问的 artifact。其中 use 方法是让 artifact 使用 manifestUpdater 任务来连接,其中 wiredWithFiles 方法表示连接的操作是把 artifact 作为该任务的输入,其输入为 ManifestTransformerTask::mergedManifest,其输出 ManifestTransformerTask::updatedManifest 作为新的 artifact。最后的 toTransform 则是指明需要操作的是生成的 AndroidManifest 的 artifact。

在 AGP 中,使用 use、wiredWithFiles 等 api 让我们不需要关心具体的任务逻辑是什么,只需要获取对应的产物进行操作就好了。比如这里,我们不需要知道生成 AndroidManifest 的 artifact 是 processDebugMainManifest 任务,也需要知道它的输入输出的目录在哪里。我们只需要了解我们要操作 AndroidManifest 就可以了,就可以使用对应的api来进行处理了。

为 res 目录添加 values-en 和 values-hk 目录

kotlin 复制代码
/**
 * 为 res 目录添加 values-en 和 values-hk 目录,并且添加 strings.xml 文件
 */
private fun addLanguageString(
    variant: ApplicationVariant,
    project: Project
) {
    // 这里需要注意 variant.sources.res 是 android 项目中的 res 目录
    // 而 variant.sources.resources 是 java 项目中的 resources 目录
    variant.sources.res?.let {
        val resCreationTask =
            project.tasks.register<CreateResourceTask>("create${variant.name}Res") {
                // 这里如果使用 variant.sources.res.all 来作为输入,会造成循环依赖(CircularReferenceException )
                // 错误。因此这里使用 project.layout.projectDirectory 来获取对应目录的内容
                inputDirectory.set(project.layout.projectDirectory.dir("src/main/res"))
            }

        it.addGeneratedSourceDirectory(
            resCreationTask,
            CreateResourceTask::outputDirectory
        )
    }
}

获取源码相关目录

less 复制代码
// 展示 variant.sources 内部的文件
private fun showSourceContent(project: Project, variant: ApplicationVariant) {
    val taskName = "${variant.name}ShowSourceTask"
    project.tasks.register<ShowSourceTask>(taskName) {
        java.set(variant.sources.java?.all)
        kotlin.set(variant.sources.kotlin?.all)
        res.set(variant.sources.res?.all)
        assets.set(variant.sources.assets?.all)
        manifest.set(variant.sources.manifests.all)
        jni.set(variant.sources.jniLibs?.all)
        resources.set(variant.sources.resources?.all)
        aidl.set(variant.sources.aidl?.all)
    }
}

abstract class ShowSourceTask : DefaultTask() {
    @get:Optional
    @get:InputFiles
    abstract val java: Property<Provider<out Collection<Directory>>>

    @get:Optional
    @get:InputFiles
    abstract val kotlin: Property<Provider<out Collection<Directory>>>

    @get:Optional
    @get:InputFiles
    abstract val res: Property<Provider<List<Collection<Directory>>>>

    @get:Optional
    @get:InputFiles
    abstract val resources: Property<Provider<out Collection<Directory>>>

    @get:InputFiles
    @get:Optional
    abstract val assets: Property<Provider<List<Collection<Directory>>>>

    @get:InputFiles
    @get:Optional
    abstract val jni: Property<Provider<List<Collection<Directory>>>>

    @get:InputFiles
    @get:Optional
    abstract val aidl: Property<Provider<out Collection<Directory>>>

    @get:InputFiles
    @get:Optional
    abstract val manifest: Property<Provider<out List<RegularFile>>>


    @TaskAction
    fun taskAction() {
        fun logFiles(title: String, directories: Collection<Directory>?) {
            if (directories.isNullOrEmpty()) {
                println("$title is empty")
                return
            }
            println("==> $title:")
            directories.forEach { dir ->
                dir.asFile.walkTopDown().forEach {
                    println("    ${it.path}")
                }
            }
            println("<== end of $title")
        }

        // 获取 Java 源文件集合
        val javaDirs = java.orNull?.get()
        logFiles("Java Sources", javaDirs)

        // 获取 Kotlin 源文件集合
        val kotlinDirs = kotlin.orNull?.get()
        logFiles("Kotlin Sources", kotlinDirs)

        // 获取 Res 资源文件集合
        val resDirList = res.orNull?.get()
        logFiles("Res Files", resDirList?.flatten())

        // 获取 Resources 源文件集合
        val resourcesDirs = resources.orNull?.get()
        logFiles("Resources Files", resourcesDirs)

        // 获取 Assets 源文件集合
        val assetsDirList = assets.orNull?.get()
        logFiles("Assets Files", assetsDirList?.flatten())

        // 获取 Jni 源文件集合
        val jniDirList = jni.orNull?.get()
        logFiles("Jni Files", jniDirList?.flatten())

        // 获取 Aidl 源文件
        val aidlDirs = aidl.orNull?.get()
        logFiles("Aidl Files", aidlDirs)

        // 获取 Manifest 文件
        val manifestFiles = manifest.orNull?.get()
        println("==> manifestFiles:")
        manifestFiles?.forEach { dir ->
            dir.asFile.walkTopDown().forEach {
                println("    ${it.path}")
            }
        }
        println("<== end of manifestFiles")

    }

}

验证类文件

下面是 Android 官方的示例demo,这里给这个demo的代码加上了注释。所有的官方示例可以看 gradle-recipes at agp-8.6

php 复制代码
// 注册一个新任务来验证应用的类文件
val taskName = "check${variant.name}Classes"
val taskProvider = project.tasks.register<CheckClassesTask>(taskName) {
    output.set(
        project.layout.buildDirectory.dir("intermediates/$taskName")
    )
}

// 将任务的projectJars和projectDirectories输入设置为
// ScopedArtifacts.Scope.PROJECT作用域的ScopedArtifact.CLASSES构件。
// 这会自动在此任务和任何生成PROJECT作用域类文件的任务之间创建依赖关系。
variant.artifacts
    .forScope(ScopedArtifacts.Scope.PROJECT)
    .use(taskProvider)
    .toGet(
        ScopedArtifact.CLASSES,
        CheckClassesTask::projectJars,
        CheckClassesTask::projectDirectories,
    )

// 将此任务的allJars和allDirectories输入设置为
// ScopedArtifacts.Scope.ALL作用域的ScopedArtifact.CLASSES构件。
// 这会自动在此任务和任何生成类文件的任务之间创建依赖关系。
variant.artifacts
    .forScope(ScopedArtifacts.Scope.ALL)
    .use(taskProvider)
    .toGet(
        ScopedArtifact.CLASSES,
        CheckClassesTask::allJars,
        CheckClassesTask::allDirectories,
    )

注意:ScopedArtifacts.Scope.ALL 和 ScopedArtifacts.Scope.PROJECT 的主要区别是 ScopedArtifacts.Scope.PROJECT 表示只包含项目中的代码或者资源,不包含依赖库的代码或者资源;而 ScopedArtifacts.Scope.ALL 是指项目和依赖中所有的代码和资源

less 复制代码
/**
 * 此任务对变体的类文件执行简单检查。
 */
abstract class CheckClassesTask : DefaultTask() {

    // 为了使任务在输入未更改时保持最新状态,
    // 任务必须声明一个输出,即使它未被使用。没有输出的任务
    // 无论输入是否更改,都会始终运行
    @get:OutputDirectory
    abstract val output: DirectoryProperty

    /**
     * 项目作用域,不包括依赖项。
     */
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val projectDirectories: ListProperty<Directory>

    /**
     * 项目作用域,不包括依赖项。
     */
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.NONE)
    abstract val projectJars: ListProperty<RegularFile>

    /**
     * 完整作用域,包括项目作用域和所有依赖项。
     */
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val allDirectories: ListProperty<Directory>

    /**
     * 完整作用域,包括项目作用域和所有依赖项。
     */
    @get:InputFiles
    @get:PathSensitive(PathSensitivity.NONE)
    abstract val allJars: ListProperty<RegularFile>

    /**
     * 此任务对类文件执行简单检查,但可以编写类似的任务
     * 来执行有用的验证。
     */
    @TaskAction
    fun taskAction() {

        // 检查projectDirectories
        if (projectDirectories.get().isEmpty()) {
            throw RuntimeException("预期projectDirectories不为空")
        }
        projectDirectories.get().firstOrNull()?.let {
            if (!it.asFile.walk().toList().any { file -> file.name == "MainActivity.class" }) {
                throw RuntimeException("预期在projectDirectories中存在MainActivity.class")
            }
        }

        // 检查projectJars。我们期望projectJars包含项目的R.jar,但不包含来自依赖项的jar
        // (例如,kotlin标准库jar)
        val projectJarFileNames = projectJars.get().map { it.asFile.name }
        if (!projectJarFileNames.contains("R.jar")) {
            throw RuntimeException("预期项目jars包含R.jar")
        }
        if (projectJarFileNames.any { it.startsWith("kotlin-stdlib") }) {
            throw RuntimeException("不期望projectJars包含kotlin标准库")
        }

        // 检查allDirectories
        if (allDirectories.get().isEmpty()) {
            throw RuntimeException("预期allDirectories不为空")
        }
        allDirectories.get().firstOrNull()?.let {
            if (!it.asFile.walk().toList().any { file -> file.name == "MainActivity.class" }) {
                throw RuntimeException("预期在allDirectories中存在MainActivity.class")
            }
        }

        // 检查allJars。我们期望allJars包含来自项目及其依赖项的jar
        // (例如,kotlin标准库jar)。
        val allJarFileNames = allJars.get().map { it.asFile.name }
        if (!allJarFileNames.contains("R.jar")) {
            throw RuntimeException("预期allJars包含R.jar")
        }
        if (!allJarFileNames.any { it.startsWith("kotlin-stdlib") }) {
            throw RuntimeException("预期allJars包含kotlin标准库")
        }
    }
}

代码转换

下面是 Android 官方的示例,这个示例展示了如何转换将用于创建 .dex 文件的类。 需要使用两个列表来获取完整的类集合,因为有些类以 .class 文件的形式存在于目录中,而另一些则存在于 jar 文件中。因此,必须同时查询 [Directory](类文件)和 [RegularFile](jar 包)的 [ListProperty] 才能获得完整列表。 在这个示例中,我们查询所有类,以便对它们执行一些字节码插桩操作。 Variant API 提供了一个基于 ASM 的便捷字节码转换 API,但本示例使用 javassist 来展示如何通过另一种字节码增强工具实现这一功能。所有的官方示例可以看 gradle-recipes at agp-8.6

php 复制代码
val taskProvider = project.tasks.register<ModifyClassesTask>("${variant.name}ModifyClasses")

// 注册类修改任务
variant.artifacts.forScope(ScopedArtifacts.Scope.PROJECT)
    .use(taskProvider)
    .toTransform(
        ScopedArtifact.CLASSES,
        ModifyClassesTask::allJars,
        ModifyClassesTask::allDirectories,
        ModifyClassesTask::output
    )
less 复制代码
abstract class ModifyClassesTask : DefaultTask() {
    // 此属性将被设置为作用域内所有可用的Jar文件
    @get:InputFiles
    abstract val allJars: ListProperty<RegularFile>

    // Gradle会用作用域内所有可用的类目录设置此属性
    @get:InputFiles
    abstract val allDirectories: ListProperty<Directory>

    // 任务会将目录和Jar中的所有类(经过可选修改后)放入单个Jar中
    @get:OutputFile
    abstract val output: RegularFileProperty

    @Internal
    val jarPaths = mutableSetOf<String>()

    @TaskAction
    fun taskAction() {

        val pool = ClassPool(ClassPool.getDefault())

        val jarOutput = JarOutputStream(BufferedOutputStream(FileOutputStream(
            output.get().asFile
        )))
        // 我们只是从Jar文件中复制类,不做修改
        allJars.get().forEach { file ->
            println("处理 " + file.asFile.getAbsolutePath())
            val jarFile = JarFile(file.asFile)
            jarFile.entries().iterator().forEach { jarEntry ->
                println("从Jar添加 ${jarEntry.name}")
                jarOutput.writeEntity(jarEntry.name, jarFile.getInputStream(jarEntry))
            }
            jarFile.close()
        }
        // 遍历目录中的类文件
        // 查找SomeSource.class,以添加生成的接口,并在toString方法中插入额外输出
        // (在本例中只是System.out)
        allDirectories.get().forEach { directory ->
            println("处理 " + directory.asFile.getAbsolutePath())
            directory.asFile.walk().forEach { file ->
                if (file.isFile) {
                    if (file.name.endsWith("SomeSource.class")) {
                        println("找到 $file.name")
                        val interfaceClass = pool.makeInterface("com.example.android.recipes.sample.SomeInterface");
                        println("添加 $interfaceClass")
                        jarOutput.writeEntity("com/example/android/recipes/sample/SomeInterface.class", interfaceClass.toBytecode())
                        val ctClass = file.inputStream().use {
                            pool.makeClass(it);
                        }
                        ctClass.addInterface(interfaceClass)

                        val m = ctClass.getDeclaredMethod("toString");
                        if (m != null) {
                            // 注入将位于toString方法开头的额外代码
                            m.insertBefore("{ System.out.println(\"Some Extensive Tracing\"); }");

                            val relativePath = directory.asFile.toURI().relativize(file.toURI()).getPath()
                            // 将修改后的类写入输出Jar
                            jarOutput.writeEntity(relativePath.replace(File.separatorChar, '/'), ctClass.toBytecode())
                        }
                    } else {
                        // 如果类不是SomeSource.class,则直接复制到输出,不做修改
                        val relativePath = directory.asFile.toURI().relativize(file.toURI()).getPath()
                        println("从目录添加 ${relativePath.replace(File.separatorChar, '/')}")
                        jarOutput.writeEntity(relativePath.replace(File.separatorChar, '/'), file.inputStream())
                    }
                }
            }
        }
        jarOutput.close()
    }

    // writeEntity方法检查输出Jar中是否已存在具有相同名称的文件
    private fun JarOutputStream.writeEntity(name: String, inputStream: InputStream) {
        // 先检查名称是否重复
        if (jarPaths.contains(name)) {
            printDuplicatedMessage(name)
        } else {
            putNextEntry(JarEntry(name))
            inputStream.copyTo(this)
            closeEntry()
            jarPaths.add(name)
        }
    }

    private fun JarOutputStream.writeEntity(relativePath: String, byteArray: ByteArray) {
        // 先检查名称是否重复
        if (jarPaths.contains(relativePath)) {
            printDuplicatedMessage(relativePath)
        } else {
            putNextEntry(JarEntry(relativePath))
            write(byteArray)
            closeEntry()
            jarPaths.add(relativePath)
        }
    }

    private fun printDuplicatedMessage(name: String) =
        println("无法添加 ${name},因为输出Jar中已存在同名文件。")
}

参考

相关推荐
梦想改变生活3 分钟前
《Flutter篇第二章》MasonryGridView瀑布流列表
android·flutter
杨航 AI15 分钟前
PHP 5.5 Action Management with Parameters (English Version)
android·开发语言·php
柿蒂1 小时前
一次Android下载优化,CDN消耗占比从50+%到1%
android·android jetpack
真夜1 小时前
关于rn下载gradle依赖速度慢的问题
react native·gradle·android studio
Andrew_Ryan2 小时前
gradle set up
android
_祝你今天愉快4 小时前
在安卓中使用 FFmpegKit 剪切视频并添加文字水印
android·ffmpeg
编程乐学6 小时前
网络资源模板--基于Android Studio 实现的新闻App
android·android studio·移动端开发·新闻·安卓大作业·新闻app
-曾牛6 小时前
PHP 与 MySQL 详解实战入门(1)
android·开发语言·mysql·渗透测试·php·php教程·脚本语言
Monkey-旭6 小时前
深入理解 Kotlin Flow:异步数据流处理的艺术
android·开发语言·kotlin·响应式编程·flow
不想迷路的小男孩8 小时前
Android Studio怎么显示多排table,打开文件多行显示文件名
android·ide·android studio