抖音包大小优化-资源优化
1.概述
随着业务的快速迭代,抖音 Android 端的包大小爆发式增长。包大小直接影响到下载转化率、推广成本、运行内存和安装时间等因素,因此对 apk 进行瘦身是一件很有必要且收益很大的事情。apk 主要由 dex、resource、asserts、native libraries 和 meta-data 组成,针对每一部分,都可以专项去做包大小优化。抖音 Android 端经过一段时间努力,包大小优化已经取得了阶段性的成果。目前仍在持续的优化中。
其中,资源在 apk 包体积中占比很大,针对资源进行优化是包大小优化中很重要的部分。本着追求极致的原则,本文将详细阐述抖音 Android 端针对资源部分的优化措施。
2.图片压缩2.1 图片压缩原理
在不进行压缩的情况下,图片大小计算公式:图片大小=长 x 宽 x 图片位深。一张原始图像(1920×1080),如果每个像素 32bit 表示(RGBA),那么图像需要的存储大小 1920x1080x4 = 8294400Byte,大约 8M,一张图这么大是难以接受的。因此我们使用的图片都是经过压缩的。图片压缩利用的是空间冗余和视觉冗余原理:
空间冗余利用的是图像上各采样点颜色之间存在着的空间连贯性,把本该一个一个像素存储的数据,合并压缩存储,加载时进行解压还原。通常无损压缩利用的就是空间冗余原理。视觉冗余是指人类的视觉系统由于受生理特性的限制,对于图像场的注意是非均匀的,人对细微的颜色差异感觉不明显。 例如,人类视觉的一般分辨能力为 26 灰度等级,而一般的图像的量化采用的是 28 灰度等级,即存在视觉冗余。 通常有损压缩利用的是人的视觉冗余原理,擦除了对人的眼睛来说冗余的信息。2.2 优势
抖音 Android 研发团队开发了 Gradle 插件 McImage,在编译期间 hook 资源,采用开源的算法 pngquant/guetzli 进行压缩,支持 webp 压缩。与 tinypng 等一些已知的方案相比,存在以下优势:
McImage 现支持 webp 压缩,压缩比高于 tinypng,不过 Android 上 webp 需要做兼容,下文会详细介绍;tinypng 不开源,每个账号每个月只能免费压缩 500 张;McImage 使用的压缩算法都是基于开源算法;McImage 不仅可以压缩 module 中的图片,还能压缩 jar 和 aar 中的图片;McImage 支持压缩算法扩展,有更优的压缩算法选择时扩展方便;和行业里其他方案相比,McImage 还能够支持压缩包含透明度的 webp 图片,并且兼容了 aapt2 对资源的 hook。2.3 收益
McImage 支持两种优化方式,这两种优化方式不可同时使用:
Compress,pngquant 压缩 png 图片,guetzli 压缩 jpg 图片;ConvertWebp,webp 压缩 pngpng 图片。
webp 的压缩比要高于 pngquant、guetzli,所以现在更推荐使用 ConvertWebp 这种压缩方式。
McImage 还被应用于字节跳动旗下多个产品的图片压缩优化工作中,收益如下:
包括抖音、今日头条在内的头条系 Android 应用,大部分 minSDK 是 16,无法直接使用 webp 图片,需要做低版本兼容。通过大量调研,找到了三种兼容的方式:
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 点要足够收敛
通过阅读源码,发现所有图片被加载解析成 Bitmap 的过程,最终都调用到了 BitmapFactory 中的方法。 比如 ImageView 的 setImageResource() 的调用路径如下:
由于系统加载解析 Bitmap 的过程已经足够收敛,都是通过 BitmapFactory 来实现,因此 BitmapFactory 是一个非常不错的 hook 点。
有了稳定的 Hook 方案和足够收敛的 Hook 点,方案的实现起来就手到擒来了,利用 dexposed 对 BitmapFactory 里的关键方法进行替换就可以了。
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 的定义制作了一个表格:
而 Resource shrink 使用了一种最笨但却最安全的方法去获取匹配的前缀/后缀字符串,那就是将应用中所有的字符串都认为是可能的前缀/后缀匹配字符串。
所以这就造成了在安全模式下,不小心被某个字符串所匹配到的资源,即使没有被使用也会被保留下来。以我们的项目为例,在 com.ss.android.ugc.aweme.utils.PatternUtils 中,我们有以下代码:
7.资源混淆(兼容 aab 模式)
资源 id 与资源全路径的映射关系记录在 arsc 文件中,app 通过资源 id 再通过 Resource 获取对应的资源,所以对于映射关系中的资源路径做名字混淆可以达到减少包体积的效果。
抖音启用了微信开源的 AndResguard 进行资源混淆,在开源的基础上进行了增加了 MD5 去、多 DPI 只保留一份资源等优化。 由于公司内部有很多海外产品,在上架 Google Play 时需要走 aab,因此团队做了资源混淆的 aab 兼容– aabResguard(开源 | AabResGuard: AAB 资源混淆工具),已开源。
我们先遍历所有数据,然后把字符串池的重复字符串合并,记录偏移的修改,最后把需要修改的 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 文件,使用相似的方法修改即可。
修改效果如图:
是什么导致了这样的空间浪费? 我们可以看到下图中框选的空白,每一个都代表了其字符串所在的偏移值,这里并没有值,赋值 FF FF FF FF 作为默认偏移值,浪费了 4 字节空间。 某些列(configuration)可能就只有几个格子有值,如图抖音中 drawable 有 4k 张图片,有 24 列,大多数 configuration 只有几张图片,因此浪费了 4k*23*4≈380k。大致估算,抖音可以减少 1M 体积。(压缩前)
9.总结
上述就是我们抖音 Android 端在包大小优化方面针对资源做的一些尝试和积累,力求追求极致。
我们针对包大小优化,在其他方面还做了很多优化措施:针对 so 优化,做了 so 合并、stl 版本统一、精简导出符号表和 so 压缩等措施;针对代码优化,细化混淆规则,开发 bytex 插件进行无用代码扫描、acess 方法内联、getter/setter 方法内联、删除行号等优化措施。
除了优化措施,良好的包大小监控系统是防止包大小劣化最重要的工具,否则包大小优化措施取得的收益抵不过业务快速迭代带来的包大小增长。抖音 Android 端结合 CI、Cony 平台,开发出了一套代码合入前置检查系统,每个分支增量超过阈值不准合入;还开发了分业务线监控包大小的工具,便于监控每个业务线包大小增长和给各个业务线定包大小指标。
最后,抖音 Android 诚招对技术有无限热情的小伙伴。感兴趣的小伙伴都可以通过 字节跳动招聘官网查询抖音 Android 相关职位(「链接」) 或简历发送至 shipeiqing@bytedance.com。
更多分享
抖音BoostMultiDex:Android低版本上首次启动时间减少80%(二)
抖音BoostMultiDex:Android低版本上首次启动时间减少80%(一)
欢迎关注字节跳动技术团队
本文来自作者:天问谈创业,不代表小新网立场!
转载请注明:https://www.xiaoxinys.cn/643275.html
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。