APK 包瘦身:追上那个胖子

RalParamor 4年前
   <h3>引子</h3>    <p>APK 大家肯定都很熟悉了,安卓应用安装包文件。而APK的尺寸对于每个产品来说都是一个非常重要的指标。对于如何减小这个数字,有无数的前人总结的或全面、或零散的经验,许多团队也对此做过各种各样的努力,说实话也是一块嚼烂了的口香糖。</p>    <p>如何在此基础上再咀嚼出一丝甜味、再翻滚出新的厚度呢,这个是笔者一直在苦苦思索的问题。</p>    <p>仔细阅读很多其他团队总结出的APK瘦身相关的文章,大体都是讲一个APK包已经胖成那( nèi )样了我们如何让它瘦下来,这是一种促成式的结果导向的思维方式。这种哪胖减哪的方式存在什么问题呢?相信很多同学都与我有同样的遭遇:一次压制、后续报复性反弹。</p>    <p>道家言:一生二、二生三、三生万物,知一、方能知二知三。因此笔者还是想要从根源上去解释APK尺寸这个问题: <em> 一个APK包从根本上如何长到这么胖的,我们如何能在如此频繁的项目迭代中保持它的身材呢 ? </em> 本文将从这个角度来说明我们APK各部分增大到底是因为什么,以及我们对于APK尺寸的影响因素都有哪些误解,继而得出作为开发者的我们怎样才能 <em> 从过程上去避免 </em> APK尺寸过分增大的问题。</p>    <p>这大体是一次面向过程的勘探。一些拙见,希望能够提供些新的思路、也欢迎大家指出错误、互相交流、提出建议。</p>    <p>项目初始:APK诞生之初</p>    <p>首先提出一问题: <em> 一个最小的HelloWorld应用APK尺寸可以有多小? </em> 带着这个问题思考,我做了几组循环对照试验,以便于我们对APK尺寸有一个直观的初始认识。在这里我也同时测试了一下很多人关心的,常常作为依赖库引入的support-v4、support-v7、以及design包,对于APK尺寸的最小影响。</p>    <p>Android Build Tools版本:25.0.2</p>    <p>构建工具:gradle_2.2.0</p>    <p>support-v4版本:25.3.1</p>    <p>support-v7版本:25.3.1</p>    <p>app包含内容:ic_launcher.png(3kb)、helloworld代码及必要资源</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/af8891205011902bfecbb82fe690daf6.jpg"></p>    <p>(备注:以上混淆指的是代码混淆,资源混淆在后文中讨论,需要引入第三方库)</p>    <p>通过上面的实验,我们对于签名、proguard以及第三方库引用对于apk尺寸产生的影响应该有了一个更加直观的认识。</p>    <p>精简APK包的包内结构析</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/cb56dd49c101ba6776ef019808c86242.jpg"></p>    <p>图1:精简APK包结构分析图</p>    <p>通过反编译神器 <strong>Jadx-gui</strong> 对刚刚生成的最小签名包进行反编译,可以发现APK包结构主要有以上5部分构成。</p>    <p>那么这五个必要部分、每个部分的到底各自包含一些什么信息呢?</p>    <p>APK包内成分详解:你真的了解它们吗</p>    <p>1. META-INF</p>    <p>META-INF文件夹是做什么的,在jar文件中,我们常常能看见它的身影。要论它的年龄,比Android要大的多。在Android还没有出生的时候,META-INF就已经被广泛用于jar包中存放各种发布、包安全、构建等辅助信息。在Android的APK中,它也承担了类似的职能,主要用于签名验证相关信息的存放。</p>    <p>Android APK <strong>中META-INF的结构:</strong></p>    <p style="text-align:center"><strong><img src="https://simg.open-open.com/show/03b350404f5a1b684fcd74a5557a266e.png"> </strong></p>    <p>图2:META-INF结构分析图</p>    <p>META-INF文件夹内一般会包含三个信息:MANIFEST.MF、 .SF文件及 .RSA文件。其中MANIFEST.MF是常驻居民,而.SF文件和.RSA文件在签名时才会生成。</p>    <p>MANIFEST.MF <strong>文件</strong></p>    <p style="text-align:center"><strong><img src="https://simg.open-open.com/show/37531fad74546fd650df7e8b543b570a.jpg"> </strong></p>    <p>图3:MENIFEST.MF 文件内容</p>    <p>如果未签名,MANIFEST.MF文件中只会保留最最基本的构建信息。签名后,文件中会增加APK包内所有文件名及对应的SHA1-Digest的数据指纹,每个三行、排列整齐。</p>    <p>.SF <strong>文件</strong></p>    <p style="text-align:center"><strong><img src="https://simg.open-open.com/show/b8830d46b84349dd6e4fc820310cd531.jpg"> </strong></p>    <p>图4:.SF 文件内容</p>    <p>可以看见,.SF文件的结构与MANIFEST.MF文件类似。.SF文件中,包含了MANIFEST.MF文件的SHA1-Digest后的数据指纹,同时包含MANIFEST.MF中每个资源[名称 - 指纹]键值对字符串、SHA1-Digest后的数据指纹。 也正因此,.SF文件的尺寸一般来说与MANIFEST.MF文件的尺寸差不多。</p>    <p><strong><img src="https://simg.open-open.com/show/a5e831d67f19cce03a2f6ba578b9dbfa.png"> </strong></p>    <p>:图5:MANIFEST.MF 文件中的一条资源 [名称 - 指纹]键值对</p>    <p>.SF文件对这三行、包含换行符进行SHA1-Digest</p>    <p>.RSA <strong>文件</strong></p>    <p>.RSA文件是一个特殊格式的文件,特定的数据存放在特定的偏移位置,不能直接用文本编辑器打开,可以用keytool、openssl等命令解析其中的相关参数。</p>    <p>.RSA文件内部包含了签名公钥、.RSA文件本身一些信息的(SHA1、SHA256、MD5) + 私钥加密指纹、以及.SF文件的私钥加密指纹。</p>    <p>.RSA文件大小基本固定在1k左右,不会随新增资源、新增代码而增大。</p>    <p>以上三个文件环环相扣,用于安装app时校验包的完整性、是否被第三方篡改。</p>    <p>2. <strong>AndroidManifest.xml</strong></p>    <p>AndroidManifest.xml这个文件应该都比较清楚了, 包含四大组件的定义、权限的定义等等,本身内容没有被加密,为纯文本文件,因此通常不会超过100kb大小。需要注意的:如果引入第三方库中同时定义了AndroidManifest.xml,在打包最后会合并成一个大的AndroidManifest.xml文件,这也是一块需要留意的尺寸增量。</p>    <p>3. <strong>classes.dex文件</strong></p>    <p>classes.dex包含了代码类的class,当方法数很多开启Multidex的时候,可能会见到classes1.dex、classes2.dex等多个dex的存在。如果代码中,引用了第三方库,那么第三方库中的类和方法也会被打入dex内。</p>    <p>4. res文件夹</p>    <p>res文件夹里有什么呢?想当然地很多人会认为所有的资源文件都会打包至res文件夹内。其实不然,res中会存放所有的文件资源,例如png文件、xml定义的drawable文件等等。而color、dimen、string等逐条定义在xml文件中的资源,并不会被包含在这里,而是被存放在resources.arsc中。</p>    <p>5. resources.arsc文件</p>    <p>resources.arsc文件夹里包含了所有有id的资源的[索引-类型.名称-路径](可以参照R文件)、以及color、dimen、string等资源的[索引-类型.名称-取值]。</p>    <p>需要注意的,该文件是一种特殊格式的文件,特定的数值存在于特定的偏移位置,不能直接用普通的文本编辑器打开。想要查看它的内容,需要用特定的工具,如下图为反编译神奇Jadx-gui_0.6.1的解析结果。(需要注意的是Jadx-gui_0.6.0版本对resources.arsc的解析有bug,不能显示)。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/10e2df463b5f861c78969f66be34867f.jpg"></p>    <p>图6:resources.arsc文件内容示例</p>    <p>6. lib文件夹</p>    <p>这次简单的实验中并没有lib文件夹的生成,然而在实际的项目中,我们可能会有一些.so库的直接或间接依赖。.so文件占用的尺寸也不容小觑。百度浏览器的apk成分中,so占用的空间就是最大的。</p>    <p>7. 总结</p>    <p>通过以上分析,可以得出以下结论:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/90d1bc0c09c8e53df911a097306c3963.png"></p>    <p>APK SIZE 守卫实战</p>    <p>通过前面的实验和总结,APK包的各部分结构及相关的增长原因的应该比较清晰了。实际一轮又一轮密集的版本迭代中,我们如何去守住APK的尺寸呢?</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/38feb8023b3b2a990a81c0b81fc2cdee.jpg"></p>    <p>视觉需求:图片资源增加</p>    <p><strong>1. 最好的图片格式是什么</strong></p>    <p>首先对比一下PNG、JPEG、WEBP三种热门图片格式的优劣:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7ea3864f24d7423bcf820d3d2e4aca5a.jpg"></p>    <p>webp压缩率实测</p>    <p>很多人都好奇webp到底有多小,笔者在这里进行了一个小小的实验,实际测试下png无损压缩为webp格式后尺寸的变化:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/556dfb83714fe801cec2f10736eeaf1f.png"></p>    <p>图7:转化前png图片:5.7kb</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8e40bc8c9ff166bc362cfd3726cb81cd.png"></p>    <p>图8:转化后webp图片:4.4kb</p>    <p>这张图片的压缩率在 <strong>23%</strong> 左右。大量实验时,也存在个别png转化为webp后尺寸反而增大的现象。 <strong>总体压缩率在20+% 。Google官方给出的数据为26% 左右。</strong></p>    <p>2. <strong>管中窥豹:PNG压缩算法</strong></p>    <p>虽然webp近年来很受欢迎、使用量有增长之势,但对于Android应用而言,PNG还是主流的图片格式。PNG的精妙之处就在于它的压缩算法。如果想要初步了解PNG压缩算法,笔者在翻博客的时候发现一个很有意思的小脚本: <strong> pngthermal </strong> ,可以帮助我们去理解png的压缩算法是怎么计算的。</p>    <p>如下图9,我们可以看到正如传统的热力图所示,png压缩度高的地方对应呈现蓝色、png压缩度低的地方对应呈现绿色、黄色甚至红色。</p>    <p>通过图一对比我们首先得出一个直观又简单的推断:① 纯色部分压缩度高、颜色变化复杂处压缩度低</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2b78f06b0aebaab47f641b65134ccd95.jpg"></p>    <p>图9:图片对比一</p>    <p>再对比一下官方给出的图片,我们可以再次得出以下推断:② 重复的部分会被压缩掉 、③ 线性渐变部分压缩度高(接近与纯色的压缩度)、非线性颜色变化部分压缩度低</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/82de057d13ee03bdefda16d1b88c561a.jpg"></p>    <p>图10:图片对比二</p>    <p>对于简单理解png尺寸的影响因素,以上三个推断已经比较充足。如果有同学对png压缩算法有进一步的兴趣。请参阅以下国外大牛写的文章,其中详细列举了png的优化算法: https://medium.com/@duhroach/how-png-works-f1174e3cc7b7 。</p>    <p>3. <strong>图片资源增加应对方案</strong></p>    <p>首先根据前文的分析,我们得出增加一个图片会影响apk的哪些部分: </p>    <p>a)    res文件夹中会增加该图片</p>    <p>b)    会增加META-INF中两个签名相关文件的大小</p>    <p>c)    会增加resources.arsc文件的大小</p>    <p>图片是否必要?能否复用已有图片?</p>    <p>对于只有色彩变化的图片,可以用ColorFilter一张图轻松实现。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/cef0e4eabceaf71aca3b36f991cb4d1a.jpg"></p>    <p>图11:ColorFilter实现示例</p>    <p>对于只有简单旋转、位移、裁切、形变的图片,可以一张图Matrix轻松实现。</p>    <p>对于帧动画,如果是简单的旋转、位移、裁切、形变(如下图11),可以用代码实现的尽量用代码实现。</p>    <p>图12:代码实现示例</p>    <p>大图是否能切割成小图?能否去除留白?</p>    <p>对于大图而言,有效信息往往只有几块,剩余的是大量的留白,甚至是不规则纹理型留白。对于png图片来说,留白部分也是会占用空间的。而在我们的实际项目中,视觉同学往往会为了留白的质感,给留白添加纹理。不规则纹理型留白,会灾难性地增加png尺寸(从png压缩章节我们就可以看出)。</p>    <p>是否能变换成.9图?</p>    <p>对于圆角icon、聊天框、等图片,可以做成.9图,一张图片到处复用。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2478c60eb07f49b94799fe895c909e9a.png"></p>    <p>图13:.9格式图片</p>    <p>是否能提供位深8bit的图片?</p>    <p>对于应用于手机上的图片,位深32bit的图片与位深8bit的图片,区别在于支持的颜色多少,在高质显示屏上可以看出细微差别,而对于用户肉眼感知而言相差无几,前者的尺寸是后者的好几倍。</p>    <p>图片是否能够压缩?</p>    <p>图片压缩基本可以分成两种思路:在当前格式的基础上进行有损压缩、或转换图片格式。</p>    <p>有很多png压缩工具或线上png压缩网站(tinypng.com)支持图片的有损压缩。他们的原理大同小异。</p>    <p>原理:首先会将高位深色值转化为近似的低位深(8bit)色值。其次会根据png压缩的特性,将一些不规则的跃迁点去掉或者使之趋于线性分布,以保证较高的压缩率。最后会去掉一些没有用的metadata。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e1e164d042baa81251e53815f388bb81.jpg"></p>    <p>百度浏览器历史版本中对png进行有损压缩后,图片总体积减少了27.5%</p>    <p>4. <strong>资源压缩的一些坑、一些黑科技和一些TRICKY的点</strong></p>    <p>a)    AAPT:  aapt有个选项要关闭,在build.gradle中需要设置cruncherEnabled = false不然资源会被再压一次,aapt可能会『帮助』你把已经压好的资源压缩得更大。</p>    <p>b)   资源混淆: 这个Android官方并没有给出靠谱方案,然而存在一些好用的第三方库。例如Github上微信团队提出的一个资源混淆方案: https://github.com/shwenzhang/AndResGuard</p>    <p>原理上,资源混淆是将资源的路径名称缩短,继而减少resources.arsc的大小、 同时减少META-INF中签名文件的大小。</p>    <p>实测开启AndResGuard资源混淆后, <strong> APK尺寸可以减少1MB左右 </strong> 。</p>    <p>c)    shrinkResources:gradle2.0.0版本后增加了这个看起来很牛的属性,需要配合minifyEnabled属性(也就是新版的混淆属性)一起用。它的作用是会帮助你把混淆期间被标记到的、没有被引用的资源变得成很小的默认格式。实际使用中则有些尴尬,这个属性可能会影响一些非直接引用到的资源文件,导致不可预期的bug。</p>    <p>d)   图标可以换成矢量图资源,变成字体文件。例如阿里巴巴给出的iconfont方案,就是以矢量图的字体文件来替换图像资源的思路。使用iconfont方案替换一些简单图标后,百度浏览器中所有简单图标的尺寸可以降低近50%。</p>    <p>e)    .9图片不宜直接使用工具或在线网站压缩,因为很多压缩算法会去除.9相关的metadata,导致.9图片失效</p>    <p>功能需求:代码及依赖库增加</p>    <p>1. ProGuard:dex的瘦身小助手</p>    <p>ProGuard很多人都比较熟悉了。Proguard是android提供的一个免费的工具,它能够移除工程中一些没有引用到的代码,或者使用更短的包名和名称来重命名代码中的类、字段和函数等,达到压缩、优化和混淆代码的功能。</p>    <p>ProGuard为什么能减少APK尺寸呢?</p>    <p>a)    ProGuard会缩短包名类名法名,减少名称导致的包空间消耗。</p>    <p>b)    ProGuard会检查每个类、每个方法的可用性、是否被引用、是否可以到达,因此如果引入第三方库,ProGuard可以帮助我们过滤去大部分不用的方法和类。</p>    <p>然而proguard并不是万能的。 <strong>ProGuard相关的常见误区:</strong></p>    <p>Q:ProGuard能够删掉所有不用的方法吗?</p>    <p>A :有些代码是需要-keep的,被-keep的类不会删除其不用的部分。并且有些库是必须-keep的,在精简APK尺寸的时候,需要考虑到这个潜在问题。</p>    <p>Q :很多方法,方法体为空,ProGuard能够删掉空方法吗?</p>    <p>A :ProGuard不能删除空方法。空方法是占空间、占用方法数的,平时开发过程中需要注意到空方法的隐藏开销。</p>    <p><strong>2. 代码及依赖库增加应对方案 </strong></p>    <p>增加代码、依赖库会影响apk中哪些部分的尺寸呢?</p>    <p>a)    增加代码:classes.dex中会增加相应代码,及代码引用的类和方法,及代码引用的jar和其他库中的类和方法。</p>    <p>b)    新增.so文件:</p>    <p>①   lib下会增加相应so文件</p>    <p>②   会增加resources.arsc的大小</p>    <p>③   会增加META-INF中两个签名相关文件的大小</p>    <p>c)    新增java依赖库:需要检查是否有无用资源同时引入,增加res的大小。上文中的对照实验可以看出,引入design包后,包大小有明显的增长,除了因为degisn包依赖于support-v7包有很多标记@Keep的代码以外,还由于design自身带有很多资源文件。</p>    <p>应对TIPS:</p>    <p>适当地控制ProGuard,尽量不keep大的依赖库</p>    <p>谨慎引入过大的.so库。适当删减so库。</p>    <p>适当删减依赖库中的资源和代码。</p>    <p>定期清除低频功能、废弃代码。</p>    <p>3. 代码压缩的一些坑、一些黑科技和一些TRICKY的点</p>    <p>a)    lib中的so可以只保留arm相关目录下的so,去除x86目录下的so。因为目前市场上主流的架构是arm架构,并且大部分x86架构兼容arm的so。所以x86的so不必特地保留。如果实在需要支持部分厂商,可考虑特定渠道包打入x86的so。 <strong>去除x86的so后,我们的APK尺寸减少了200k左右。</strong></p>    <p>b)    有些同学会以为,xml布局文件中定义的view需要-keep,不让它被混淆掉。而事实是Android已经在你之前想到了这个问题,这些view无需声明-keep。</p>    <p>c)    一些启动无需用到的so,可以通过 7zip压缩,在安装后解压使用。百度浏览器中,我们使用7zip工具的lzma算法,可以将原本26MB大小的so,压缩至11MB, <strong> 压缩率高达58% </strong> 。而使用zip,只能压缩至14MB,压缩率为46%</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7e1825527f1195564f25cd9a3b8d21c0.png"></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4528410f800aacec90d7449a99903278.png"></p>    <p>图14:zip和7zip的压缩对比</p>    <p>4. proguard配置中有这么一项,用来保留代码行号,方便定位问题。</p>    <p>在发版时可以去掉,实测可以减少2.8%左右的包尺寸。</p>    <p>-keepattributes SourceFile,LineNumberTable                             </p>    <p>定期体检</p>    <p>随着项目迭代的深入、项目参与人数的增长,APK的尺寸膨胀自然会变得不可控制,那么对于APK尺寸的科学监控,就显得尤为重要。 幸好,已经有很多现成的好工具可以利用:</p>    <p>1. APK成分监控</p>    <p>Apk成分监控很多网站都提供了在线检测的功能,在这里就不赘述,举一个可以免费试试的栗子:NimbleDroid: https://nimbledroid.com/。这是一个国外的项目,上传apk包,就能轻松解析包内成分,让APK中的脂肪无处遁形。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f9c91aaf2cb902ac27cdc923722a9a0c.jpg"> 图15:百度浏览器apk尺寸分析</p>    <p><strong>2.</strong> <strong>代码监控 Android Studio->Analyze-></strong> <strong>Inspect Code</strong></p>    <p>这个工具可以帮助我们快速分析出冗余类、冗余方法,帮我们定位弃用的功能点,从而从根本上减少dex的大小。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9792329b3e43443735d462a91f6b232f.jpg"></p>    <p>图16:Inspect Code分析示例</p>    <p>3. 废弃资源梳理 </p>    <p>随着项目迭代的脚步不断向前,自然而然会产生许多不用的资源,我们可以通过自动化脚本跑出这部分资源,定期进行删除。</p>    <p>例如,百度浏览器在近6个版本的迭代后,各模块可以跑出来这么多的历史遗留垃圾:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/791c324eb5cea89ed2f5229bf0978ff1.png"></p>    <p>图17:废弃资源分析示例</p>    <p>这些资源清理之后,可以省出一大笔资源本身占用的空间、减少resources.arsc的大小。</p>    <p>4. 用图片相似算法找出视觉同学给的重复切图</p>    <p>项目迭代中,我们往往会重复引入一些过去就已经添加过的资源。完全相同的资源可以通过比较md5来找出。不过除了相同资源以外,项目中大量存在的是重复、有细微差别的切图,这个就可以根据图片相似性算法跑出来。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6efc8b3348bc1142b372626ef4c714ec.png"></p>    <p>图18:相似图片分析(图片来源于网络)</p>    <p>还想再瘦一些:瘦到极致</p>    <p>1. 独立低频业务模块插件化,后下载。历史版本中,我们将图片搜索、语音识别、日夜间主题等独立功能转化为插件后, <strong>APK包尺寸减少了4.4MB。</strong></p>    <p>2. 独立大资源后下载。</p>    <p>3. 尝试非死book的Redex字节码优化方案。</p>    <h3>结束语</h3>    <p>以上就是我们目前在APK瘦身方面做的一些尝试和积累。其实,对于APK瘦身,其实是一件持续长久的事情,如何在密集的版本迭代中、不断新增新需求的同时,能够不粘连、无残留地删除旧的废弃需求,如何搭建项目结构、实现低耦合可插拔式的子模块功能,这些也是值得我们深思的问题。</p>    <p>希望本文能给致力于减小APK尺寸、致力于打磨产品的程序员工匠们一些启发和借鉴意义。</p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s/w-JnlBRLiSbRRi_btwFDgA</p>    <p> </p>