App瘦身不只是一个传说

(这是来自 Mikhail Nakhimovich 的第二篇文章。Mikhail Nakhimovich 白天在屡获殊荣的纽约时报安卓 APP 团队担任架构师,晚上则撰写安卓 APP 开发相关的文章,以及在 Friendly Robot 团队帮助开发者开发高性能、用户体验优良的 APP。)

在上一篇文章中,我们探究了一套能保证近乎完美的 APP 启动体验的架构/第三方库的选择。今天我想探究另一种性能优化:APK 的大小。更小的 APK 会带来更快的资源查找和反射调用。图片优化是一种重要的 APK 瘦身手段,能减少更多的掉帧现象。而且用户通常更喜欢更小的 APP,因为它们需要下载的数据更小,安装占用的空间也更小。

今年我有幸参加了 Google I/O 大会,大会上 Wojtek Walicinski 演讲了如何缩小 APK 的大小(“为你的 APP 瘦身”)。我建议大家观看完整的视频,本文将总结视频中的主要内容。我们也将在 Q&A 环节中和来自谷歌的性能优化专家 Boris Farber 进行深入探讨。

为什么 APP 的大小很重要?

一言以蔽之,性能。APP 越小,需要加载的资源也就越少,速度也就越快。在我的 APP 中,移除掉 20K 个方法之后,加载 layout 的速度提升了 30%。

此外,尽管在美国的用户没有数据流量的限制,但世界范围内大多数地区都不是这样。大部分国家都是按使用的数据流量付费,因此更小的 APK 能给用户带来更好的体验。

如何考察 APP 的大小

当我们在谈论 APP 大小的时候,我们到底在谈论什么?目前存在以下一种不同的大小:

  • 原始 APK 文件大小。有趣的是,这是开发者谈论最多的标准,但却是对用户影响最小的。
  • APK 的下载大小。下载一个 APP 需要的数据量。
  • 安装大小。实际安装 APP 需要占用手机的存储空间。
  • 更新大小。更新时需要的空间。

APK 大小

当我们考察 APK 大小时,我们需要记住,APK 只是一个 zip 压缩文件。安卓 APK 文件包含以下几部分:

  • 字节码。包括 classes.dex(APP 的代码)以及引用的第三方库的代码。它们可能是 Java 或者 安卓的。
  • 本地代码(Native code)。各种架构下的 C 库文件(.so 文件)。
  • 资源文件。这基本上就是其他所有的部分了:字符串、颜色、图像、安卓 manifest 以及 META-INF 文件夹。NimbleDroid 实际上能展示上传 APK 的上述不同部分的大小。查看 Facebook 的 APK 大小报告

下载大小

有时候开发者考察的是下载 APK 时的大小,并尝试缩小这个大小。你可能会发现 Resources.arsc 很大,认为字符串是可以压缩的,并对其进行了压缩。这实际上是个坏主意:一旦 APK 下载之后,系统需要解压,并把解压之后所有的资源作为一个单一的字符串保存在内存中。所以压缩你的资源实际上是适得其反的。你可能想知道正常情况下系统是如何加载资源的,实际上系统会把资源文件映射到一块内存区域内,并对读写进行了优化。此外,Play 商店下载的都是压缩之后的 APK 文件,以减小下载大小。在缩减 APK 大小之前,理解这样做到底会导致怎样的情况非常重要。

安装大小

APK 中包含多种 CPU 架构的文件,以及不同设备尺寸的资源文件。不幸的是,在安装之后,手机没有任何办法缩减这一大小。只有安卓 5.0 和 6.0 的系统会运用 ART 技术把 APK 文件压缩为 OAT 的本地文件。安卓 N 会重新使用 JIT 编译期,这意味着安装时不会再有 APP 的优化了。

更新大小

当用户安装更新时,类文件和资源文件的增量会被计算出来。然后增量部分会被压缩,手机会下载这些增量,并计算出新版本的 APK 文件。目前谷歌计算增量的算法是 BSDiff 算法。Play 商店现在显示的是下载的大小而不是 APK 的大小。更新的用户看到的将是更新大小。

Android Studio 2.2 的打包过程也进行了优化,帮助你缩减你的 APK 的大小:

  • 把归档中的文件按名字排序
  • 把所有的时间戳都设置为 0
  • 把归档中的空白都设置为 0
  • 可以存放未压缩的 .so 文件

你可能想知道这些优化是怎样影响 APK 大小的。谷歌最近开源了由 Julian Toledo 开发的 APK Patch Size Estimator。它会显示估计的 APK 磁盘空间大小,新的 gzip 压缩的 APK 大小(新安装用户的下载大小),以及 gzip 压缩的 BsDiff 补丁大小(更新大小)。

下面对上文进行一个小结:

首先对 APK 的 Zopfli 压缩将会适得其反。Zopfli 是谷歌开发的一种压缩算法。好消息是它使得你的 APK 文件变小,但坏消息是它在压缩资源时存在巨大的计算开销,而且可能无法和增量更新的算法兼容。因此不建议使用 Zopfli 对 APK 进行压缩。

