APK瘦身最佳实践

本文发表于简书,各位客官如果觉得不错的话,麻烦点一个赞吧。

在开始对APK进行正式的减肥之前,我们先来了解一下APK包的构成,这样才好对症下药嘛。知己知彼方能百战不殆。

APK文件内容速揽

我们可以通过 unzip <apkname>.apk 命令实现对apk的解压(你也可以直接将<apkname>.apk更名为<apkname>.zip后直接通过解压软件解压)。解压后,我们就会得到一个文件夹了,里面包含了:

AndroidManifest.xml
/assets
classes.dex
classes2.dex
...
/lib
    /arm64-v8a
    /armeabi-v7a
    /mips
    /x86
    /x86_64
/META-INF
    CERT.RSA
    CERT.SF
    MANIFEST.MF
/res
    /anim
    ...
resources.arsc

各个文件和目录的作用是什么呢?

文件/目录作用
AndroidManifest.xml清单文件,列出了应用的名称,版本,权限等信息。
assets配置文件,放置一些本地资源,例如本地html,可以通过使用AssetManager检索。
classes.dex, classes2.dex编译后的字节码文件,能够被Dalvik/ART理解。
lib存放特定平台使用的编译后的.so文件,每一个子文件夹对应一个特定的平台,例如armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, and mips.
META-NIF包含 CERT.RSA, CERT.SF, MANIFEST.MF, 存放的签名信息,用于保证系统的安全性和APK文件的完整性。
res存放没有被编译到 resources.arsc文件中的资源文件。
resouces.arsc编译后的二进制资源文件。存放 res/values/ 目录下的所有XML内容。打包工具对XML内容进行提取,编译为二进制,并进行分类,其中,XML内容包含了不同语言的strings和styles,同时还包含没有被直接打包至 resources.arsc 文件的其他文件的路径,例如布局文件和图片文件。

OK,既然我们已经了解了APK的各个组成部分,那么我们就可以针对下面的三个组成部分,采取逐个击破的方式,达到缩减APK体积的目的:

Java代码

我们可以使用Proguard,在编译时对Java代码进行混淆,优化和压缩。Proguard对代码进行遍历,然后剔除其中未被使用的冗余的代码,并对类,属性,接口等进行重命名,从而达到瘦身的目的。

我们可以在 build.gradle 文件中配置Proguard。

buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

在未使用Proguard之前,我们的APK体积达到了13.9M。

开启Proguard后,APK的体积缩减到了10.1M。可见,效果还是很明显的。

除此之外,微信的工程师还开源了一个项目,可以直接对APK进行处理,缩小体积。具体的使用方法:https://github.com/shwenzhang/AndResGuard。

资源文件

Lint

由于Proguard只对Java代码起作用,对于资源文件,它就无能为力了。这个时候,Lint就派上用场了。Lint是一个静态的代码分析器,我们可以通过在 Android Studio -> Analyze -> Inspect code... , 选择范围即可开始就检查。

Lint在检查完成后,会提供一份详细的资源文件清单,并且将没有用到的资源在 UnusedResources:Unused resources 区域。只要我们没有通过反射使用这些资源,就可以放心的删掉它们了。

下面是我根据lint的提示,剔除了部分无用资源后,APK的体积:

Notice:需要注意的是,Lint并不会扫描 assets 目录,所以最好还是手动的检查一下 assets 目录下是否有未被使用的文件吧。

我们还可以通过配置 shrinkResources 来移除未使用的资源。不过,使用 shrinkResources 必须开启代码混淆。在处理过程中,ProGuard 会移除未被使用的代码,但是不会移除资源。而开启 shrinkResources 后,Gradle就会移除资源了。

下面为开启 shrinkResources 后APK体积的变化:

又缩小了一点点。

图片压缩

对于资源文件的优化,其实占大头的还是图片。Android打包本身会对png进行无损压缩,在不那么影响图片显示效果的情况下,对图片进行有损压缩对apk体积的减小还是非常可观的。

我们先使用TinyPng对项目中四张比较大的图片进行压缩,可以看出,效果还是非常不错的。

使用TinyPng后,我们还可以将图片转换为 WebP 格式,进一步缩小图片的体积。不过需要注意的是,WebP格式仅适用于Android 4.0+版本,如果你的应用需要兼容Android 2.3,那么需要额外的引入 .so 文件,apk的体积自然也会增加。所以,根据你的需求,权衡利弊吧。

