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的配置阶段执行的)
- 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
的相关属性。
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 中直接操作。主要原因是:
- onVariants 是在配置阶段调用的,此时中间产物或者最终产物还没有生成
- 配置阶段不建议执行耗时操作,比如创建文件、网络请求。应该把这些操作放到执行阶段
Component、Variant 和 Artifact
在 AGP 中,我们需要了解 Component
、Variant
和 Artifact
这三个概念。
Artifact
:Artifact
是 Task 的产物,比如 class、 manifest 文件、aar等。Variant
:是构建插件的主要输出,比如 apk、aarComponent
:Component
(组件)可以是 APK、AAR、测试 APK、单元测试、测试工具等,涵盖了所有的构建输出。
它们的关系如下所示:

Artifact
有两个属性,分别是 ArtifactKind
和 Category
。其中 ArtifactKind
用于表示 Task 生成的 Artifact
是文件还是目录;而 Category
则用于表示 Artifact
输出文件的位置。
c
/**
* 定义产物类型的类别。例如,这将用于确定输出文件的位置。
*/
enum class Category {
/* 源码类产物 */
SOURCES,
/* 生成的文件,旨在让用户从 IDE 中可见 */
GENERATED,
/* 任务产生的中间文件 */
INTERMEDIATES,
/* 输出到 outputs 文件夹的文件。这是构建的结果 */
OUTPUTS,
/* 测试和 lint 的报告文件 */
REPORTS,
;
}
其中根据 Task 生成的 Artifact
是一个还是可能多个(如果Artifact
种类是文件,那么多个就是多个文件,如果Artifact
种类是目录,那么多个是指多个目录),把 Artifact
分成了 SingleArtifact
和 MultipleArtifact
。
所有的 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
我们可以看到 SingleArtifact
和 MultipleArtifact
还实现了不同的接口,其作用分别为:
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中已存在同名文件。")
}