缩减图片大小

接下来让我们看看图片压缩。你需要使用外部的工具进行图片压缩。如果你使用了外部工具进行图片压缩,你需要禁用打包过程中的默认图片压缩。你可以在 build.gradle 中这样配置:


aaptOptions {
    cruncherEnabled=false
}

有趣的是,你可以使用 Zopfli 算法压缩 png 图片。它们看起来是一样的(无损),而且一样快。

如果你不想要压缩,你可以使用 WebP。它比 Jpeg 小 30%,在安卓 4.0 以上的系统中支持非透明的 PNG 图片,在 4.2.1 以上的系统中支持带透明度的无损图片。

作为安卓平台特有的格式,Vector Drawable 非常适合 icon。它是基于文本的格式,而且已经有了支持库了。另一种基于文本的格式是 ShapeDrawable,它在安卓 1.0 系统就有了支持。Shape Drawable 非常适合按钮的背景、边界以及梯度效果。

缩减你的代码

缩减代码最好的办法就是 ProGuard。你可能会很讨厌 ProGuard,但你会因为它让你的 APK 方法数没有超过限制而爱上它。开启 ProGuard 很简单。你只需要在 build.gradle 中加上 minifyEnabled true。这个配置会保存一个规则文件列表,规则文件配置哪些类应该被保留。这些规则被保存到 aapt_rules.txt 文件中。而这正是安卓相关文件保存的地方。你也可以通过为类加上 @Keep 注解,来告诉 ProGuard 该类需要被保留。你需要保留所有可能通过反射进行调用的代码。

除了使用 ProGuard,你还可以深入分析你的 APK 中到底包含什么内容。更常见的是一个不大的依赖带来了大量的方法和资源。首先你可以运行 ./gradlew app:dependencies 来分析依赖。它会以树状形式显示 APP 的所有依赖。此外,还有另一个很棒的开源项目 ClassyShark github.com/google/android-classyshark。Classyshark 是一个 APK 文件分析器,用于调试 ProGuard 非常方便(请查看文末对其作者的访谈部分)。

缩减你的资源

另一种缩减 APP 大小的策略是缩减资源的大小,例如没有使用的字符串或者 style。如果你使用了 ProGuard,你可以在 build.gradle 中加入 shrinks.Resources true 来移除没有使用的资源。

更高级的 APP 瘦身

终极的 APK 瘦身策略是把 APP 根据不同尺寸、纹理(游戏开发)或者 CPU 架构进行拆分,所以用户可以只下载他们需要的部分。你可以把你的 APK 拆分为针对特定设备的更小的 APK,并把它们全部上传到 Play 商店中。唯一的缺陷是你需要为每一个 APK 生成不同的版本。而且,拆分 APK 也是很简单的:


android {
    splits {
        density {
            enable true
            exclude ldpi, tvdpi, xxxhdpi
            compatibleScreens small, normal, large', xlarge
        }
        abi {
            enable true
            reset()
            include x86, armeabi-v7a
            universalApk true
        }
    }
}

向专家请教更多关于 Classy Shark 的内容

为了更深入的探讨 APP 瘦身策略,我采访了来自谷歌的专家,Classy Shark 的作者,Boris Farber

Q. 如果一个 APP 运行缓慢,你首先检查的是什么?

我会首先检查依赖。通常来说出问题的都不是我的代码,尤其是在新设备上。通常问题都出在我用的一些不太了解的库。不必过度优化你的代码,要优化你的选择。(注意:NimbleDroid 能显示有问题的依赖)

Q. Wojtek Walicinski 关于 APK 大小的 I/O 演讲中提到了 Classy Shark。你能更详细介绍一下 Classy Shark 吗?

Classy Shark 是一个可以浏览 APK 文件的浏览器。有了它,运行在用户手机上的 APP 也变得可以考察和分析了。你将在工具中进行分享,而不是分享一些 shell 脚本了。

Q. Classy Shark 有哪些有用的场景?

我建议你像用牙刷一样使用 Classy Shark,每天两次。始终弄清楚你的 APK 文件发生了什么。如果你需要引入一个依赖,检查一下 Classy Shark 的结果。你越早发现问题,代价越小。如果你事后才发现问题,修改的时候讲更加痛苦。

Q. 我曾经参与过的项目中,我们会使用并非完美匹配需求的库。你在考虑是否引入一个库的时候,会考虑什么问题?

当我考察一个库是,最重要的是,”这个库是否解决了我的问题?“。我会尝试找 3~4 个备选,然后进行评估。包括查看方法数,它和已有的依赖能否兼容,测量引入之后 APP 的性能,最重要的是它能否解决我的问题。

Q. 你对不把性能作为首要目标的团队中的开发者有什么建议?

我们发现 Play 商店中的评分和 APK 大小与性能是高度相关的。做事不要走极端,不要过度优化。找到具体的瓶颈和问题,然后评估是否值得优化。有的公司会优化吞吐量,而有的公司则优化图像处理。你的方案需要匹配你需要解决的具体问题。