01 前言

百度APP Android包体积优化实际系列文章的前两篇别离介绍了体积优化的整体计划和Dex行号优化的具体内容。Dex行号优化基于尽可能减少Dex文件中的DebugInfo 体积来优化包体积。资源优化则通过优化APK中的资源项来优化包体积,本文咱们会介绍百度APP 在资源优化上的实际。首先介绍 APK 中资源局部的构造,而后比照剖析现存的资源优化工具,介绍百度App自定义优化开发计划,最初还会介绍一些带来其余收益的资源优化。

百度APP Android包体积优化实际系列文章回顾:

百度APP Android包体积优化实际(一)总览

百度APP Android包体积优化实际(二)Dex行号优化

02 APK资源项

如下图所示,能够看到APK 中有三局部内容与资源相干:res/ 目录、resources.arsc、assets/ 目录。除了assets/ 目录外,其余两个资源项初始设计目标是为了实现更不便的机型适配和语言适配等,进步兼容性,因而存在一些优化的空间。

APK 构造

**2.1 res/

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还会对资源文件内容进行校验,理论格局与后缀名不匹配的话也会报错。

**2.2 resources.arsc

resource.arsc文件是Apk打包过程中由 AAPT2 依据 res/ 目录下资源生成的一个资源索引文件,负责将代码中的资源援用映射到 res/ 下最合适的资源文件或资源内容。

下图中能够看出 arsc 中的重点信息包含:包名、资源类型、资源ID、资源名、资源配置。

arsc次要信息

通过浏览在arsc中寻找对应资源的源码,能够看到在 LoadedPackage::GetEntryOffset 办法中,有两种资源 entry 偏移量定位形式,其中 SPARSE 格局在Android O+ 引入。咱们以下图为例,假如 0x7f020010 和 0x7f020011 两个 ID 对应的entry为空,则两种形式的布局如下图所示,能够发现 SPARSE 格局在体积上会有优化,但查找资源的工夫复杂度会从O(1)回升到O(logn)。

arsc DENSE & SPARSE格局

**2.3 assets/

assets/ 下的资源属于 raw 文件,raw 文件示意需以原始模式保留的任意文件。从目录构造到文件内容均由开发者间接管制,应用时通过 AssetManager 间接获取。实质上 assets/和 res/ 的资源文件读取形式是一样的,都是 AssetsProvider 将 APK 内对应门路的文件解压映射到内存中。不同的是开发者调用 API 到 AssetsProvider 读取文件之间的门路,res/ 做了更多封装,所以相应地限度也会多一些。

因为 assets 资源文件灵便度很高,通用优化机制对其作用无限,咱们个别会采取后下发的形式间接抹除这部分体积。后续咱们的优化项全副针对 res/ 和resources.arsc开展。

03 现有资源优化工具

**3.1 AGP和AAPT

AGP(Android Gradle Plugin)在编译流程中定义了不少资源优化相干的工作,AGP 资源优化工作底层都是通过 AAPT2 实现的(除了旧资源缩减工作)。AAPT2(Android 资源打包工具)是一种构建工具,Android Studio 和 Android Gradle 插件应用它来编译和打包利用的资源。AAPT2 会解析资源、为资源编制索引,并将资源编译为针对 Android 平台进行过优化的二进制格局。上面介绍一些资源优化相干工作和AAPT2的优化参数。

**OptimizeResources

AGP 4.2 + 注册了一个新的编译工作 OptimizeResourcesTask,顾名思义是对资源进行优化,在 LinkResourcesTask(即资源链接) 或 ShrinkResourceTask (即资源缩减)之后执行。该优化工作在 debuggable false 状况下默认开启,能够应用 android.enableResourceOptimizations = false 手动敞开。

// 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/ 目录没有变,稍后咱们会讲述起因)。

资源文件门路优化成果

**ShrinkResources

资源缩减是 AGP 初期版本就注册的优化工作,在 MinifyTask (即代码缩减)后执行。

该工作会对资源的申明及应用(包含源码应用、manifest 应用、资源外部应用)进行剖析,最终会将仅申明未应用的资源文件替换为事后设定好的 Dummy entry(即该文件格式下的最小体积格式化文件)。

