抖音 Android 端包大小优化:资源优化的极致追求

2024-07-22
来源:网络整理

1 概述

随着业务的快速迭代,抖音的包大小呈现爆发式增长。包大小直接影响下载转化率、推广成本、运行内存、安装时间等因素,因此对apk进行瘦身是必要且有利可图的。apk主要由dex、、、meta-data组成,针对每个部分都可以针对性的进行包大小优化。经过一段时间的努力,抖音在包大小优化方面已经取得阶段性成果,目前仍在持续优化中。

其中资源占据APK包大小的很大一部分,优化资源是包大小优化非常重要的一个环节。本着追求完美的原则,本文将详细阐述针对抖音端资源的优化措施。

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

不压缩的情况下,图片大小计算公式为:图片大小=长x宽x图片位深。对于一张原始图片(),如果每个像素代表(RGBA),那么该图片需要的存储大小=,大约8M,对于这么大的图片来说,这是不可接受的。因此,我们使用的图片都是经过压缩的。图片压缩利用了空间冗余和视觉冗余的原理:

2.2 优点

抖音研发团队开发了一款在编译时钩住资源的插件,使用开源算法进行压缩,支持webp压缩,相比一些已知方案有以下优势:

2.3 优点

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

webp的压缩率相对于来说要高一些,所以现在比较推荐这种压缩方式。

它还应用于字节跳动旗下多个产品的图片压缩优化工作中,有以下好处:

2.4 其他

除了压缩和优化图片之外,它还提供以下功能:

大图检测。会在app//目录下生成.txt日志文件,除了转换结果的日志之外,最后还会输出大像素图和大体积图,这里可以设置阈值,方便大图审核,优化包大小;还支持编译阶段检测,若检测到大图则直接编译,可以及时发现大图提交;压缩算法易于扩展,若要接入其他压缩算法只需要继承并实现接口中的work方法即可;支持多线程压缩,将所有任务的执行放到线程池中,大大缩短了执行时间;增加图片缓存,进一步缩短打包时间,当开启多线程+图片缓存时,如果缓存全部命中,整个过程耗时小于10s;缓存路径可配置;压缩质量可配置,满足不同压缩质量要求,缓存文件也会根据不同压缩质量进行保存和命中; 将不包含透明通道的图片扫描到app//目录下。3.webp非侵入兼容3.1和webp选择

哪个压缩率更高呢?网上没找到两种压缩算法压缩率的直接对比,需要更直观的对比,于是做了如下实验:

使用不同的压缩算法对扫描项目中的 1960 张图像进行了比较:

从项目里找出490张图片,新建一个demo,对比一下使用不同算法压缩图片后打包apk的大小:

通过这两组实验对比,我们可以看出webp的压缩率比要好。之前我们手动使用webp工具对抖音项目中的图片全部进行压缩,包体大小减少了约1.6MB。因此我们选择了Webp压缩算法。

3.2 解决方案选择

相比于 webp 压缩算法,webp 的压缩率更高,因此压缩图片应该是更好的选择。不过 webp 对设备的支持存在兼容性问题,4.3 以上才全面支持。从官网得知,如果想在应用中直接使用带透明度的 webp,至少要 18 以上。

包括抖音、今日头条在内的大部分头条应用都是 16 版本,无法直接使用 webp 图片,所以需要兼容低版本。经过一番调研,我们发现了三种兼容方式:

3.3 方案实施

如果要实现非侵入式的兼容,运行时钩子是最好的选择。但是运行时钩子方案需要解决以下问题:

3.3.1 Hook解决方案应稳定可靠

经过对比和调研、、、、等常见的Java hook方案,我们最终选择了如下不需要root但可以hook系统的方法:

3.3.2 钩点应足够收敛

通过阅读源码发现,所有图片的加载、解析过程最终都是调用了 中的方法,例如() 的调用路径如下:

的创建是通过 来实现的,例如View的e(int)的源码如下:

纵观所有加载图片的API,它们都经过了调用流程,会调用相关方法,然后解析不同的资源类型(File\\),由此可以推断是系统通过不同的资源类型加载的统一接口,这一点从类的注释中也可以看出来:

由于系统加载和解析过程足够收敛,并且是通过来实现的,所以这是一个非常好的hook点。

有了稳定的Hook方案,以及足够收敛的Hook点,就可以通过替换关键方法轻松实现解决方案。

4. 多DPI优化

为了适配不同分辨率或不同模式的设备,会为开发者设计多个资源路径,资源配置相同,应用在获取图片资源时,会根据设备配置自动加载适配的资源,但这种配置存在一个问题,就是高分辨率设备里有无用的低分辨率图片,或者低分辨率设备里有无用的高分辨率图片。

一般来说,对于国内的应用市场,App为了减小包大小,会选择一套市场占有率最高的dpi(推荐),兼容所有设备。而对于海外的应用市场,大部分App都会通过打包的方式上传到Play,可以享受动态分配dpi的功能。不同分辨率的手机可以下载不同dpi的图片资源,所以我们需要提供多套dpi来满足所有设备。在项目中,我们的图片有的只有一套dpi,有的则有多套dpi。针对以上两种场景,我们在打包时会进行资源合并、资源复制等操作,以减小包大小。

4.1 DPI 复制(打包)

在国内的项目中,为了降低图片使用量,一般会适配市场占有率高的dpi,比如只保留高分辨率的图片。这样会造成两个问题,一个是现在市面上2k分辨率的手机越来越多,如果未来手机主流分辨率都是,那么项目中修改几千张图片的成本会很高。还有一个问题是公司很多海外产品都是以包的形式上传到Play,不同dpi的资源可以发送给不同设备的用户。但在项目中,还是只有图片发送,无法通过降低dpi来缩小包大小。在巴西,我们80%的用户都是用HDPI的手机,图片占用是HDPI的两倍,所以这部分收益是相当高的。

因此我们通过压缩分辨率的方式,将高分辨率图片降为低分辨率图片,项目业务只存储最高dpi的图片,打包时根据需要进行复制过滤。我们hook了图片压缩任务,在压缩图片前,我们获取所有PNG图片包括依赖库,降低图片分辨率,并放置在对应分辨率文件夹内,然后执行图片压缩任务,防止部分图片重采样后体积增大。

我们只是缩放了图片分辨率,并没有降低图片采样率,所以显示效果没有区别。我们根据不同dpi应该调整到什么分辨率的定义做了一个表格:

我们将一个默认的logo复制到所有dpi,流程如下图,如果mdpi文件夹中没有对应图片,则复制;如果hdpi文件夹中有对应图片,则跳过;如果没有对应图片,但为了避免降低图片精度,无法复制到更高分辨率的文件夹中,则跳过。

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

在复制的时候需要注意这些问题:为了处理所有图片包括依赖库,在资源合并阶段会进行复制,这样会造成 . 目录很多路径下都会有大量额外的图片资源,所以我们在 CI 上开启了该插件,避免在本地打包提交到代码仓库时增加大量新图片。同时由于 . 中会复制多份图片,所以打包过程中需要做多 dpi 去重。CI 上会有并发的场景,同时复制压缩会导致 . 目录下同时存在 a.png 和 a.webp,从而导致错误,所以最后需要扫描并删除同名的 .png 文件。

4.2 多DPI重复数据删除(打包)

对于普通的打包模式(直接输出apk,比如包),我们可以选择只保留一张高分辨率图片,这样高分辨率设备可以获取到合适的图片,低分辨率设备获取时会自动缩放,同时依然保证合理的运行内存。

多dpi图片可以使用内置去重功能去重,但此配置仅对资源进行去重,例如像素密度和屏幕尺寸不会同时去重。抖音采用基于修改的去重方式,可以定义不同配置的优先级和范围。请务必按照优化后的配置保留资源的副本。优化方法如下(灰色数据表示将被删除):

5. 合并重复资源

