你开发的应用该减肥了--APP终极瘦身方案

1,713 阅读13分钟

随着项目版本的迭代,功能不断丰富,app的体积也愈来愈大,动辄几十MB甚至上百MB,用户看到这么大的体积,相信如果不是版本强制更新,绝大多数用户对版本更新都会敬而远之吧。毕竟更新一次版本的成本也太大了,不说流量不够用,假如网络不好,更新的时间成本也是不可小觑的,这对于体量不是很大的业务线来说是灾难性的,用户的可以选择的替代品太多了,如何能够在如此激烈的竞争环境中脱颖而出呢,降低包大小,让用户高高兴兴的去下载去更新,这也是我们app开发者需要去好好思索的。

APK文件结构

既然要优化包大小,首先要了解apk的组成部分。APK 文件由一个 Zip 压缩文件组成,其中包含构成应用的所有文件。这些文件包括 Java 类文件、资源文件、签名文件等。

  • AndroidManifest.xml:包含核心 Android 清单文件。用于声明应用包名信息、相关的使用权限、四大组件、android sdk版本等
  • class.dex:打包后的字节码,一个dex文件最多支持65535个方法。可能会包含多个dex文件,dex分包是不均匀的,可以理解为装箱子,一个箱子的大小是固定的,但装多少是不确定的。
  • res:包含应用的主要资源文件,包括res/layout、res/drawable等,主要是一些布局文件和图片资源
  • resources.arsc:资源映射表,是编译后的二进制资源文件,描述的是[id、name、value]三者的映射关系。可以通过资源R.java中的ID映射到对应的资源文件或者值
  • assets:包含应用的原始资源文件,打包过程中不会编译,不会生成资源id,应用可以使用 AssetManager对象检索这些资源。
  • lib目录:使用到的各种.so链接库,分析器会检查出项目自身和三方库中所有的so
  • META-INF目录:主要包含APK的签名信息,用于保证apk包的完整性和系统的安全性,让用户避免安装来历不明的apk。其中又包含:
    • 1、MANIFEST.MF:清单文件,当前APK中所有文件清单及其对应hash值

    • 2、CERT.SF:上述清单文件中的每条信息的hash值;

    • 3、CERT.RSA:对CERT.SF文件的数字签名以及签名时所用的数字证书

image.png

优化方案

优化Assets目录

  • 删除无用字体:中文字体不同于英文字体,一个中文字体库是很大的,因此不建议将字体文件随意丢弃到Assets中。对于有些只会用在Logo中的字体,推荐将字体文件进行删减处理。forJrking/FontZip就是一种字体提取工具。
  • 动态下载资源:对于字体、js、html这样的资源,尽量动态下载
  • 压缩资源文件:对于JS代码或者Html代码,可以通过混淆压缩的方案进行压缩,对于非代码类的文件,可以采用压缩存储的方式,在需要的时候将其解压使用。

优化Resources.arsc

resources.arsc中存在一个对应关系 关于文件,第一时间想到的就是压缩存储,在app运行时,要经常用到这个id,说明这个文件需要被频繁的读取。如果将这个文件进行压缩,在第一次读取之前必须进行解压的操作,就会有一些性能和内存的开销,综合考虑是得不偿失的。

  • 删除无用的映射:resources.arsc的正确瘦身方式是删除不必要的string entry,可以借助Android-arscblamer检查出可以优化的部分,例如一些空的引用。
  • 进行资源混淆:微信团队开源了资源混淆工具-AndResGuard,能将资源的名称进行混淆和缩减。可以使用它对该文件进行优化,只是具体优化效果与编码方式、id数量和命名长度有关。被AndResGuard优化后的资源名称会发生改变,一般会所见为一到两个英文字母。 AndResGuard工具其实是一个Task,可以根据需要进行配置即可。对于不需要混淆的可以将其放到白名单中。 具体可参见 github.com/shwenzhang/…

优化META-INF

MANIFEST.MF:是摘要文件,程序会遍历apk包中所有的文件,对非文件夹、非签名文件的文件,逐个编码生成摘要信息,并记录于此。如果逆向修改了任何文件,那么将出现文件和摘要信息不匹配的情况,导致安全校验失败。每一个资源文件都有一个sha1-digest的值,为该文件sha-1的值进行base64编码后的结果。

