首页 » 漏洞 » APK 包瘦身:追上那个胖子

APK 包瘦身:追上那个胖子

 

作为一个集技术与才华于一身的浏览器,今天 又送干货来了!咳咳,敲黑板了!童鞋们快搬好小板凳

引子

APK 大家肯定都很熟悉了,安卓应用安装包文件。而APK的尺寸对于每个产品来说都是一个非常重要的指标。对于如何减小这个数字,有无数的前人总结的或全面、或零散的经验,许多团队也对此做过各种各样的努力,说实话也是一块嚼烂了的口香糖。

如何在此基础上再咀嚼出一丝甜味、再翻滚出新的厚度呢,这个是笔者一直在苦苦思索的问题。

仔细阅读很多其他团队总结出的APK瘦身相关的文章,大体都是讲一个APK包已经胖成那( nèi )样了我们如何让它瘦下来,这是一种促成式的结果导向的思维方式。这种哪胖减哪的方式存在什么问题呢?相信很多同学都与我有同样的遭遇:一次压制、后续报复性反弹。

道家言:一生二、二生三、三生万物,知一、方能知二知三。因此笔者还是想要从根源上去解释APK尺寸这个问题: 一个APK包从根本上如何长到这么胖的,我们如何能在如此频繁的项目迭代中保持它的身材呢 本文将从这个角度来说明我们APK各部分增大到底是因为什么,以及我们对于APK尺寸的影响因素都有哪些误解,继而得出作为开发者的我们怎样才能 从过程上去避免 APK尺寸过分增大的问题。

这大体是一次面向过程的勘探。一些拙见,希望能够提供些新的思路、也欢迎大家指出错误、互相交流、提出建议。

项目初始:APK诞生之初

首先提出一问题: 一个最小的HelloWorld应用APK尺寸可以有多小? 带着这个问题思考,我做了几组循环对照试验,以便于我们对APK尺寸有一个直观的初始认识。在这里我也同时测试了一下很多人关心的,常常作为依赖库引入的support-v4、support-v7、以及design包,对于APK尺寸的最小影响。

Android Build Tools版本:25.0.2

构建工具:gradle_2.2.0

support-v4版本:25.3.1

support-v7版本:25.3.1

app包含内容:ic_launcher.png(3kb)、helloworld代码及必要资源

APK 包瘦身:追上那个胖子

(备注:以上混淆指的是代码混淆,资源混淆在后文中讨论,需要引入第三方库)

通过上面的实验,我们对于签名、proguard以及第三方库引用对于apk尺寸产生的影响应该有了一个更加直观的认识。

精简APK包的包内结构析

APK 包瘦身:追上那个胖子

图1:精简APK包结构分析图

通过反编译神器 Jadx-gui 对刚刚生成的最小签名包进行反编译,可以发现APK包结构主要有以上5部分构成。

那么这五个必要部分、每个部分的到底各自包含一些什么信息呢?

APK包内成分详解:你真的了解它们吗

1. META-INF

META-INF文件夹是做什么的,在jar文件中,我们常常能看见它的身影。要论它的年龄,比Android要大的多。在Android还没有出生的时候,META-INF就已经被广泛用于jar包中存放各种发布、包安全、构建等辅助信息。在Android的APK中,它也承担了类似的职能,主要用于签名验证相关信息的存放。

Android APK 中META-INF的结构:

APK 包瘦身:追上那个胖子

图2:META-INF结构分析图

META-INF文件夹内一般会包含三个信息:MANIFEST.MF、 .SF文件及 .RSA文件。其中MANIFEST.MF是常驻居民,而.SF文件和.RSA文件在签名时才会生成。

MANIFEST.MF 文件

APK 包瘦身:追上那个胖子

图3:MENIFEST.MF 文件内容

如果未签名,MANIFEST.MF文件中只会保留最最基本的构建信息。签名后,文件中会增加APK包内所有文件名及对应的SHA1-Digest的数据指纹,每个三行、排列整齐。

.SF 文件

APK 包瘦身:追上那个胖子

图4:.SF 文件内容

可以看见,.SF文件的结构与MANIFEST.MF文件类似。.SF文件中,包含了MANIFEST.MF文件的SHA1-Digest后的数据指纹,同时包含MANIFEST.MF中每个资源[名称 - 指纹]键值对字符串、SHA1-Digest后的数据指纹。 也正因此,.SF文件的尺寸一般来说与MANIFEST.MF文件的尺寸差不多。

APK 包瘦身:追上那个胖子

:图5:MANIFEST.MF 文件中的一条资源 [名称 - 指纹]键值对

.SF文件对这三行、包含换行符进行SHA1-Digest

.RSA 文件

.RSA文件是一个特殊格式的文件,特定的数据存放在特定的偏移位置,不能直接用文本编辑器打开,可以用keytool、openssl等命令解析其中的相关参数。