随着项目的迭代,不可避免地会有相同的资源被重复添加到资源路径中,对于这样的文件,手动处理肯定是行不通的,可以在打包阶段进行自动去重。

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

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

6. 严格模式 6.1 背景

随着项目的迭代,我们会有很多不再使用,但仍然存在于项目中的资源。虽然之前我们可以使用公司的开源字节码插件开发平台开发插件来扫描出一些无用资源,但是因为这一步没有经过无用代码删除,所以扫描结果并不完整。而是在之后运行的官方针对此类无用资源优化的方法,可以将所有无用资源进行标记,并进行优化。

6.2 优点

开启严格模式后,抖音拥有资源超过600个,收益大小0.57MB。

6.3 访问方法

由于其为官方工具,因此详细的访问方法请参阅文档。

抖音压缩素材_压缩衣服抖音_抖音压缩视频教程

6. 原理

默认情况下,是安全模式的,也就是说它会帮我们识别像 val name = .("img_", + 1) val res = .(name, "", ) 这样的代码,保证我们在反射调用资源文件的时候,也能安全的返回资源。从源码上看,它会帮我们识别以下五种情况:

使用最愚蠢但最安全的方法来获取匹配的前缀/后缀字符串,即将应用程序中的所有字符串视为可能的前缀/后缀匹配字符串。

因此在安全模式下,被某个字符串意外匹配到的资源,即便没有用到,也会被保留下来。以我们的项目为例,在com.ss..ugc...中有如下代码:

在安全模式下,这意味着所有以 tt 开头的无用资源都不会被丢弃(这就是为什么打开严格模式时会有那么多以 tt 开头的无用资源)。

当开启严格模式时,其作用是强制关闭此段的字符匹配过程:

当然这也使得我们使用()的时候变得不安全,因为严格模式下不会匹配任何字符串,所以开启严格模式之后我们必须严格检查所有被反射的资源,看是否有我们需要反射的资源!

6.5 兼容性

它是近几年大力推广的一个功能,可以让我们apk按照不同的维度进行生成和分发,也提供了动态分发的功能,但是开启之后如果我们使用会提示如下错误:

好像官方不支持使用App,发现有人提交过这个,是相关的,回复也简单粗暴——有计划,但是没时间:

但正常情况下,如果做得好,我们的App模块很少会引用资源,即使有,也可以通过使用keep.xml来保留这些资源,所以理论上如果我们分别对模块进行反射调用,是没有太大问题的,接下来的检查配置就在阶段

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

这将打开下面的任务。整个代码写起来非常简单,不到50行就可以完成:

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

arsc文件中记录了资源id和资源全路径的映射关系,App通过资源id获取对应的资源,因此对映射关系中的资源路径进行名称混淆可以减少包大小。

抖音已启用微信开源的资源混淆,并加入了去MD5、多个DPI只保留一个资源等优化。由于公司海外产品较多,上架Play时需要经过aab,所以团队做了兼容aab的资源混淆--(开源|:AAB资源混淆工具),已开源。

8.ARSC 瘦身 8.1 背景

.arsc 文件在很多项目中占用了相当大空间,常见的优化方式有使用混淆减少文件名和目录长度、7z 压缩、如果有海外产品可以动态分配语言等。在我们做了这些优化之后,因为公司内部海外产品较多,涉及多种语言,ARSC 还是非常大的,所以我们决定尝试进一步的优化。经过排查,我们最终优化了三个方面,分别是删除无用的名称、合并字符串池中的重复字符串、删除无用的文案,最终收益为 1.6MB。在此之前,我们也基于同样的原理完成了重复 MD5 文件镜像的合并。

8.2 原理

首先贴一张arsc结构图,这个二进制文件的数据结构还是比较复杂的,其实这个文件只是修改了很小一部分,至于更多的修改,我们无能为力,所以就自己解析这个文件进行分析了。网上对这个文件格式的介绍也很多,这里就不细说了,推荐老罗和的博客和源码。-和提供的代码也值得一看。