CERT.SF:是对MANIFEST.MF的签名文件,经过三步生成:

  • 1、系统会把MANIFEST.MF整个文件进行sha1计算,并且计算base64编码后的值。
  • 2、系统会对MANIFEST.MF中的每一条内容分别进行sha1计算,然后再用base64编码。
  • 3、上述两部完成后,系统会将内容写入到CERT.SF文件中。 可以这样理解CERT.SF的作用:
  • 该文件是MANIFEST.MF的二次编码后产生的文件。
  • 如果逆向修改了任何文件,则MANIFEST.MF文件必定会发生改变,从而和CERT.SF不匹配。
  • CERT.SF二次编码的特点是用于在APK安装时校验MANIFEST.MF是否被篡改
  • 若根据修改的文件伪造了一份新的CERT.SF文件,那么数字签名值必定与CERT.RSA中的记录不一样

CERT.RSA:包含了公钥和加密算法等信息,而最重要的信息是“对CERT.SF用私钥进行加密之后的值”。

META-INF文件夹下的文件环环相扣,总结如下:

  • 如果逆向修改了APK包中的文件,那么被修改的文件的摘要和MANIFEST.MF中的信息则不对应
  • 如果修改了某个文件,则必须修改MANIFEST.MF中对应的摘要值,必须保证对应关系
  • 要修改MANIFEST.MF的摘要值,会产生新的MANIFEST.MF,必然和CERT.SF中的记录不匹配
  • CERT.SF中记录了MANIFEST.MF整个文件的编码和其所有内容的编码值,逆向时必须修改CERT.SF
  • 修改了CERT.SF后,安装apk时CERT.RSA文件中的内容和修改后的CERT.SF会不匹配,出现安装失败
  • 逆向者只有拿到了开发者的秘钥才能完全创造一个相同的apk

优化建议:通过分析得出,除了RSA没有缩减机会外,其余两个文件都可以通过混淆资源名称的方式进行压缩。

优化Res目录

  • 打包时剔除无用资源:shrinkResources true shrinkResources意思是收缩资源,将它设置为true,每次打包时就会自动排除无用的资源,不仅作用于图片,还会清理无用的layout资源等,但是只有配合开启混淆才能生效。
  • 删除无用的语言:大部分app其实不需要支持几十种语言,国内应用,可以只支持中文
defaultConfig {
    ...
    resConfig "zh"
}

这样配置后,打包时会排除私有项目、Android support库、三方库中的非中文资源文件。

  • 控制raw中的资源大小:Raw和Assets可以用来存放资源,但两者有以下差异:
  1. Assets目录允许下面有多级子目录,而Raw不允许存在字目录结构。
  2. Assets目录不会产生R文件,Raw则相反
  3. 因为Raw文件会产生R文件的映射,所以可以被lint分析,而Assets不能
  4. Raw不支持子目录让其无法成为存放多种类文件的目录 Raw虽然不会对文件大小有限制,但是存放的音频文件尽量不要使用无损格式,可以考虑同等质量但文件更小的音频格式,如OGG、wav、mp3等格式

减少layout文件

减少layout有两个方法:复用和融合。

复用:把一些页面共用的布局抽出来,这对于layout文件的管理还是瘦身都非常有用。

融合:对于不会被复用的layout呢?

  • shatter是一个类似于Fragment的解耦库,可以为同一个layout中不同区域的view进行逻辑解耦,还可以尽可能少的建立layout文件。
  • 将没有必要复用的header头布局和ListView/RecycleView放在同一个layout布局中,通过代码完成头部的添加

动态下载图片

贴纸、表情这类的文件是相当大的,对于这类图片资源,强烈建议通过在线的方式获取,虽然有一点的复杂度和出错率,但是投入产出比还是不错的。

分目录放置图片

不同分辨率的图片应该放在不同的目录中,如果放错了,对于app运行时的内存大小会有一定的影响。

如果把一个本来应该放在drawable-xxhdpi里面的图片放在了drawable文件夹中,会出现什么问题呢? 在xxhdpi设备上,图片会放大3倍,图片内存占用会变为原来的9倍。

因为不同分辨率的图片大小有差距,很多认为可以使用一套图片来做,不用多套图,借此达到瘦身的目的。但谷歌建议针对不同分辨率出不同的图片。 比较通用的做法是:

  • 聊天表情就出一套图,放在hdpi中(因为此类图片对于清晰度要求不高)
  • 纯色小icon用svg制作,用矢量图适配所有分辨率
  • 对于背景图等大图,出一套放在hdpi或者xxhdpi中
  • logo等权重较大的图片可针对hdpi、xhdpi、xxhdpi做多套图
  • 如果某些图在真机中展示异常,就用多套图适配
  • 如果有特殊机型,可针对性的补图