.RSA文件内部包含了签名公钥、.RSA文件本身一些信息的(SHA1、SHA256、MD5) + 私钥加密指纹、以及.SF文件的私钥加密指纹。

.RSA文件大小基本固定在1k左右,不会随新增资源、新增代码而增大。

以上三个文件环环相扣,用于安装app时校验包的完整性、是否被第三方篡改。

2. AndroidManifest.xml

AndroidManifest.xml这个文件应该都比较清楚了, 包含四大组件的定义、权限的定义等等,本身内容没有被加密,为纯文本文件,因此通常不会超过100kb大小。需要注意的:如果引入第三方库中同时定义了AndroidManifest.xml,在打包最后会合并成一个大的AndroidManifest.xml文件,这也是一块需要留意的尺寸增量。

3. classes.dex文件

classes.dex包含了代码类的class,当方法数很多开启Multidex的时候,可能会见到classes1.dex、classes2.dex等多个dex的存在。如果代码中,引用了第三方库,那么第三方库中的类和方法也会被打入dex内。

4. res文件夹

res文件夹里有什么呢?想当然地很多人会认为所有的资源文件都会打包至res文件夹内。其实不然,res中会存放所有的文件资源,例如png文件、xml定义的drawable文件等等。而color、dimen、string等逐条定义在xml文件中的资源,并不会被包含在这里,而是被存放在resources.arsc中。

5. resources.arsc文件

resources.arsc文件夹里包含了所有有id的资源的[索引-类型.名称-路径](可以参照R文件)、以及color、dimen、string等资源的[索引-类型.名称-取值]。

需要注意的,该文件是一种特殊格式的文件,特定的数值存在于特定的偏移位置,不能直接用普通的文本编辑器打开。想要查看它的内容,需要用特定的工具,如下图为反编译神奇Jadx-gui_0.6.1的解析结果。(需要注意的是Jadx-gui_0.6.0版本对resources.arsc的解析有bug,不能显示)。

APK 包瘦身:追上那个胖子

图6:resources.arsc文件内容示例

6. lib文件夹

这次简单的实验中并没有lib文件夹的生成,然而在实际的项目中,我们可能会有一些.so库的直接或间接依赖。.so文件占用的尺寸也不容小觑。百度浏览器的apk成分中,so占用的空间就是最大的。

7. 总结

通过以上分析,可以得出以下结论:

APK 包瘦身:追上那个胖子

APK SIZE 守卫实战

通过前面的实验和总结,APK包的各部分结构及相关的增长原因的应该比较清晰了。实际一轮又一轮密集的版本迭代中,我们如何去守住APK的尺寸呢?

APK 包瘦身:追上那个胖子

视觉需求:图片资源增加

1. 最好的图片格式是什么

首先对比一下PNG、JPEG、WEBP三种热门图片格式的优劣:

APK 包瘦身:追上那个胖子

webp压缩率实测

很多人都好奇webp到底有多小,笔者在这里进行了一个小小的实验,实际测试下png无损压缩为webp格式后尺寸的变化:

APK 包瘦身:追上那个胖子

图7:转化前png图片:5.7kb

APK 包瘦身:追上那个胖子

图8:转化后webp图片:4.4kb

这张图片的压缩率在 23% 左右。大量实验时,也存在个别png转化为webp后尺寸反而增大的现象。 总体压缩率在20+% 。Google官方给出的数据为26% 左右。

2. 管中窥豹:PNG压缩算法

虽然webp近年来很受欢迎、使用量有增长之势,但对于Android应用而言,PNG还是主流的图片格式。PNG的精妙之处就在于它的压缩算法。如果想要初步了解PNG压缩算法,笔者在翻博客的时候发现一个很有意思的小脚本: pngthermal ,可以帮助我们去理解png的压缩算法是怎么计算的。

如下图9,我们可以看到正如传统的热力图所示,png压缩度高的地方对应呈现蓝色、png压缩度低的地方对应呈现绿色、黄色甚至红色。

通过图一对比我们首先得出一个直观又简单的推断:① 纯色部分压缩度高、颜色变化复杂处压缩度低

APK 包瘦身:追上那个胖子

图9:图片对比一

再对比一下官方给出的图片,我们可以再次得出以下推断:② 重复的部分会被压缩掉 、③ 线性渐变部分压缩度高(接近与纯色的压缩度)、非线性颜色变化部分压缩度低

APK 包瘦身:追上那个胖子

图10:图片对比二

对于简单理解png尺寸的影响因素,以上三个推断已经比较充足。如果有同学对png压缩算法有进一步的兴趣。请参阅以下国外大牛写的文章,其中详细列举了png的优化算法: https://medium.com/@duhroach/how-png-works-f1174e3cc7b7

3. 图片资源增加应对方案