然而优化的同时也存在一些限度:

1、必须启用严格模式

2、没有齐全删除无用的资源文件

3、没有删除无用的value资源

针对后两个问题,AGP4.2+ 也提供了实验性选项 android.experimental.enableNewResourceShrinker.preciseShrinking(AGP7.1以下还需同时启用新资源缩减器 android.experimental.enableNewResourceShrinker),开启后可利用 AAPT2 齐全移除无用资源文件,同时移除 arsc 中的无用资源。但因为优化在链接工作之后,资源 ID 曾经调配结束,所以被移除的资源还是会保留填充占位(DENSE格局)。优化成果如下所示:

  • 启用 preciseShrinking 成果
  • 工作程序

MinifyTask —> ShrinkResourcesTask —> OptimizeResourceTask(自定义 & 官网) 工作的程序是不可变的。

**resConfigs

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)。

分辨率配置

(2) 非分辨率配置

  • 能够配置多个值。例如语言配置能够同时选英文、中文。**

  • 应用激进优化。该类型配置下,非指标配置均会被移除(可能呈现NO\_ENTRY)。**

**splits

splits 的作用是分包,例如依据不同分辨率打多个包。与 resConfigs 的区别是能够指定多个分辨率,一次性出包;但仅反对分辨率配置。谷歌官网倡议,分包需要优先应用 AAB,利用商店不反对 AAB 的状况下再应用splits。

**--shorten-resource-paths

增加资源门路优化参数后,AAPT2 会解决除了 res/color 目录外的全副资源门路,并在指定目录输入优化前后的门路映射文件。

// Android detects ColorStateLists via pathname, skip res/color*if (util::StartsWith(res_subdir, "res/color"))      continue;

但翻看Android源码没有发现对应的应用,只是会对res/color目录下的资源扩展名进行校验,以辨别xml文件和其余格式文件(这里进一步决定了后续的扩展名优化加白策略)。

**--resources-config-path args

该参数的值是配置文件门路,配置文件格式为:type/resource\_name#[directive][,directive]

其中 directive 可选项包含:

  • no\_collapse。资源名优化加白。
  • no\_obfuscate。同no\_collapse(尽管目前跟no\_collapse作用一样,但依据命名看将来有可能会满足混同需要,资源同名化 大节会讲什么场景下有资源名混同的需要)。
  • remove。移除该资源,优先级高于前两类 directive(咱们认为这个优先级不合理)。是资源缩减 preciseShrinking 的底层实现。

**--collapse-resource-names

增加该优化参数后,除了配置文件中的加白资源,其余资源名均会折叠为同一个字符串。

**--enable-sparse-encoding

增加该优化参数后,在arsc文件生成的资源映射流程中,会依据arsc的格局抉择查找资源 entry 偏移量的办法。这有助于优化 APK 大小,但会升高资源检索性能。SPARSE 格局就是通过这个优化参数开启的。

**3.2 AndResGurard

AndResGuard 是微信提供的Android资源混同打包工具,国内的 Android 资源优化根底根本是由 AndResGuard 奠定的,是目前利用最为宽泛的资源优化工具。反对资源门路混同、资源名异化、产物压缩。

**3.3 AabResGuard

AabResGuard 是字节于20年开源的资源优化工具,其在 AndResGuard 的根底上,专门针对 AAB 产物进行优化,同时减少资源文件和字符串的去重。

04 百度APP资源优化工具

最终咱们抉择基于 AAPT2 做二次开发,减少百度App资源优化逻辑。次要出于以下思考:

(1) 多格局产物反对,包含APK 和 AAB 格局。同时AAPT2反对 resources.ap\_ 和 resources.pb 的双向转换。

(2) 将来可见范畴内的AGP降级适配,缩小版本兼容老本。

(3) 稳固牢靠。

**4.1 资源文件门路优化

在资源优化方面我们首要思考的就是资源文件门路优化。一般来说,一个资源文件的门路在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字段中,如下图所示。

数据存储区中的门路信息

核心目录区中的门路信息

