一、前言
百度APP Android包体积优化实践系列文章的前两篇分别介绍了体积优化的整体方案和Dex行号优化的具体内容。Dex行号优化基于尽可能减少Dex文件中的DebugInfo 体积来优化包体积。资源优化则通过优化APK中的资源项来优化包体积,本文我们会介绍百度APP 在资源优化上的实践。首先介绍 APK 中资源部分的结构,然后对比分析现存的资源优化工具,介绍百度App自定义优化开发方案,最后还会介绍一些带来其他收益的资源优化。
百度APP Android包体积优化实践系列文章回顾:
百度APP Android包体积优化实践(二)Dex行号优化
二、APK资源项
APK 结构
res/ 资源通常包括用到的各种静态内容,如位图,颜色,布局定义,用户界面字符串,动画等等,这些资源一般放置在项目的 res/ 下特定子目录中。
对应资源目录名称格式如下:
<resources_type_name>-<qualifier_1>-<qualifier_2>
resources_type_name 即资源类型,必须完全匹配,否则不会被编译链接到APK中。Qualifier 即配置标识,可添加多个 qualifier 以匹配到最适合的资源,是多机型适配的基础。qualifier 的内容及顺序必须完全匹配,否则会编译失败,提示 Invalid resource directory name。
除了res/raw/下可放任意类型资源外,其他目录下资源文件格式均受严格控制。如果放置了范围外的类型文件会编译失败,提示 The file name must end with <指定的扩展名>,由此可见文件后缀名是编译校验的一部分。后缀名校验通过后,AAPT2还会对资源文件内容进行校验,实际格式与后缀名不匹配的话也会报错。
resource.arsc文件是Apk打包过程中由 AAPT2 根据 res/ 目录下资源生成的一个资源索引文件,负责将代码中的资源引用映射到 res/ 下最合适的资源文件或资源内容。
下图中可以看出 arsc 中的重点信息包括:包名、资源类型、资源ID、资源名、资源配置。
arsc主要信息
arsc DENSE & SPARSE格式
三、现有资源优化工具
// com/android/build/gradle/internal/tasks/OptimizeResourcesTask.class
// OptimizeResourcesTask关联了AAPT2提供的优化项
enum class AAPT2OptimizeFlags(val flag: String) {
COLLAPSE_RESOURCE_NAMES("--collapse-resource-names"),
SHORTEN_RESOURCE_PATHS("--shorten-resource-paths"),
ENABLE_SPARSE_ENCODING("--enable-sparse-encoding")
}
internal fun doFullTaskAction(params: OptimizeResourcesTask.OptimizeResourcesParams) {
// 添加 资源路径优化 参数
val optimizeFlags = mutableSetOf(
AAPT2OptimizeFlags.SHORTEN_RESOURCE_PATHS.flag
)
// 目前enableResourceObfuscation默认为false,且没有提供参数配置,所以不会开启资源名优化任务
if (params.enableResourceObfuscation.get()) {
optimizeFlags += AAPT2OptimizeFlags.COLLAPSE_RESOURCE_NAMES.flag
}
}
从上面的代码可以看出,OptimizeResourcesTask 本质是调用 AAPT2 完成资源优化,目前只使用了SHORTEN_RESOURCE_PATHS,即资源路径优化。优化前后结果对比如下:
资源文件路径优化效果(arsc)
APK 中实际文件路径也发生了变化(但可以发现 res/color/ 目录没有变,稍后我们会讲述原因)。
资源文件路径优化效果
资源缩减是 AGP 初期版本就注册的优化任务,在 MinifyTask (即代码缩减)后执行。
该任务会对资源的声明及使用(包括源码使用、manifest 使用、资源内部使用)进行分析,最终会将仅声明未使用的资源文件替换为预先设定好的 Dummy entry(即该文件格式下的最小体积格式化文件)。
但是优化的同时也存在一些限制:
必须启用严格模式
没有完全删除无用的资源文件
没有删除无用的value资源
针对后两个问题,AGP4.2+ 也提供了实验性选项 android.experimental.enableNewResourceShrinker.preciseShrinking(AGP7.1以下还需同时启用新资源缩减器 android.experimental.enableNewResourceShrinker),开启后可利用 AAPT2 完全移除无用资源文件,同时移除 arsc 中的无用资源。但因为优化在链接任务之后,资源 ID 已经分配完毕,所以被移除的资源还是会保留填充占位(DENSE格式)。优化效果如下所示:
启用 preciseShrinking 效果
MinifyTask —> ShrinkResourcesTask —> OptimizeResourceTask(自定义 & 官方) 任务的顺序是不可变的。
resConfigs 是 BaseFalvor 提供的资源配置选项,可配置多个资源配置项,最终非这些配置项的资源不会被打包进 APK 中。
根据是否为分辨率配置,resConfigs 的具体实现不同(会使用不同的 AAPT2 参数)。
(1) 分辨率配置
分辨率配置最多配置一个值,若配置多个会编译报错 Cannot filter assets for multiple densities using SDK build tools 21 or later. Consider using apk splits instead 。
使用安全优化。优化逻辑如图所示(不会出现NO_ENTRY)。
分辨率配置
// Android detects ColorStateLists via pathname, skip res/color*
if (util::StartsWith(res_subdir, "res/color"))
continue;
该参数的值是配置文件路径,配置文件格式为:type/resource_name#[directive][,directive]
其中 directive 可选项包括:
no_collapse。资源名优化加白。
no_obfuscate。同no_collapse(虽然目前跟no_collapse作用一样,但根据命名看未来有可能会满足混淆需求,资源同名化 小节会讲什么场景下有资源名混淆的需求)。
remove。移除该资源,优先级高于前两类 directive(我们认为这个优先级不合理)。是资源缩减 preciseShrinking 的底层实现。
添加该优化参数后,除了配置文件中的加白资源,其余资源名均会折叠为同一个字符串。
添加该优化参数后,在arsc文件生成的资源映射流程中,会根据arsc的格式选择查找资源 entry 偏移量的方法。这有助于优化 APK 大小,但会降低资源检索性能。SPARSE 格式就是通过这个优化参数开启的。
AndResGuard 是微信提供的Android资源混淆打包工具,国内的 Android 资源优化基础基本是由 AndResGuard 奠定的,是目前应用最为广泛的资源优化工具。支持资源路径混淆、资源名同化、产物压缩。
AabResGuard 是字节于20年开源的资源优化工具,其在 AndResGuard 的基础上,专门针对 AAB 产物进行优化,同时增加资源文件和字符串的去重。
四、百度APP资源优化工具
最终我们选择基于 AAPT2 做二次开发,增加百度App资源优化逻辑。主要出于以下考虑:
(1) 多格式产物支持,包括APK 和 AAB 格式。同时AAPT2支持 resources.ap_ 和 resources.pb 的双向转换。
(2) 未来可见范围内的AGP升级适配,减少版本兼容成本。
(3) 稳定可靠。
在资源优化方面我们首要考虑的就是资源文件路径优化。一般来说,一个资源文件的路径在APK中会体现在以下几处地方,分别是:
(1) resources.arsc文件
通过了解resources.arsc文件结构信息,如下图所示,可以看到在全局字符串池(strPool)中,记录了完整的资源路径。
全局字符串池中的路径信息
(2) 在签名过程产生的MANIFEST.MF文件
如下图所示, 在签名过程中会计算每个文件对应的 SHA1-Digest 值保存在MANIFEST.MF文件中。
MANIFEST.MF文件中资源的摘要信息
(3) APK(ZIP)文件中的数据存储区和中心目录区
我们知道APK文件实际上是ZIP格式,而ZIP文件格式大致可以分为三个部分:数据存储区(File Entry)、中心目录区(Central Directory)以及一个目录结束标识(End of central directory record)。
对于ZIP中的一个文件,文件路径会分别在数据存储区和中心目录区同时保存,例如对于ZIP中一个路径为 res/mipmap-anydpi-v26/ic_launcher.xml 的资源,通过分析其二进制,可以看到文件路径分别存在数据存储区的frFileName字段和中心目录区的deFileName字段中,如下图所示。
数据存储区中的路径信息
中心目录区中的路径信息
std::string ShortenFileName(const android::StringPiece& file_path, int output_length) {
std::size_t hash_num = std::hash<android::StringPiece>{}(file_path);
std::string result = "";
// Convert to (modified) base64 so that it is a proper file path.
for (int i = 0; i < output_length; i++) {
uint8_t sextet = hash_num & 0x3f;
hash_num >>= 6;
result += base64_chars[sextet];
}
return result;
}
private ComplexColor loadComplexColorForCookie(Resources wrapper, TypedValue value, int id,
Resources.Theme theme) {
...
if (file.endsWith(".xml")) {
// xml 格式解析
} else {
// 校验不通过,必须是xml文件
}
...
}
private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
int id, int density) {
...
if (file.endsWith(".xml")) {
// xml 格式解析
} else {
// 其他格式解析
}
...
}
bool ResourcePathShortener::Consume(IAaptContext* context, ResourceTable* table) {
// res/color 和 res/drawable 目录下的xml文件扩展名需要保留
if (util::StartsWith(res_subdir, "res/color") || util::StartsWith(res_subdir, "res/drawable")) {
if (util::StartsWith(extension, ".xml")) {
keep_extensions = true;
}
}
}
路径优化前后对比
资源名字符串
// android/content/res/Resources.java
public int getIdentifier(String name, String defType, String defPackage)
public String getResourceName(@AnyRes int resid)
public String getResourceEntryName(@AnyRes int resid)
// android/content/ContentResolver.java
// URI scheme = android.resource,内部调用的还是Resources.getIdentifier
public final @Nullable InputStream openInputStream(@NonNull Uri uri)
public final @Nullable AssetFileDescriptor openAssetFileDescriptor(@NonNull Uri uri,
@NonNull String mode, @Nullable CancellationSignal cancellationSignal)
resources.arsc 空白占位
五、总结
参考链接
[1] 应用资源
https://developer.android.com/guide/topics/resources/providing-resources#ResourceTypes
[2] AAPT2
https://developer.android.com/studio/command-line/aapt2?hl=zh-cn
[3] ZIP结构
https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.2.0.txt
[4] 使用别名
https://developer.android.com/training/multiscreen/screensizes#TaskUseAliasFilters
[5] ImageOptim
https://imageoptim.com/mac
[6] Jetpack Compose — Before and after
https://medium.com/androiddevelopers/jetpack-compose-before-and-after-8b43ba0b7d4f