AGP与资源构建交互的相关API

Android构建的过程,其中很重要的部分就是资源的构建,AGP通过把项目中资源进行link、complie、merge…等这些操作,最终把需要的资源打包进入APK中。如果大家熟悉AGP流程它是把这些操作封装各种Task来完成,但除此之外AGP还提供很多方便的API帮助我们动态化的构建。

gradle用到的脚本是*.gradle.kts格式,插件语言也是使用的kotlin

添加资源SourceSet

很多开发者在项目都会用到SourceSet来设置java/kotin文件,其实资源文件也可以使用同样的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
android{
sourceSets {
getByName("main"){
java{}
kotlin{}
res{
srcDir("test/res")//添加一个额外的res文件夹
srcDirs("test1/res","test2/res")//添加多个额外的res文件夹
//setSrcDirs(srcDirs: Iterable<*>)//覆盖设置当前的"res"文件夹
}
resouces{}
}
}
}

经过上面这段代码设置以后,Android的项目结构目录会为新建的文件生成资源文件夹的标志:
image.png
这种直接添加了完整的文件夹方式,在实际的项目开发中常用来解决工程上的问题:

  1. 模块组件的合并,比如业务和架构的改动,致使原来分散在不同组件的代码要合并成一个,Java和Kotlin的部分,只要通过包名来区分即可。而资源文件完全可以通过sourcesSets的方式来解决。
  2. 动态资源的构建。假如一个工程是依赖云端某些资源包(文字、图片、安全密钥等等)的下发。一些面向国际化大型app,各个组件需要用到各个国家的多语言,作为组件的开发者最佳方案应当不用关注用来那个国家的语言,只需文字的key,然后项目根据key动态生成多语言。
1
2
3
4
5
6
7
8
9
10
11
androidExtension.onVariants{variant->
val variantCapitalizedName=appVariant.name.capitalized()\
afterEvaluate {
project.tasks.named("pre${variantCapitalizedName}Build")
.configure {
doFirst {
//下载文件放在 /lang/res/value
}
}
}
}

AndroidManifest.xml占位与合并

AGP中有一个针对AndroidManifest.xml的使用特性——占位符与值替换;当AndroidManifest.xml中某些值不能再开发的时确定,而依赖于渠道信息、构建类型、甚至是编译时动态从环境中获取信息,我们完全可以使用占位符预占坑,再通过AGP的相关DSL针对行具体情况配置不同的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="xxxx">
<application
... >
<activity android:name=".MainActivity"
...>
<intent-filter>
...
<data android:scheme="https" android:host="${hostName}"/>
</intent-filter>
</activity>
</application>
</manifest>
1
2
3
4
5
buildTypes{
getByName("debug"){
manifestPlaceHolders["hostName"]="debug.bar.com"
}
}

更灵活的方式就是通过Variant组合进行操作,

1
2
3
4
5
6
7
8
9
appExtension.onVariants(
appExtension.selector()
.withBuildType("debug")
.withFlavor(Pair("server","production"))
){variant->
variant.manifestPlaceholders
.put("hostName","pre-live.bar.com")

}

常规XML健值对资源插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
appExtension.finalizeDsl { appExt ->
with(appExt) {
buildTypes {
getByName("debug") {
resValue("string", "app_token1", "123")
}
}
}
}
appExtension.onVariants(appExtension.selector()
.withBuildType("debug")
.withFlavor(Pair("server", "production"))) { variant ->

val key = variant.makeResValueKey("string", "app_token")
val value = ResValue("1234", "comment")
variant.resValues.put(key, value)
}

为APK/AAR添加额外资源文件

前面讲的大部分都是/res文件夹下的Android资源,如果我们想要添加单纯的资源比如密钥、raw、json等文件,常见的做法就是放入到assert和resource目录下。
在resouce目录下添加的文件,对于APK会把文件添加到APK压缩包的根目录下:app.apk/app-appended-file.txt,对于AAR则会加入到内部jar包目录下:lib.aar/classes.jar/lib-appended-file.txt,AAR打包进APK后,添加的文件也会在APK的根目录下。

除此之外向AAR添加文件,还可通过如下方式:

1
2
3
4
5
6
7
8
9
10
libExtension.onVariants{variant-> 
afterEvaluate {
project.tasks.named("bundle${variant.name.capitalized()}Aar")
.configure {
val bundle=this as com.android.bundle.gradle.tasks.BundleAar
bundle.from(file("lib-appended-file-arr-root.txt"))
}

}
}

这是因为AAR的BundlerAar 打包任务 ,该任务继承了Gradle原生的Zip任务,而其上游AbstractCopyTast提供了from接口能方便的添加家外部文件的需求,但是在AAR打包进APK的过程中,添加文件会被忽略掉,没发现有什么其它用处。
作为Android开发者,assert文件都很熟悉,它是一个重要的存放资源的文件夹。如果文件是在编译时动态生成下发。我们可以结合MultipleArtifact.ASSETS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
appExtension.onVariants { variant ->
val capitalizedName = variant.name.capitalized()

val genAssetTaskProvider =
project.tasks.register<GenAssetTask>("genAssetTask${capitalizedName}") {
group = "asset"
additionModuleFileProp.set(project.file("app_module.json"))
}

val addAssetTaskProvider =
project.tasks.register<AddAssetTask>("addAssetTask${capitalizedName}") {
group = "asset"
additionModuleFileProp.set(genAssetTaskProvider.flatMap {
it.additionModuleFileProp
})
}
variant.artifacts
.use(addAssetTaskProvider)
.wiredWith(AddAssetTask::outputDirectoryProp)
.toAppendTo(MultipleArtifact.ASSETS)
}
abstract class GenAssetTask : DefaultTask() {
@get: OutputFile
abstract val additionModuleFileProp: RegularFileProperty

@TaskAction
fun getAsset() {
additionModuleFileProp.get().asFile.apply {
createNewFile()
writeText("{\"module\":\"app_key\"}")
}
}
}

abstract class AddAssetTask : DefaultTask() {
@get: InputFile
abstract val additionModuleFileProp: RegularFileProperty

@get: OutputDirectory
abstract val outputDirectoryProp: DirectoryProperty

@TaskAction
fun getAsset() {
val moduleFile = additionModuleFileProp.get().asFile
val assetDir = outputDirectoryProp.get().asFile
assetDir.mkdirs()
moduleFile.copyTo(File(assetDir, moduleFile.name), overwrite = true)
}
}

为APK/AAR移除资源文件

在新版本AGP中对资源移除做了API的限制:

  1. res/下的排出选项被取消
  2. packageOptions.exclues被细分成resource jinLibs dex三个选项。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    android{
    sourceSets {
    androidResources{
    //excludes +="..." 无法在AGP 7+使用
    }
    }
    packagingOptions {
    dex{ }
    resources {
    excludes+="res/layout/lib_layout.xml"//移除资源
    }
    jniLibs{
    //exclude 'lib/arm64-v8a/libunwanted.so' 移除JNI库
    }
    }
    }