首先根据前文的分析,我们得出增加一个图片会影响apk的哪些部分: 

a)    res文件夹中会增加该图片

b)    会增加META-INF中两个签名相关文件的大小

c)    会增加resources.arsc文件的大小

图片是否必要?能否复用已有图片?

对于只有色彩变化的图片,可以用ColorFilter一张图轻松实现。

APK 包瘦身:追上那个胖子

图11:ColorFilter实现示例

对于只有简单旋转、位移、裁切、形变的图片,可以一张图Matrix轻松实现。

对于帧动画,如果是简单的旋转、位移、裁切、形变(如下图11),可以用代码实现的尽量用代码实现。

图12:代码实现示例

大图是否能切割成小图?能否去除留白?

对于大图而言,有效信息往往只有几块,剩余的是大量的留白,甚至是不规则纹理型留白。对于png图片来说,留白部分也是会占用空间的。而在我们的实际项目中,视觉同学往往会为了留白的质感,给留白添加纹理。不规则纹理型留白,会灾难性地增加png尺寸(从png压缩章节我们就可以看出)。

是否能变换成.9图?

对于圆角icon、聊天框、等图片,可以做成.9图,一张图片到处复用。

APK 包瘦身:追上那个胖子

图13:.9格式图片

是否能提供位深8bit的图片?

对于应用于手机上的图片,位深32bit的图片与位深8bit的图片,区别在于支持的颜色多少,在高质显示屏上可以看出细微差别,而对于用户肉眼感知而言相差无几,前者的尺寸是后者的好几倍。

图片是否能够压缩?

图片压缩基本可以分成两种思路:在当前格式的基础上进行有损压缩、或转换图片格式。

有很多png压缩工具或线上png压缩网站(tinypng.com)支持图片的有损压缩。他们的原理大同小异。

原理:首先会将高位深色值转化为近似的低位深(8bit)色值。其次会根据png压缩的特性,将一些不规则的跃迁点去掉或者使之趋于线性分布,以保证较高的压缩率。最后会去掉一些没有用的metadata。

APK 包瘦身:追上那个胖子

百度浏览器历史版本中对png进行有损压缩后,图片总体积减少了27.5%

4. 资源压缩的一些坑、一些黑科技和一些TRICKY的点

a)    AAPT:  aapt有个选项要关闭,在build.gradle中需要设置cruncherEnabled = false不然资源会被再压一次,aapt可能会『帮助』你把已经压好的资源压缩得更大。

b)   资源混淆: 这个Android官方并没有给出靠谱方案,然而存在一些好用的第三方库。例如Github上微信团队提出的一个资源混淆方案: https://github.com/shwenzhang/AndResGuard

原理上,资源混淆是将资源的路径名称缩短,继而减少resources.arsc的大小、 同时减少META-INF中签名文件的大小。

实测开启AndResGuard资源混淆后, APK尺寸可以减少1MB左右

c)    shrinkResources:gradle2.0.0版本后增加了这个看起来很牛的属性,需要配合minifyEnabled属性(也就是新版的混淆属性)一起用。它的作用是会帮助你把混淆期间被标记到的、没有被引用的资源变得成很小的默认格式。实际使用中则有些尴尬,这个属性可能会影响一些非直接引用到的资源文件,导致不可预期的bug。

d)   图标可以换成矢量图资源,变成字体文件。例如阿里巴巴给出的iconfont方案,就是以矢量图的字体文件来替换图像资源的思路。使用iconfont方案替换一些简单图标后,百度浏览器中所有简单图标的尺寸可以降低近50%。

e)    .9图片不宜直接使用工具或在线网站压缩,因为很多压缩算法会去除.9相关的metadata,导致.9图片失效

功能需求:代码及依赖库增加

1. ProGuard:dex的瘦身小助手

ProGuard很多人都比较熟悉了。Proguard是android提供的一个免费的工具,它能够移除工程中一些没有引用到的代码,或者使用更短的包名和名称来重命名代码中的类、字段和函数等,达到压缩、优化和混淆代码的功能。

ProGuard为什么能减少APK尺寸呢?

a)    ProGuard会缩短包名类名法名,减少名称导致的包空间消耗。

b)    ProGuard会检查每个类、每个方法的可用性、是否被引用、是否可以到达,因此如果引入第三方库,ProGuard可以帮助我们过滤去大部分不用的方法和类。

然而proguard并不是万能的。 ProGuard相关的常见误区:

Q:ProGuard能够删掉所有不用的方法吗?

A :有些代码是需要-keep的,被-keep的类不会删除其不用的部分。并且有些库是必须-keep的,在精简APK尺寸的时候,需要考虑到这个潜在问题。

Q :很多方法,方法体为空,ProGuard能够删掉空方法吗?

A :ProGuard不能删除空方法。空方法是占空间、占用方法数的,平时开发过程中需要注意到空方法的隐藏开销。

