一文了解 AGP8 的使用

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

image.png

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

Android studio 下载agp源码

implementation("com.android.tools.build:gradle:8.6.1")

AGP 的基本使用


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

//定义插件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 的Taskhttps://juejin.cn/post/7423066953039626278),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 的相关属性。

android {
   buildTypes {// 生产/测试环境配置        release {// 生产环境            ...        }        debug {// 测试环境            ...        }    }
    installation {        // 设置 adb 操作的超时时间为 5000 毫秒        timeOutInMs = 5000
        // 配置 APK 安装选项        installOptions.apply {            // 允许覆盖已安装的应用            add("-r")            // 授予所有运行时权限            add("-g")        }    }}
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 配置扩展 https://juejin.cn/post/7470702616906039332

beforeVariants() 回调

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

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() 回调是最常用的,主要的作用是创建修改任务,来对对构建过程的产生的中间产物或者最终产物进行修改。

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 中,我们需要了解 ComponentVariant 和 Artifact 这三个概念。

  • Artifact

    Artifact 是 Task 的产物,比如 class、 manifest 文件、aar等。

  • Variant

    :是构建插件的主要输出,比如 apk、aar

  • Component

    Component(组件)可以是 APK、AAR、测试 APK、单元测试、测试工具等,涵盖了所有的构建输出。

它们的关系如下所示:

image.png

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

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

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

所有的 SingleArtifact 种类如下所示:

/** * 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 定义, * 其稳定性不做保证。 */@Incubatingobject METADATA_LIBRARY_DEPENDENCIES_REPORT : SingleArtifact<RegularFile>(FILE),    Replaceable,    Transformable
/** * 将打包到最终 APK 或 Bundle 中的资产。 * * 作为输入时,内容为合并后的资产。 * 对于 APK,资产在打包前会被压缩。 * * 如需向 [ASSETS] 添加新文件夹,必须使用 [com.android.build.api.variant.Sources.assets]。 */@Incubatingobject ASSETS :    SingleArtifact<Directory>(DIRECTORY),    Replaceable,    Transformable
/** * 包含所有屏幕密度资产的通用 APK。 * 它未针对特定设备优化,且比普通 APK 大得多。 * 构建过程会先创建 bundle 文件,再从中生成通用 APK。 * * 使用 [APK_FROM_BUNDLE] 效率不高,因为其体积较大,且需要先创建 Bundle(.aab)文件, * 最后从中提取 APK。这些步骤会减慢构建流程。因此,除非你需要检查从 .aab 文件生成的通用 APK, * 否则建议优先使用 [APK]。 */@Incubatingobject APK_FROM_BUNDLE :    SingleArtifact<RegularFile>(FILE, Category.OUTPUTS),    Transformable
/** * 包含将打包到 APK、AAR 或 Bundle 中的所有原生库(.so 文件)的目录。 * * 此目录中的原生库可能会经过进一步处理(例如剥离调试符号)后再打包。 */@Incubatingobject MERGED_NATIVE_LIBS : SingleArtifact<Directory>(DIRECTORY)
/** * 文本符号输出文件(R.txt),包含资源及其 ID 的列表(包括传递依赖的资源)。 */@Incubatingobject RUNTIME_SYMBOL_LIST :    SingleArtifact<RegularFile>(FILE)

所有的 

MultipleArtifact 种类如下

/** * 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 定义, * 其稳定性不做保证。 */@Incubatingobject METADATA_LIBRARY_DEPENDENCIES_REPORT : SingleArtifact<RegularFile>(FILE),    Replaceable,    Transformable
/** * 将打包到最终 APK 或 Bundle 中的资产。 * * 作为输入时,内容为合并后的资产。 * 对于 APK,资产在打包前会被压缩。 * * 如需向 [ASSETS] 添加新文件夹,必须使用 [com.android.build.api.variant.Sources.assets]。 */@Incubatingobject ASSETS :    SingleArtifact<Directory>(DIRECTORY),    Replaceable,    Transformable
/** * 包含所有屏幕密度资产的通用 APK。 * 它未针对特定设备优化,且比普通 APK 大得多。 * 构建过程会先创建 bundle 文件,再从中生成通用 APK。 * * 使用 [APK_FROM_BUNDLE] 效率不高,因为其体积较大,且需要先创建 Bundle(.aab)文件, * 最后从中提取 APK。这些步骤会减慢构建流程。因此,除非你需要检查从 .aab 文件生成的通用 APK, * 否则建议优先使用 [APK]。 */@Incubatingobject APK_FROM_BUNDLE :    SingleArtifact<RegularFile>(FILE, Category.OUTPUTS),    Transformable
/** * 包含将打包到 APK、AAR 或 Bundle 中的所有原生库(.so 文件)的目录。 * * 此目录中的原生库可能会经过进一步处理(例如剥离调试符号)后再打包。 */@Incubatingobject MERGED_NATIVE_LIBS : SingleArtifact<Directory>(DIRECTORY)
/** * 文本符号输出文件(R.txt),包含资源及其 ID 的列表(包括传递依赖的资源)。 */@Incubatingobject RUNTIME_SYMBOL_LIST :    SingleArtifact<RegularFile>(FILE)

我们可以看到 

SingleArtifact 和 MultipleArtifact 还实现了不同的接口,其作用分别为:

  • Appendable

    : 表示可以追加的构件类型。由于追加场景的累加行为,Appendable 必须是 MultipleArtifact。

  • Transformable

    : 表示可以转换的构件类型

  • Replaceable

    : 表示可以替换的构件类型。只有 SingleArtifact 可以被替换。如果要替换MultipleArtifact 构件类型,则需要通过将所有输入合并为单个输出来转换它。

  • ContainsMany

    : 表示一个可能包含零个或多个BuiltArtifact的单个DIRECTORY

AGP 使用示例


创建构建配置

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 中创建的构建配置。

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} 占位符。

<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>

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

variant.manifestPlaceholders.put("MyActivityName", "MainActivity")

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

// 修改 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 中注册这个任务,如下所示:

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 目录

/** * 为 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        )    }}

获取源码相关目录

// 展示 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 https://link.juejin.cn/?target=https%3A%2F%2Fsiteproxy.ruqli.workers.dev%3A443%2Fhttps%2Fgithub.com%2Fandroid%2Fgradle-recipes%2Ftree%2Fagp-8.6

// 注册一个新任务来验证应用的类文件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 是指项目和依赖中所有的代码和资源

/** * 此任务对变体的类文件执行简单检查。 */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  

https://link.juejin.cn/?target=https%3A%2F%2Fsiteproxy.ruqli.workers.dev%3A443%2Fhttps%2Fgithub.com%2Fandroid%2Fgradle-recipes%2Ftree%2Fagp-8.6

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    )
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中已存在同名文件。")}
作者:小墙程序员链接:https://juejin.cn/post/7533169614206713883

关注我获取更多知识或者投稿

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值