下图简单描述了一下修改过程:

如图所示,字符串其实就是通过索引获取的,所有的字符串都存放在两个字符串池(单个)中,一个是全局字符串池,一个是下层字符串池,我们只需要修改指向全局字符串的偏移值即可,名字和它的二进制位置如下图所示。

8.3 解决方法 8.3.1 删除无用的名称

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

首先我们需要通过调用判断哪些名字被配置成了白名单,遍历名字项,如果不在白名单中,就把这个名字的偏移量替换成0指向[],如果名字在白名单中,就不应该删除,我们通过备份字符串池找到这个名字对应的字符串,添加到字符串池中,并将偏移量指向对应的索引。

通过本次优化,抖音包大小减少了70k。

8.3.2 合并重复字符串

这个对应的是全局的字符串池,虽然名字听起来不会有重复的值,但是我们扫描排序之后发现,其实有很多重复的字符串(如果使用包装的话就不会有这个问题),在项目中,这个字符串池中有1k+个重复的字符串,非常有必要对这些字符串进行合并。

我们首先遍历所有的数据,然后合并字符串池中重复的字符串,记录偏移量的修改,最后将引用指向新的偏移量。这个过程需要操作arsc数据结构的sum,保证所有类型的值都能被替换。

通过本次优化,抖音包大小减少了30k。

8.3.3 删除无用文字

在打包过程中,.xml 中保存的所有字符串都不会被优化。随着项目的增长,一些废弃的文本或者在下一版本会用到的文本被引入到了 apk 中。我们再次扫描了一下,发现了 3000 多个无用的字符串。在公司内部的一些海外项目中,一些文本被翻译成了 100 多个国家的语言,占用了大量的空间。

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

不同项目的收益可能有所不同,公司内部的海外项目将这些无用的文本替换掉了,包大小减少了约1.5M。

8.4 实施

如果是普通的打包,在打包过程中直接获取ap_文件中的arsc文件,然后使用我们的工具进行修改。

如果这样打包的话,修改ap_是没用的,因为最终的产物是aapt格式生成的.pb文件,修改它的唯一方法就是hook aapt进程。这个文件和arsc文件的结构不一样。好在我们可以使用官方提供的类来解析生成pb文件,使用类似的方法修改即可。

修改效果如图:

8.5 进一步优化

arsc中的偏移数组还有优化的空间,后续我们会尽力优化。用二进制编辑器打开arsc文件时,可以发现文件中有大量的FF值。

是什么原因造成这么大的空间浪费呢?我们可以看到下图中方框里的空白处,每一个都代表了它字符串的偏移值,这里是没有值的,FF FF FF FF 被赋值为默认的偏移值,浪费了4个字节的空间。有些列()可能只有少数几个格子有值,比如抖音里有4k+张图片,24列,大部分只有几张图片,所以就浪费了4k*23*4≈380k。粗略估算,抖音可以缩小1M大小。(压缩前)

如下图所示,对于arsc文件的处理,我们可以把一行中只有一个值的id提取出来,单独放到一个Type中,每个id只有一个值,避免了上面的空间浪费。但是这样修改了ID,那么对应代码中的ID也必须修改,涉及到反向XML和dex,增加了修改成本。还有一种思路就是修改aapt源码,这样不如直接修改arsc灵活。

9. 总结

以上是我们在抖音端优化包裹大小方面的一些资源方面的尝试和积累,力求追求完美。

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

除了优化措施,好的包大小监控体系是防止包大小恶化最重要的利器,否则包大小优化措施带来的收益无法抵消业务快速迭代带来的包大小增长。抖音结合CI、Cony平台开发了代码合并预检体系,每个分支增量超过阈值则不允许合并。还开发了分业务线包大小监控工具,方便监控各业务线包大小增长情况,为各业务线设置包大小指标。

最后,抖音诚招对技术充满热情的合伙人,感兴趣的合伙人可以通过字节跳动招聘官网(“链接”)查看抖音相关职位,也可以将简历发送至。

分享