抖音包大小优化-资源优化
作者:翔天盛世
发布时间:2022-04-05 21:32
浏览数:1024
1.概述

随着业务的快速迭代,抖音 Android 端包大小爆炸性增长。包大小直接影响下载转化率、推广成本、运行内存、安装时间等因素,因此对 apk 进行瘦身是一件很有必要且收益很大的事情。apk 主要由 dex、resource、asserts、native libraries 和 meta-data 组成,对于每个部分,可以专门做包大小优化。抖音 Android 经过一段时间的努力,包大小优化取得了阶段性成果。目前仍在不断优化。

-优化前优化后的百分比73MB61.5MB15.7%抖音 lite10MB4.9MB51%

资源在 apk 包体积占很大比例,优化资源是包体积优化的重要组成部分。本文将本着追求极致的原则,详细阐述抖音 Android 端对资源部分的优化措施。

2.图片压缩2.1 图片压缩原理

图片大小计算公式不压缩:图片大小=长 x 宽 x 图片位深。原始图像(1920x1080),若每个像素 32bit 表示(RGBA),那么图像所需的存储尺寸 1920x1080x4 = 8294400Byte,大约 8M,很难接受这么大的图片。因此,我们使用的图片被压缩。图片压缩采用空间冗余和视觉冗余原理:

空间冗余利用图像上各采样点颜色之间的空间连贯性,将应一个像素存储的数据合并压缩存储,并在加载压还原。通常,无损压缩使用空间冗余原理。视觉冗余是指由于生理特性的限制,人类视觉系统对图像场的关注不均匀,人们对细微的颜色差异感觉不明显。例如,人类视觉的一般分辨率是 26 灰度等级,而一般图像的量化是 28 灰度等级,即视觉冗余。通常,有损压缩利用人类视觉冗余原理,消除人类眼睛冗余信息。2.2 优势

抖音 Android 研发团队开发了 Gradle 插件 McI ** ge, 在编译期间hook 资源,开源算法 pngquant/guetzli 压缩,支持 webp和 tinypng 与一些已知方案相比,具有以下优点:

McI ** ge 现支持 webp 压缩比高于 tinypng,不过 Android 上 webp 需要兼容,下面会详细介绍;tinypng 不开源,每个账户每月只能免费压缩 500 张;McI ** ge 基于开源算法使用的压缩算法;McI ** ge 不仅能压缩 module 中的图片也可以压缩 jar 和 aar 中图;McI ** ge 支持压缩算法的扩展,选择压缩算法时更方便扩展;与行业内其他方案相比,McI ** ge 还支持包含透明度的压缩 webp 图片兼容 aapt2 资源 hook。2.3 收益

McI ** ge 支持两种优化方法,不能同时使用:

Compress,pngquant 压缩 png 图片,guetzli 压缩 jpg 图片;ConvertWebp,webp 压缩 pngpng 图片。webp 压缩比高于 pngquant、guetzli,所以现在更推荐使用 ConvertWebp 这种压缩方法。McI ** ge 也用于字节跳动旗下多种产品的图片压缩优化,收入如下:

描述收入抖音-Compress9.5MB抖音-ConvertWebp11.6MB火山-ConvertWebp3.6MBVigo-ConvertWebp4MBVigo aab-Compress1.2MBvigo aab-ConvertWebp3.2MB多闪-ConvertWebp3.5MB

2.4 其他

除了压缩和优化图片,McI ** ge 还提供以下功能:

大图检测app/build/mci ** ge_result 将在目录下生成 mci ** ge_log.txt 日志文件,除了输出转换结果的日志外,还输出了大像素图片和大体积图片,阈值可以是 McI ** geConfig 设置在内,方便大图复盘优化包大小;还支持编译阶段检测,直接检测到大图 block 编译,可及时提交大图;压缩算法易于扩展。如果您想访问其他压缩算法,只需继承 AbstractTask,实现 ITask 界面中的 work 方法可以;支持多线程压缩。把所有 task 的执行放入线程池中,大大缩短了 mci ** ge 执行时间;增加图片缓存 cache,进一步缩短包装时间。mci ** ge 的过程不到 10s;缓存路径可配置;可配置压缩质量,以满足不同压缩质量的需要。缓存文件也将根据不同的压缩质量进行保存和命中;扫描不包含透明通道的图片到 app/build/mci ** ge_result 目录下。3.webp 无侵入性兼容性3.1 tinypng 和 webp 的选择

tinypng 与 webp 哪个压缩比更高?网上找不到两种压缩算法压缩比的直接比较,需要更直观的比较,所以做了以下实验:

通过不同的算法压缩对比扫描项目中 1960 的图片:

描述大小原图13463.07KBwebp 压缩4177.18KBtinypng 压缩6732.18KB

从项目中找到 490 图片,新建 demo,压缩图片后,不同算法更包装 apk 的大小:

描述大小原图 APK9617.53KBwebp 压缩 APK3924.06KBtinypng 压缩 APK5386.80KB

通过这两组实验对比,可以看出 webp 压缩比优于 tinypng 的。以前手动使用 。webp 工具压缩了抖音工程中的所有图片,包的大小减少了 1.6MB 左右。因此选择了 Webp 压缩算法。

3.2 方案选型

webp 压缩算法,相较于 pngquant、guetzli、tinypng,webp 压缩比较高,所以 webbp 压缩图片应该是更好的选择。Android 设备对 webp 支持存在兼容性问题, 4.3 以上完全支持。通过官方网站,我们知道我们想直接使用透明 webp,minSDK 至少需要 18。

头条,包括抖音和今日头条, Android 应用,大部分 minSDK 是 16,不能直接使用 webp 图片,需要做低版本容性。通过大量的研究,我们找到了三种兼容性的方法:

-具体 提供优缺点api 兼容性太简单,侵入性太强,必须使用特定接口或特定 View 进行加载LayoutInflater setFactory 兼容性很容易实现。它需要针对所有 I ** geView 及子 View 处理,必须有统一的 Activity、Fregment 的基类处理运行时 hook 替换系统的关键方法和方法可以实现无侵入性的复杂性

3.3 方案实现

要实现无侵入性兼容性,运行时 hook 是最好的选择。但运行时 hook 解决以下问题:

选择的 hook 方案要稳定可靠;hook 点应足够收敛,以确保所有分析图片的操作都能满足预期。3.3.1 Hook 方案要稳定可靠

通过对 Xposed、AndFix、Cydia Substrate、dexposed 等常见的 Android Java hook调查对比 方案,dexposed 不需要 root、又能 hook 系统方法的特点,最终选择 dexposed:

dexposed 在 Dalvik 上部相对稳定,只需针对 4.3 以下手机版做 hook,没有必要考虑版本兼容性和系统升级;通过内部数据,抖音 4.3 用户不多,占用户总数的万分之几,风险较低。3.3.2 Hook 点要足够收敛

通过阅读源代码,发现所有图片都被加载并分析成 Bit ** p 的过程最终调用到 Bit ** pFactory 中的方法。比如 I ** geView 的 setI ** geResource() 调用路径如下:

I ** geView 的 setI ** geResource 过程,Bit ** p 是通过 创建的Bit ** pFactory如 View 的 setBackgroundResource(int resid)源代码如下:

查阅所有加载图片的 api,都会经历 Resources 调用 getDrawable 的过程。将调用到 Drawable 相关方法,然后通过 Bit ** pFactory 分析不同类型的资源(FileByteArrayStreamFileDescriptory)为 Bit ** p。可以推断,Bit ** pFactory 是 Android 系统通过不同的资源类型加载成 Bit ** p 的统一接口,从 Bit ** pFactory 注释也可以看到:

由于系统加载分析 Bit ** p 的过程已经足够收敛了,都是通过 Bit ** pFactory因此 Bit ** pFactory 是个很好的 hook 点。

有稳定的 Hook 方案和足够收敛的 Hook 点,方案实现方便,使用 dexposed 对 Bit ** pFactory 可以替换关键方法。

4.多 DPI 优化

Android 为了适配各种不同分辨率或者模式的设备,为开发者设计了同一资源多个配置的资源路径,app 通过 resource 获取图片资源时,根据设备配置自动加载适当的资源,但这些配置的问题是高分辨率设备包含低分辨率无用图片或低分辨率设备包含高分辨率无用图片。

一般情况下,针对国内应用市场,App 为了减少包的大小,会选择市场份额最高的 dpi(google 推荐 xxhdpi)与所有设备兼容。海外应用市场 APP,大部分都会通过 AppBundle 打包上传至 Google Play,能享受动态分发 dpi 不同分辨率的手机可以下载不同的功能 dpi 图片资源,所以我们需要提供多套 dpi 满足所有设备。在项目中,只有一套 dpi,有的有很多套 dpi,针对上述两种场景,我们在包装时合并资源, ** 资源,减少了包大小。

4.1 DPI ** (bundle 打包)

在国内项目中,为了减少图片的占用,市场占用率一般较高的 dpi 适配,如只保留 xxhdpi 分辨率图片。这导致了市场上 的两个问题2k如果未来手机的主流分辨率是 xxxhdpi,然后修改项目中数千张图片的成本会很高。另一个问题是,公司的许多海外产品都是通过 制造的AppBundle 打包上传到 Google Play 的,能够给不同设备用户下发不同 dpi 资源。但项目中只有 xxhdpi,仍然下发 xxhdpi 的图片不能通过降低 dpi 减小包的大小。在巴西,80%的用户使用 xhdpi 和 hdpi 手机,xxhdpi 图片比 hdpi 占用多了一倍,这部分收入相当高。

因此,我们通过压缩分辨率将高分辨率图片降低到低分辨率,项目业务只存储最高 dpi 图片,打包时需要 ** 筛选。我们在 hook 图片压缩 task,在图片压缩之前,获得所有 ,包括依赖库PNG 图片,使用 Graphics2D 降低图的分辨率文件夹中降低图像分辨率。然后执行图像压缩 task,防止重新采样后图片大小增加。

我们只缩放图片的分辨率,不降低图片的采样率,所以显示效果没有区别。不同的 dpi 应调整具体分辨率,我们根据 Google 的定义制作了一个表格:

我们 ** 一张 xxhdpi 的默认 logo 到所有 dpi,流程如下图所示,xhdpi 和 mdpi 文件夹下没有相应的图片,** ;在 hdpi 有相应的图片,跳过;xxxhdpi 没有相应的图片,但为了避免降低图片精度,不能夹 到更高的分辨率文件** ,跳过。

最终收益如图,公司内海外产品 TikTok 研发团队在使用该方案优化时,ldpi 相比 xxhdpi 减少了 2.5M 包大小。同时,低分辨率手机加载图片时直接加载对应 dpi 图片资源,不再需要对高分辨率图片进行缩放处理,提高了性能。

在 ** 时需要注意这些问题:为了处理包括依赖库中的所有图片,在资源合并阶段进行了 ** ,这样会导致.cache 目录的很多路径下会多出大量图片资源,因此这个插件我们在 CI 上开启,避免本地打包新增大量图片,提交到代码仓库。同时,由于.cache 中被 ** 了多份图片,需要在 assemble 打包流程中进行多 dpi 去重。在 CI 上会有并发场景,同时 ** 和压缩会导致.cache 目录下同时存在 a.png 和 a.webp,出现 Duplicated 错误,因此最后需要扫描删除同名的.png 文件。

4.2 多 DPI 去重(assemble 打包)