Android Studio 中,选定需要转换的图片,然后右键鼠标,即可在菜单底部看到 Convert to WebP 的选项了,然后就可以进行转换的操作。

经过上面的处理,我们的App体积来到了9.6M。

矢量图

如果你的项目中使用 appcompat 兼容包,并且版本在23.2以上,那么,使用矢量图或许是个不错的选择。矢量图和分辨率无关,理论上支持任何级别的缩放。以一张常用的 点赞 图标为例,在 Android中使用 Vector 矢量图,大小为:

而它对应的xxxhdpi的png格式,大小为:

效果显而易见,但是这也并不是意味着,我们就可以将所有的图片替换为 矢量图 了 。我们来看一看 ic_favorite_black_24dp.xml 的源代码:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportWidth="24.0"
        android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
</vector>

正如你所见,vector的颜色是单一的颜色,所以,vector 适用于图标等颜色单一的图片,如果是颜色比较复杂,那么vector很明显就不合适了。

使用JPG

对于非透明的大图,JPG格式将会比PNG格式的大小有显著的优势,虽然不是绝对的,但是通常会减小到一半都不止。在启动页,活动页等之类的大图展示区采用JPG将是非常明智的选择。

对于图片的使用,Google的建议,简单来说就是:VD->WebP->Png->JPG

  • 如果是纯色的icon,那么用svg
  • 如果是两种以上颜色的icon,用webp
  • 如果webp无法达到效果,选择png
  • 如果图片没有alpha通道,可以考虑jpg

复用图片

项目中我们还可能遇到图片内容相同,仅仅是颜色不同的情况。这个时候我们就可以使用Android提供的着色来完成,而不用提供好几套图片。例如在Android 5.0+上我们可以使用 android:tintandroid:tintMode,在低版本中可以使用 ColorFilter

对于那些内容颜色等都相同,只是方向不同的图片,我们可以只保留一中方向的,其他方向的图片通过代码实现。例如我们可以对上面提到的 ic_favorite_black_24dp 进行翻转。创建一个 drawable

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_favorite_black_24dp"
    android:fromDegrees="180"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toDegrees="180" />

虽然Android设备的分辨率非常的多,但这并不代表着我们需要为每一种分辨率都准备一套资源。在显示差异不大的情况下,我们可以尽量复用一套图片资源,一套布局,然后再考虑特定屏幕密度。

语言资源

说完了图片,我们还要来说说语言资源。对于大多数的应用,并不需要支持几十种的国际化。使用Gradle,对语言资源进行配置,也可以达到应用瘦身的目的。例如,我们的应用如果只需要支持中文和英语:

android {
    defaultConfig {
        ...
        resConfigs "en", "zh"
        // 支持分辨率
        resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
    }
}

Native Code

如果你的App使用到了Native code,在不影响功能的前提下,可以考虑去除部分平台对应的代码,例如移除对 armeabi、mips的支持,

android {
    ... 
    splits {
        abi {
            enable true
            reset()
            include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
            universalApk true
        }
    }
 }

当然,如果是特别大的原生库,我们还可以通过网络,从云端获取,而不直接打包在APK中。

其他

  • 别忘了最简单,也最容易忽视的,去除重复的依赖,或者是引用更加轻量级的库,也可以达到apk瘦身的效果。
  • 在Release版本中,去除那些只会在debug时才会出现的代码。
  • 对于那些使用频率很小的文件或者是图片,可以存放到云端后,通过网络加载。

结语

相对小的体积能够在用户安装前就给用户留下不错的印象,但是,这不并代表着我们可以为了追求APK体积的小巧而过度的牺牲用户体验。权衡利弊后,选择合适的,才是最重要的。

项目完整地址: Espresso - Espresso is an express delivery tracking app designed with Material Design style, built on MVP(Model-View-Presenter) architecture with RxJava2, Retrofit2, Realm database and ZXing.

如果你觉得文章不错的话,请点个赞吧。有其他问题,可以通过以下方式联系我:

Zhihu: https://www.zhihu.com/people/kirin-momo/

Slack: https://androiddevsslack.slack.com

Weibo: http://weibo.com/u/5313690193

Medium: https://medium.com/@TonnyL

Twitter: https://twitter.com/TonnyLZTL