2. 代码及依赖库增加应对方案

增加代码、依赖库会影响apk中哪些部分的尺寸呢?

a)    增加代码:classes.dex中会增加相应代码,及代码引用的类和方法,及代码引用的jar和其他库中的类和方法。

b)    新增.so文件:

①   lib下会增加相应so文件

②   会增加resources.arsc的大小

③   会增加META-INF中两个签名相关文件的大小

c)    新增java依赖库:需要检查是否有无用资源同时引入,增加res的大小。上文中的对照实验可以看出,引入design包后,包大小有明显的增长,除了因为degisn包依赖于support-v7包有很多标记@Keep的代码以外,还由于design自身带有很多资源文件。

应对TIPS:

适当地控制ProGuard,尽量不keep大的依赖库

谨慎引入过大的.so库。适当删减so库。

适当删减依赖库中的资源和代码。

定期清除低频功能、废弃代码。

3. 代码压缩的一些坑、一些黑科技和一些TRICKY的点

a)    lib中的so可以只保留arm相关目录下的so,去除x86目录下的so。因为目前市场上主流的架构是arm架构,并且大部分x86架构兼容arm的so。所以x86的so不必特地保留。如果实在需要支持部分厂商,可考虑特定渠道包打入x86的so。 去除x86的so后,我们的APK尺寸减少了200k左右。

b)    有些同学会以为,xml布局文件中定义的view需要-keep,不让它被混淆掉。而事实是Android已经在你之前想到了这个问题,这些view无需声明-keep。

c)    一些启动无需用到的so,可以通过 7zip压缩,在安装后解压使用。百度浏览器中,我们使用7zip工具的lzma算法,可以将原本26MB大小的so,压缩至11MB, 压缩率高达58% 。而使用zip,只能压缩至14MB,压缩率为46%

APK 包瘦身:追上那个胖子

APK 包瘦身:追上那个胖子

图14:zip和7zip的压缩对比

4. proguard配置中有这么一项,用来保留代码行号,方便定位问题。

在发版时可以去掉,实测可以减少2.8%左右的包尺寸。

-keepattributes SourceFile,LineNumberTable                             

定期体检

随着项目迭代的深入、项目参与人数的增长,APK的尺寸膨胀自然会变得不可控制,那么对于APK尺寸的科学监控,就显得尤为重要。 幸好,已经有很多现成的好工具可以利用:

1. APK成分监控

Apk成分监控很多网站都提供了在线检测的功能,在这里就不赘述,举一个可以免费试试的栗子:NimbleDroid: https://nimbledroid.com/。这是一个国外的项目,上传apk包,就能轻松解析包内成分,让APK中的脂肪无处遁形。

APK 包瘦身:追上那个胖子 图15:百度浏览器apk尺寸分析

2. 代码监控 Android Studio->Analyze-> Inspect Code

这个工具可以帮助我们快速分析出冗余类、冗余方法,帮我们定位弃用的功能点,从而从根本上减少dex的大小。

APK 包瘦身:追上那个胖子

图16:Inspect Code分析示例

3. 废弃资源梳理 

随着项目迭代的脚步不断向前,自然而然会产生许多不用的资源,我们可以通过自动化脚本跑出这部分资源,定期进行删除。

例如,百度浏览器在近6个版本的迭代后,各模块可以跑出来这么多的历史遗留垃圾:

APK 包瘦身:追上那个胖子

图17:废弃资源分析示例

这些资源清理之后,可以省出一大笔资源本身占用的空间、减少resources.arsc的大小。

4. 用图片相似算法找出视觉同学给的重复切图

项目迭代中,我们往往会重复引入一些过去就已经添加过的资源。完全相同的资源可以通过比较md5来找出。不过除了相同资源以外,项目中大量存在的是重复、有细微差别的切图,这个就可以根据图片相似性算法跑出来。

APK 包瘦身:追上那个胖子

图18:相似图片分析(图片来源于网络)

还想再瘦一些:瘦到极致

1. 独立低频业务模块插件化,后下载。历史版本中,我们将图片搜索、语音识别、日夜间主题等独立功能转化为插件后, APK包尺寸减少了4.4MB。

2. 独立大资源后下载。

3. 尝试Facebook的Redex字节码优化方案。

结束语

以上就是我们目前在APK瘦身方面做的一些尝试和积累。其实,对于APK瘦身,其实是一件持续长久的事情,如何在密集的版本迭代中、不断新增新需求的同时,能够不粘连、无残留地删除旧的废弃需求,如何搭建项目结构、实现低耦合可插拔式的子模块功能,这些也是值得我们深思的问题。

希望本文能给致力于减小APK尺寸、致力于打磨产品的程序员工匠们一些启发和借鉴意义。

原文链接:APK 包瘦身:追上那个胖子,转载请注明来源!

0