针对普通打包模式(直接产出 apk,比如抖音包),我们可以选择只保留一份分辨率偏高的的图片,这样高分辨率设备可以拿到合适的图片、低分辨率设备通过 Resource 获取时会自动进行缩放,依然可以保证合理的运行内存。

多 dpi 图片可以通过 Android 自带的 resConfig 去重,但这个配置只对资源的 qualifier 去重,比如对像素密度和屏幕尺寸不会同时做去重,抖音使用基于 AndResguard 修改的方式对 drawable 去重,可以定义不同配置的优先级和作用范围。根据优化配置确保留一份资源,优化方式如下图(灰色数据表示会被删除):

5.重复资源合并

随着项目的迭代,项目中难免会出现相同的资源被重复添加到资源路径中,对于这类文件,人工处理肯定是不可行的,可以在打包阶段自动去重。

抖音选择在 AndResguard 阶段对所有的资源进行分析,对 md5 相同的资源文件保留一份,删除其余的重复的文件,然后在 AndResguard 写入 arsc 文件时进行将删除的资源文件对应的资源路径指向唯一保留的一份资源文件。优化方式如下图:

下图是抖音 511 版本接入多 dpi 去重与重复资源合并功能的优化结果:

6.shrinkResource 严格模式6.1 背景

随着项目的开发迭代,我们会有许多资源已经不再使用了,但仍然存在于项目中。虽然开源的字节码插件开发平台 ByteX 开发插件在 ProGuard 之前扫描出一些无用资源,但因为这一步没有经过无用代码删除,因此扫描出的结果并不全。而 shrinkResources 是 google 官方提供的优化此类无用资源的方法,它运行在 Proguard 之后,能标记所有无用资源并将其优化。

6.2 收益

抖音 Android 在开启 shrinkResources 严格模式后,shrink 资源数 600+,收益大小 0.57MB。

6.3 接入方法

shrinkResources 是由 Google 官方提供的工具,因此详细的接入方式参考 Google Developer 上的文档即可。

6.4shrinkResources 原理

默认情况下,Resource shrink 是 safe 模式的,即其会帮助我们识别类似val name = String.for ** t("img_%1d", angle + 1)val res = resources.getIdentifier(name, "drawable", packageName)这样模式的代码,从而保证我们在反射调用资源文件的时候,也是能够安全返回资源的。从源码来看,Resource shrink 时会帮助我们识别以下五种情况:

而 Resource shrink 使用了一种最笨但却最安全的方法去获取匹配的前缀/后缀字符串,那就是将应用中所有的字符串都认为是可能的前缀/后缀匹配字符串。

所以这就造成了在安全模式下,不小心被某个字符串所匹配到的资源,即使没有被使用也会被保留下来。以我们的项目为例,在 com.ss.android.ugc.aweme.utils.PatternUtils 中,我们有以下代码:

在安全模式下,这就造成了所有以 tt 开头的无用资源都不会被 shrink 掉(这也就是为什么严格模式一开,ttlive_ 开头的无用资源那么多的原因)。

而严格模式打开后,其作用便是强行关闭这一段的字符匹配的过程:

当然这也就造成了我们在使用 getIdentifier() 的时候是不安全的,因为严格模式下是不会匹配任何字符串的,所以在开启严格模式之后,一定要严格检查所有被 shrink 的资源,是否有自己需要反射的资源!

6.5 shrinkResources 兼容 Dynamic Feature

AppBundle 是 Google 近年来力推的一个功能,它能够让我们的 apk 按照不同的维度生成下发,也提供了一个动态下发功能的方式,Dynamic Feature。但是如果我们在开启 Dynamic Feature 之后使用 shrinkResources,则提示以下错误:

由此看来 Google 官方并不支持 App Bundle 使用 Dynamic Feature 时使用 shrink resource。在 Google Issue Tracker 上发现已经有人对此提交过 Issue 了,相关 Issue。而 Google 的回复也是简单粗暴----计划中,但是没有时间:

但是正常来说,如果做的好的话,我们的 App Bundle 的 Dynamic Feature 模块是很少会引用 Master 的资源的,即使有,使用 keep.xml 的方式也能将这种资源给保留下来。因此,理论上来说,单独对 Master 模块进行 shrinkResource 并注意反射调用的话,是没多大问题的。Dynamic Feature 下检查 shrinkResources 配置是在 Configuring 阶段

因此我们的想法便是在配置阶段不开启 shrinkResources 开关,而在后面执行资源处理任务的时候自行插入 shrinkResources 的 Task:

这样就能在 Dynamic Feature 下开启 shrinkResources 的 Task 了,整个代码编写十分简单,不到 50 行就能完成:

7.资源混淆(兼容 aab 模式)

资源 id 与资源全路径的映射关系记录在 arsc 文件中,app 通过资源 id 再通过 Resource 获取对应的资源,所以对于映射关系中的资源路径做名字混淆可以达到减少包体积的效果。

抖音启用了微信开源的 AndResguard 进行资源混淆,在开源的基础上进行了增加了 MD5 去、多 DPI 只保留一份资源等优化。内部有很多海外产品,在上架 Google Play 时需要走 aab,因此团队做了资源混淆的 aab 兼容-- aabResguard(开源 | AabResGuard: AAB 资源混淆工具),已开源。

8.ARSC 瘦身8.1 背景

resources.arsc 这个文件在很多项目中都占用了相当多的空间。常见的优化方法是使用 AndResGuard 混淆减少文件名及目录长度,7z 压缩,如果有海外产品的话可以动态下发语言。我们在做完这些优化后,内部有很多海外产品,涉及到多语言的关系,ARSC 依然很大,我们决定尝试进一步优化。经过调研,最终我们对 3 个方面做了优化,分别是删除无用 Name、合并字符串池中重复字符串、删除无用文案,最终带来的收益是 1.6MB。在此之前,我们还在 AndResGuard 的基础上完成了重复 MD5 文件图片合并,原理是一样的。

8.2 原理

先贴一张 arsc 结构的图,这个二进制文件的数据结构相当复杂,AndResGuard 其实只修改了这个文件的一小部分,至于更多的修改就无能为力了,于是我们自己解析了这个文件进行分析。网上也有不少关于这个文件格式的说明,这里就不赘述了。推荐老罗和尼古拉斯的博客以及 aapt2 源码。google 提供的 android-arscblamer 和 apktool 的代码也值得一看。

下面用一张图简单描述一下修改过程:

如图,字符串其实是通过索引的方式来获取的,所有字符串都保存在两个字符串池中(单个 package),一个是全局字符串池,一个是 package 下的字符串池,我们只需要修改指向全局字符串的偏移值就行了。name 和 value 所在二进制位置如下图。

8.3 方案8.3.1 删除无用 Name

AndResGuard 在今年的 7 月也增加了这个功能,我们来看一下实现原理。Name 对应的字符串池是 package 字符串池,由于这个字符串池中只包含所有 Name,我们操作可以稍微暴力一点,先做一份备份,然后清空字符串池,添加一个用于替换的字符串,赋值为 [name_removed]。

首先要确定哪些 name 是通过 getIdentifier 调用,配置成白名单。遍历 name 项,如果不在白名单,那么把这一个 name 的偏移替换成 0,使其指向[name_removed]。如果 name 在白名单,那么不应该删除,我们通过备份的字符串池找到这个 name 对应的字符串,添加到字符串池中,把偏移指向对应下标即可。

抖音通过这个优化减少了包大小 70k。

8.3.2 合并重复字符串

value 所对应的是全局字符串池,虽然名字听起来不会有重复值,但在我们扫描排序后发现其实有很多重复字符串(用 AppBundle 打包就不会存在这个问题) 在抖音项目中,这个字符串池里有 1k+个重复字符串,合并这些字符串是非常必要的。