因为资源门路同时存在上述到处中央,而且除了MANIFEST.MF文件是可压缩的,其余三处均不可压缩。因而如果能对资源门路进行缩减,带来的将是近乎四倍的收益。例如,对每个资源文件,其资源门路缩减一个字符(占用1byte),依照以上形式所述再乘以四倍的收益,可缩小大概4byte体积,假如一个App中有10000个资源文件,就能够优化将近40k的体积。如果能大幅缩小资源文件门路长度则会带来更显著的收益。

百度App在资源门路方面的具体优化点次要分为以下三点:

**资源文件目录优化

咱们将资源文件所属目录从res/type[-config\_qualifier ]批改为r/,尽可能的缩短了资源文件的门路长度。

**资源文件名优化

咱们通过一致性Hash映射机制,放弃了原资源门路与优化后的门路固定映射,优化后的文件名固定为三个字符,相比原文件名有了显著缩短,理论测试有较少的哈希抵触,这样可能放弃较小的装置差量包,同时也缩小了笼罩装置后首次启动因为资源名称和资源ID变动造成的解体问题。

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;}

**资源文件扩展名优化

除此之外,咱们还较为激进地去掉了大部分文件的扩展名,这样每个资源至多能够优化4个byte。

文件的扩展名次要有两个作用,一是给使用者分别文件格式,二是操作系统默认应用什么软件加载文件,真正的文件格式并不受扩展名影响。对 Android 零碎来说,res文件扩展名也有两个作用:

(1)在编译期利用扩展名疾速校验,限度文件类型。

(2)运行期间获取文件流后,依据扩展名进行不同的解析封装操作(或者再次校验),再传递给下层。

因为咱们的优化是在资源编译之后进行,所以问题1能够不必思考。针对问题2,咱们发现源码中应用扩展名的状况包含:

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 {        // 其余格局解析    }    ...}

剖析下面的代码能够发现,是将 res/color 和 res/drawable 目录下的文件分为 xml 格局和其余格局,所以只须要针对这两类目录下的 xml 格式文件保留扩展名即可。**

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;      }  }}

除此之外,咱们还配置了资源文件门路优化白名单机制,对于须要通过门路查找资源等非凡状况进行了豁免。

通过上述对三种门路优化形式,咱们剖析APK能够直观的看出优化的后果。

门路优化前后比照

**4.2 资源名优化

资源名优化次要蕴含了资源同名化和资源名混同两局部。

如第4章开始处介绍,除了arsc文件中的全局字符串池记录了残缺的资源门路,在arsc中的Package数据块中还保留了所有资源名的字符串池。

资源名字符串

在理论利用中,咱们默认通过资源 id 查找资源内容,对资源名的应用频率非常低,仅限于通过资源名反查资源 id 以及 通过资源 id 获取资源名两种状况。所以资源项名称字符串池所占据的空间即是咱们的优化对象。极限优化后果是,这个池子里仅寄存一个字符串,所有 ResTable\_entry 的资源项名称 index均指向这个池子里仅有的字符串,即所有资源的名字都变得一样了。思考到豁免的需要,咱们也减少了白名单机制。对于资源文件来说,尽管文件名和 ResourceEntryName 的内容是一样的,但本质是两个不同的概念,所以优化与加白都应该离开解决。

因为当初 arsc 不能压缩,资源名对应的字符串都是能够实实在在优化的体积。

在理论应用中,如果调用了以下接口,那么同名化后,不能通过资源名辨别资源,可能会导致某些场景的生效。例如全埋点场景,通常会收集UI控件的名字(也就是资源名)作为惟一标识。在同名化后必须批改为将[资源名,资源类型,包名]作为惟一标识。

// android/content/res/Resources.javapublic 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.getIdentifierpublic final @Nullable InputStream openInputStream(@NonNull Uri uri)public final @Nullable AssetFileDescriptor openAssetFileDescriptor(@NonNull Uri uri,            @NonNull String mode, @Nullable CancellationSignal cancellationSignal)

除此之外,咱们还提供了混同性能,能够输入混同前后的资源名映射文件。对于下面全埋点场景的例子,倡议应用资源名混同。这样既保证了场景的有效性,也能够缩小一部分体积。

