8、资源处理(Resource Processing)
资源处理这块,说简单也简单,说复杂能把你绕晕。我做了这么多年Android构建,见过太多人在资源上翻车。今天咱们就把资源合并、限定符、资源压缩、还有那个Non-Transitive R Classes一次性讲透。
核心一句话:资源处理就是AGP帮你把散落在各个模块、各种限定符下的资源,合并成一个高效、无冲突的资源包。
8.1 资源合并优先级
资源合并,说白了就是AGP把多个模块的资源往一块儿凑。但凑的时候总得有个先来后到吧?
我遇到过最典型的场景:主模块里定义了一个colors.xml,依赖的某个library里也有同名颜色。到底用谁的?
规则其实很简单:主模块的优先级最高。具体来说:
- 主应用模块(app) > 本地依赖模块(library) > 远程依赖(aar)
- 如果同一个资源在多个同级别模块中出现,后声明的会覆盖先声明的
- 资源合并是文件级别的,不是单个资源条目级别
我的习惯:在团队协作时,我会在主模块的build.gradle里显式声明pickFirsts或excludes,避免依赖库之间的资源冲突。尤其是那些第三方SDK,经常互相覆盖资源。
举个例子,假设你有这样的依赖结构:
app (主模块)
└── libraryA (本地库)
└── libraryB (远程aar)
如果三个模块都有res/values/strings.xml,那么最终生效的是app里的。如果app里没有,就看libraryA的。说白了就是离主模块越近,优先级越高。
8.2 资源限定符
资源限定符这东西,你想想看,Android设备千奇百怪,屏幕密度从ldpi到xxxhdpi,语言从中文到阿拉伯语。没有限定符,你的App在不同设备上就是灾难。
AGP在处理限定符时,遵循的是最匹配优先原则。比如:
| 限定符类型 | 示例 | 匹配规则 |
|---|---|---|
| 屏幕密度 | drawable-mdpi, drawable-hdpi |
设备dpi最接近的优先 |
| 语言 | values-zh, values-en |
完全匹配优先,否则回退到默认 |
| 屏幕尺寸 | layout-sw600dp, layout-sw720dp |
最小宽度最接近的优先 |
| 夜间模式 | values-night |
匹配当前系统主题 |
我曾经踩过的坑:有一次给海外版本做多语言适配,我把values-zh里的字符串写错了,结果中文用户看到的全是英文。排查了半天才发现,原来values-zh目录下少了一个strings.xml文件,系统自动回退到了默认的values目录。嗯,这里要注意:限定符目录必须完整,否则会静默回退。
我个人建议,在项目里至少维护values(默认)、values-zh(中文)、drawable-mdpi、drawable-hdpi、drawable-xhdpi这几个基础限定符目录。其他的按需添加,别一上来就搞几十个目录,维护起来累死人。
8.3 shrinkResources
shrinkResources这个功能,说白了就是帮你自动砍掉那些没用的资源。我见过太多App里塞了一堆用不上的图片和布局文件,APK体积就这么被撑大了。
启用方式很简单:
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
}
}
}
注意,shrinkResources必须和minifyEnabled一起用。为什么?因为它依赖ProGuard或R8的代码分析结果——只有知道哪些代码没被用到,才能判断哪些资源是冗余的。
它的工作原理是这样的:
- R8/ProGuard先分析代码,标记出所有被引用的资源ID
- AGP遍历所有资源文件,把没被标记的删掉
- 如果有
tools:keep或tools:discard声明,按声明处理
避坑指南:我曾经在一个项目里启用了shrinkResources,结果Release包启动就崩溃。查了半天发现,有个第三方SDK通过反射引用资源,ProGuard分析不到,资源被误删了。解决方案是在res/raw/keep.xml里用tools:keep显式保留:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@layout/special_activity,@drawable/icon_*" />
另外,shrinkResources还有一个隐藏功能:资源混淆。它会将资源文件重命名为短路径,比如res/drawable/ic_launcher.png变成res/drawable/a.png。这能进一步减小APK体积,但要注意,如果你有通过字符串拼接来引用资源的代码,可能会出问题。
8.4 资源ID非最终化(Non-Transitive R Classes)
这个特性,嗯,算是AGP 8.0之后的一个大改动。以前每个模块的R类都是final的,资源ID是硬编码的常量。这在多模块项目里会带来一个问题:
假设你有模块A和模块B,模块A引用了模块B的资源。如果模块B新增了一个资源,模块A的R类不会自动更新——因为R类是final的,编译时就定死了。这就导致模块A必须重新编译才能感知到模块B的资源变化。
Non-Transitive R Classes的解决方案是:让R类不再传递。每个模块只包含自己的资源ID,不包含依赖模块的。这样模块B的资源变化不会影响模块A的R类,增量编译更快。
启用方式:
android {
// 在gradle.properties中设置
android.nonTransitiveRClass=true
}
我的建议:新项目直接开启nonTransitiveRClass。老项目迁移时要注意,如果你在代码里直接引用了其他模块的R类(比如com.other.module.R.id.xxx),需要改成引用自己模块的R类,或者使用R2注解(ButterKnife等框架)。
开启后,资源ID不再是final的,而是变成了public static int。这意味着资源ID在编译期不确定,要到链接期才确定。好处是模块间解耦了,坏处是如果你用了某些依赖反射的库,可能需要适配。
我记得有一次帮一个团队做模块化改造,他们项目有20多个模块,每次改一个资源都要全量编译,耗时十几分钟。开启nonTransitiveRClass后,增量编译时间直接降到了2分钟以内。效果立竿见影。
总结一下资源处理的几个关键点:
- 资源合并优先级:主模块 > 本地库 > 远程库
- 限定符匹配:最匹配优先,注意目录完整性
- shrinkResources:配合minifyEnabled使用,小心反射引用
- Non-Transitive R:提升增量编译速度,新项目建议默认开启
资源处理这块,说白了就是AGP帮你做了很多脏活累活。但你要是不理解背后的规则,出了问题就抓瞎。我个人觉得,花点时间把这些机制搞明白,比遇到问题再临时查资料要划算得多。
公众号:蓝海资料掘金营,微信deep3321