AGP 是 Android Gradle Plugin 的简称。AGP 的主要主要的用于实现 Android 项目的构建。当我们执行 assemble 命令时,会有如下图的任务执行,这些任务就是 AGP 提供的。
除此之外,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的配置阶段执行的)
- DSL 解析(DSL parsing)
:此时会解析构建脚本,并创建和设置 android 块中各类 Android DSL 对象的属性。下文所述的变体 API 回调也会在此阶段注册。
- finalizeDsl() 回调
:该回调允许你在 DSL 对象被锁定以用于组件(变体)创建前修改它们。变体构建器(VariantBuilder)对象会基于 DSL 对象中包含的数据创建。
- DSL 锁定(DSL locking)
:此时 DSL 已锁定,无法再修改。
- beforeVariants() 回调
:在构建的此阶段,你可以访问 VariantBuilder 对象,这些对象决定了将要创建的变体及其属性。例如,你可以通过编程方式禁用某些变体、其测试,或仅为特定变体修改属性值(如 minSdk)。与 finalizeDsl() 类似,你提供的所有值必须在配置阶段解析,且不能依赖外部输入。beforeVariants() 回调执行完成后,VariantBuilder 对象不得再被修改。
- 变体创建(Variant creation)
:此时将创建的组件和产物列表已确定,无法再更改。
- onVariants() 回调
:调用 onVariants() 时,AGP 将要创建的所有产物已确定,因此你不能再禁用它们。不过,你可以通过为变体对象中的 Property 属性设置值,来修改用于任务的某些参数。由于 Property 值仅在 AGP 任务执行时才会解析,因此你可以安全地将其与自定义任务的提供者(provider)关联 —— 这些自定义任务将执行所需的计算,包括读取外部输入(如文件或网络数据)。
- 变体锁定(Variant locking)
:此时变体对象已锁定,无法再修改。
- 任务创建(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 中直接操作。主要原因是:
onVariants 是在配置阶段调用的,此时中间产物或者最终产物还没有生成
配置阶段不建议执行耗时操作,比如创建文件、网络请求。应该把这些操作放到执行阶段
Component、Variant 和 Artifact
在 AGP 中,我们需要了解 Component
、Variant
和 Artifact
这三个概念。
Artifact
:
Artifact
是 Task 的产物,比如 class、 manifest 文件、aar等。Variant
:是构建插件的主要输出,比如 apk、aar
Component
:
Component
(组件)可以是 APK、AAR、测试 APK、单元测试、测试工具等,涵盖了所有的构建输出。
它们的关系如下所示:
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
关注我获取更多知识或者投稿