**4.3 arsc configuraion稠密条目优化

咱们晓得arsc的Package数据块中蕴含了Type Spec(类型标准数据块)列表,每个Type Spec蕴含了configuraion 列表。每一个资源id是从属于特定Type Spec的,会在该Type Spec上面的所有configuration列表中有对应的res value信息。这是同一个资源ID在不同配置下,找到不同资源值的原理。依据起始偏移量和每个字符串的偏移量数组,咱们就能定位资源。如果这个资源对应的configuration不存在,仍会保留一个res value的空间(值为0)  ,占用4个字节的空间,以满足偏移量查问形式。

如下图中的空白区域所示,对一个名为abc\_edit\_text\_material的资源来说,只存在于默认的drawable目录下,其余配置项均为空白占位,有较大的优化空间。

resources.arsc 空白占位

因而通过优化arsc中不必要的 configuraion,就能够缩小对齐占位。百度App目前次要是以优化源码中的资源目录来实现,删除不必要的资源类型门路,从而达到缩小configuraion的目标。

如第2章介绍,AAPT2曾经反对对稠密条目进行优化,百度App因为minSdkVersion的起因暂未开启。

**4.4 其它优化

下面讲得是集成在编译流程中的体积优化项,还有一部分优化因为工夫起因或者老本起因没有做到工具里,这里也会逐个介绍。这部分优化关系到的不止是体积,还有开发效率等。

**图片文件压缩

图片压缩次要有两种形式:

(1)缩小色彩数。一张图具备色彩数量越多,单个pixel位数就会越多。个别状况下,非渐进色图片只须要256种颜色(即pixel 8bit)。TinyPng采纳的就是这个原理。

(2)移除元数据。图片中会携带版权、相机信息等元数据,能够抉择移除这部分数据。

咱们比照了多种业界图片压缩工具,最终抉择了ImageOptim工具来实现图片压缩。ImageOptim能移除元数据,并反对无损压缩,在磁盘空间和带宽方面收益显著。

**反复资源

反复资源指的是资源内容雷同,但资源门路不同的资源,这个问题会导致反复的体积。咱们能够通过比照md5判断资源文件是否反复。

**类似资源

相较反复资源,类似资源呈现的概率更高、更不容易被发现。对于图片资源,能够应用opecv中集成的特色检测器计算类似度,利用内置资源通常特色点数量少,计算速度快。

反复资源与类似资源最佳的解决方案是协同UE共建资源平台,从源头上晋升资源复用率。

**AAB

从2021年8月开始,谷歌商店要求利用以AAB格局上架,其次要目标是在利用散发处消化机型适配和动静性能造成的体积减少,防止了开发者治理多个分包的麻烦事。

**申明式UI

随着申明式UI 逐步走上前台,越来越多的代替传统的View + xml的格局,逻辑代码与 UI 布局之间的转化隔膜势必会被打消。Compose 带来的长处很多,其中之一即是体积会比View + xml更小。在谷歌官网的 《Jetpack Compose 应用前后比照》 一文说道:Tivi利用在应用了 Compose 后,咱们发现 APK 大小缩减了 41%,办法数缩小了 17%。

05 总结

本文次要介绍了百度APP资源优化计划,其中重点讲述了在资源门路和资源名方面的优化。感激各位浏览至此,如有问题请不吝指正。

——————————END——————————

参考资料:

[1] 利用资源

https://developer.android.com...

[2] AAPT2

https://developer.android.com...

[3] ZIP构造

https://pkware.cachefly.net/w...

[4] 应用别名

https://developer.android.com...TaskUseAliasFilters

[5] ImageOptim

https://imageoptim.com/mac

[6] Jetpack Compose — Before and after

https://medium.com/androiddev...

举荐浏览:

百度APP Android包体积优化实际(二)Dex行号优化

百度APP Android包体积优化实际(一)总览

百度APP iOS端内存优化实际-大块内存监控计划

百家号基于AE的视频渲染技术摸索

百度工程师教你玩转设计模式(观察者模式)

Linux通明大页机制在云上大规模集群实际介绍