一文了解 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中已存在同名文件。")
}

参考

相关推荐
阿巴斯甜16 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker16 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952717 小时前
Andorid Google 登录接入文档
android
黄林晴19 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android