优化图片资源

图片的优化,最重要的是知道选择什么样的图片格式。

  • 如果是纯色的icon,推荐使用svg
  • 如果是两种以上颜色的icon,推荐使用webp
  • 如果webp无法达到效果,则选择用png
  • 如果图片没有alpha通道,则考虑使用jpg
  • 因为svg是通过xml描述的,所以可以享受到资源优化和代码压缩,通常可以压缩到1kb
  • 对于无透明度的大图,可以换为jpg格式进行有损压缩

webp格式从4.0开始原生支持,知道4.2.1才支持显示含有透明度的webp,一般png转换为webp,大小可以减少一半

另外,在大型项目中,会引入汗多support库以及三方库,如果库中包含一些大图,并且不会用到的话,可以用1x1的同名透明图片替代,达到技能编译通过,又能缩小体积的目的。

针对动画,尤其是帧动画,一直是相当占用资源的,现在可以使用svg动画或者Airbnb公司的Lottie动画库实现,如果使用Lottie可以直接使用json文件描述动画,图片会减少很多。

优化dex

dex文件是class文件被编译后可供art虚拟机理解的文件格式,可以理解为java代码包。

利用lint分析无用代码

可以借助Inspect Code,对工程做静态代码检查。Lint是一个强大的工具,它能做的事情不限于检查无用资源和代码,它能检测丢失的属性、写错的单位、会引起内存溢出的代码等,当然Lint虽然强大,但也会带来一些缺点,就是生成的信息量过大,不适合快速定位无用的代码。除此之外,Lint会提示不要使用枚举方法,如果将枚举变为int,apk的大小也会缩小一些,没减少一个enum,可以减少大约1到4kb的大小。

image.png

删除R文件

Android中的R文件,除了styleable类型外,所有的字段都是int型变量或者常量,且在运行期间都不会改变。可以在编译时记录R中所有字段的名称以及对应值,然后利用ASM工具遍历所有class,将引用R字段的地方替换成对应的常量 ThinRPlugin插件可以方便地将R.xxx的地方替换为具体值,可以减少一部分dex大小。

在最外层的build.gradle中加入如下依赖:

classpath   'com.mogujie.gradle:ThinRPlugin:0.0.2'

在内层的gradle文件中加入如下代码:

apply plugin: 'thinR'

thinR {
 //为了不影响日常开发的编译速度,debug版本可以不用删除R
 skipThinRDebug = true
 }
启用ProGuard:

ProGuard是一款优秀的代码优化、混淆工具,适用于java和Android。关于ProGuard,最需要了解的是它的方法检测机制。它将产出的class作为输入,然后寻找代码中所有的调用点,计算出代码中可达的调用关系图,然后移出剩余的部分,将真正用到的方法变量保留下来进行优化,最终输出一个新的class。

image.png 上图简单描述了什么是可达和不可达的代码

buildTypes {
        release {
            minifyEnabled true
            shrinkResources true //压缩资源
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

虽然这种方式成果显著,但也要配合正确的ProGuard规则才能起作用。

image.png 这张图展示了原始apk和混淆后apk的大小差异

每次构建时,ProGuard都会输出下列文件:

  • dump.txt:说明APK中所有类文件的内部结构

  • mapping.txt:提供原始与混淆过的类、字段、属性、方法之间的映射关系

  • seeds.txt:列出未进行混淆的类和成员

  • usage.txt:列出从apk中移除的类和代码 利用混淆来删除代码的方式是一种保险措施,真正治本的方法是在开发过程中随手删除无用的代码。

  • 使用更小的库或合并现有的库:同一个功能就用一个库,禁止出现一个app中有多个网络库、多个图片库等情况,如果第三方sdk很大,并且申请了各种权限,则考虑换掉它。

  • 根据环境依赖库:Debug模式是开发者的调试模式,这个模式下log全开,并且会有一些帮助调试的工具(apm工具、leakCanary等),可以通过debugImplementation(只在debug模式的编译和最终的debug apk打包时有效)、releaseImplementation(仅仅针对release 模式的编译和最终的release apk打包)来做不同的依赖

结语

控制app的大小非常重要,但也不是一蹴而就的,这是一个持续性的过程,也许隔几个月或者隔几个版本,就需要我们去检查一下大小是否有了足够多的冗余,在开发过程中,及时删除废弃的资源文件和代码也是一个良好的习惯。