我们先遍历所有数据,然后把字符串池的重复字符串合并,记录偏移的修改,最后把需要修改的 value 的引用指向新的偏移。这个过程需要操作 arsc 数据结构的 ResValuel 和 ResTableMap,以保证所有 string 类型的值都能得到替换。

抖音通过这个优化减少了包大小 30k。

8.3.3 删除无用文案

在打包过程中,其实所有 strings.xml 中保存的字符串都是不会被优化的,随着项目逐渐变大,一些废弃文案或者下个版本才有用的文案被引入了 apk 中,我们在 Proguard 后再次扫描,发现了 3000+个无用字符串。内部的一些海外项目中,有的文案被翻译成 100 多个国家的语言,占用了极大的空间。

删除的方法和上面类似,都是指向替换的字符串所在偏移。如图可能会存在两个不同 name 指向同一个字符串,需要判断待删除的字符串是否还有其他引用。

不同项目收益可能不太一样,公司内部海外项目对这些无用文案进行了替换,减少了 1.5M 包大小左右。

8.4 实现

如果是普通的 assemble 打包,直接在 ProcessResources 过程中获取 ap_文件中的 arsc 文件,利用我们的工具修改即可。

如果是 AppBundle 方式打包,修改 ap_是没有用的,因为最后产物是用 aapt 以 proto 格式生成的 resources.pb 文件,要修改只能 hook aapt 过程。这个文件和 arsc 文件结构不太一样,好在我们可以使用官方提供的 Resources 类解析、生成 pb 文件,使用相似的方法修改即可。

修改效果如图:

8.5 进一步优化

arsc 中的偏移数组是有优化空间的,我们会在未来尝试进行优化。用二进制编辑器打开 arsc 文件可以发现,这样的 FF 值在文件中大量存在。

是什么导致了这样的空间浪费?我们可以看到下图中框选的空白,每一个都代表了其字符串所在的偏移值,这里并没有值,赋值 FF FF FF FF 作为默认偏移值,浪费了 4 字节空间。某些列(configuration)可能就只有几个格子有值,如图抖音中 drawable 有 4k+张图片,有 24 列,大多数 configuration 只有几张图片,因此浪费了 4k*23*4≈380k。大致估算,抖音可以减少 1M 体积。(压缩前)

如下图 facebook 针对 arsc 文件的处理,我们可以把一行只有一个值的 id 抽出来,单独放到一个 Resource Type 中,每一个 id 只有一个值,避免了上述空间浪费情况。但这样做修改了 ID,因此对应的代码中的 ID 也要修改,涉及了逆向 xml 以及 dex,提高了修改成本。还有一种思路是修改 aapt 源码,没有直接改 arsc 灵活。

原创:抖音Android团队,分享链接

抖音包大小优化-资源优化9.总结

上述就是我们抖音 Android 端在包大小优化方面针对资源做的一些尝试和积累,力求追求极致。

我们针对包大小优化,在其他方面还做了很多优化措施:针对 so 优化,做了 so 合并、stl 版本统一、精简导出符号表和 so 压缩等措施;针对代码优化,细化混淆规则,开发 bytex 插件进行无用代码扫描、acess 方法内联、getter/setter 方法内联、删除行号等优化措施。

除了优化措施,良好的包大小监控系统是防止包大小劣化最重要的工具,否则包大小优化措施取得的收益抵不过业务快速迭代带来的包大小增长。抖音 Android 端结合 CI、Cony 平台,开发出了一套代码合入前置检查系统,每个分支增量超过阈值不准合入;还开发了分业务线监控包大小的工具,便于监控每个业务线包大小增长和给各个业务线定包大小指标。

地址:北京珠江摩尔国际大厦
电话:18516882688
邮箱:xcni@qq.com
关注我们
Copyright @ 2010 - 2022 京ICP备11047770号-8 京公网安备11011402012373号