Android包体优化总结

·  阅读 371

应用安装包大小的重要性已经不需要多说,安装包大小直接影响用户的下载,留存,甚至部分运营商上线要求必须小于一定的值。但是随着业务的迭代开发,应用会越来越大,安装包体积不断增长。而随着安装包体积的增大,还会导致应用的安装时间,运行内存和 rom 空间也因此增大,因此 APK 的瘦身已经是不得不考虑的事情。

Apk的构成

可以通过Android studio 自带的 Analyze APK 工具分析apk。

直接将电脑上的 apk 拖进 Android Studio 中就可以自动使用 Analyze APK 打开 apk。然后,我们就可以看到 APK 文件的绝对大小以及各组成文件的百分占比,如下图所示:

在这里插入图片描述

可以看到APK主要由以下部分组成:

文件/目录描述
lib/存放so文件,可能会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多数情况下只需要支持armabi,与x86的架构即可,如果非必需,可以考虑拿掉x86的部分
res/存放编译后的资源文件,例如:drawable、layout等等
assets/应用程序的资源,应用程序可以使用AssetManager来检索该资源
META-INF/该文件夹一般存放于已经签名的APK中,它包含了APK中所有文件的签名摘要等信息
classes(n).dexclasses文件是Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式
resources.arsc编译后的二进制资源文件
AndroidManifest.xmlAndroid的清单文件,格式为AXML,用于描述应用程序的名称、版本、所需权限、注册的四大组件

在了解了APK各个组成部分以及它们的作用后,现在准备从以下几个方面对apk进行"瘦身“:

DEX优化

DexAndroid 系统的可执行文件,包含 应用程序的全部操作指令以及运行时数据Dex 一般在应用包体积中占据了不少比重,并且,Dex 数量越多,App 的安装时间也会越长。所以,优化它们可以说是 重中之重。下面,我们就来看看有哪些方式可以优化 Dex 这部分的体积。

配置ProGuard

可以通过开启ProGuard来实现代码压缩,可以在build.gradle文件相应的构建类型中添加minifyEnabled true

代码压缩会拖慢构建速度,因此应该尽可能避免在调试构建中使用。不过一定要为用于测试的最终APK启用代码压缩,如果不能充分地自定义要保留的代码,可能会引入错误。

配置如下:

buildTypes {
    release {
        // 1、是否进行混淆
        minifyEnabled true
        // 2、开启zipAlign可以让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗
        zipAlignEnabled true
        // 3、移除无用的resource文件:当ProGuard 把部分无用代码移除的时候,
        // 这些代码所引用的资源也会被标记为无用资源,然后系统通过资源压缩功能将它们移除。
        // 需要注意的是目前资源压缩器目前不会移除values/文件夹中定义的资源(例如字符串、尺寸、样式和颜色)
        shrinkResources true
        // 4、混淆文件的位置,其中 proguard-android.txt 为sdk默认的混淆配置,
        // 它的位置位于android-sdk/tools/proguard/proguard-android.txt,
        // 此外,proguard-android-optimize.txt 也为sdk默认的混淆配置,
        // 但是它默认打开了优化开关。并且,我们可在配置混淆文件将android.util.Log置为无效代码,
        // 以去除apk中打印日志的代码。而 proguard-rules.pro 是该模块下的混淆配置。
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        signingConfig signingConfigs.release
    }
}

复制代码

添加完这个配置以后,每次在build release包时,会在${project.buildDir}/outputs/mapping/${flavorDir}/生成以下文件:

在这里插入图片描述

文件名描述
dump.txtAPK中所有类文件的内部结构
mapping.txt提供原始与混淆过的类、方法和字段名称之间的转换,可以通过proguard.obfuscate.MappingReader来解析
seeds.txt列出未进行混淆的类和成员
usage.txt列出从APK移除的代码

我们需要仔细检查最终合并的 ProGuard 配置文件,是不是存在过度 keep 的现象。

可以在混淆配置中添加下列规则输出 ProGuard 的最终配置,尤其需要注意各种的 keep *,很多情况下我们只需要 keep 其中的某个包、某个方法,或者是类名就可以了。

# 输出 ProGuard 的最终配置
-printconfiguration configuration.txt
复制代码

输出的配置中部分结果:

...
-keep class com.lutongnet.kalaok2.helper.RequestStatus {
    <fields>;
    <methods>;
}

-keep class com.lutongnet.kalaok2.util.RxView {
    <fields>;
    <methods>;
}

-keep class okhttp3.internal.** {
    <fields>;
    <methods>;
}

-keep class retrofit2.** {
    <fields>;
    <methods>;
}
...
复制代码

移除无用代码

随着项目的不断迭代更新,代码量也越来越多,随之而产生的无用代码也会增加,在删除无用代码时,我们常常会因为代码太多而不敢删除,这是我们就可以使用Android studio 自带的 Lint 工具来检测无效代码,使用步骤如下:

  • 点击菜单栏 Analyze
  • 选择 Run Inspection by Name
  • 在弹出的输入框中输入 unused declaration
  • 选择 Moudule ‘app’
  • 点击ok

就会自动扫描除无用代码,选择删除即可。同时,也可以通过在线下使用 Simian工具 来检查重复代码.除此之外,还有一些其他小技巧,比如:减少ENUM的使用(详情可以参考:Remove Enumerations),每减少一个ENUM可以减少大约1.0到1.4 KB的大小;移除掉所有无用或者功能重复的依赖库等。

资源瘦身优化

上面说到,在一个apk当中,占用最大的就是DEX 和资源文件,因此,资源文件的优化,就显得尤为总要,针对资源的优化,可以从以下几个方面进行:

冗余资源优化

应用通过长时间的迭代,总会有一些无用的资源,尽管它们在程序运行过程不会被使用,但是依然占据着安装包的体积。

1.使用 Lint 删除无用资源

Android Studio自带的 Lint 这个静态代码扫描工具,它里面就支持 Unused Resources 扫描。具体使用如下:

  • 点击菜单栏 Analyze
  • 选择 Run Inspection by Name
  • 在弹出的输入框中输入 unused resources
  • 选择 Moudule ‘app’ 或者 Whole project
  • 点击ok

扫描出来的结果如下:

在这里插入图片描述

点击 **"Remove All Unsed Resources" ** 就可以将无用资源删除了,而且使用lint,还可以删除xml资源文件中部分未使用的资源,比如colors.xml中未使用的颜色等。Lint 作为一个静态扫描工具,它最大的问题在于没有考虑到 ProGuard 的代码裁剪。在 ProGuard 过程我们会 shrink 掉大量的无用代码,但是 Lint 工具并不能检查出这些无用代码所引用的无用资源。

2.优化 shrinkResources

Android的编译工具链中提供了一款资源压缩的工具,可以通过该工具来压缩资源,如果要启用资源压缩,可以在build.gradle文件中将shrinkResources true。例如:

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
}
复制代码

需要注意的是目前资源压缩器目前不会移除values/文件夹中定义的资源(例如字符串、尺寸、样式和颜色),所以,可以配合Lint一起使用。

图片压缩

一般来说,1000行代码在APK中才会占用 5kb 的空间,而图片呢,一般都有几十到几百kb,所以说,对图片做压缩,它的收益明显是更大的,而往往UI 设计师或开发人员如果忘记了添加图片时进行压缩,添加的就是原图,那么包体积肯定会增大很多。对于图片压缩,我们可以在 tinypng 这个网站进行图片压缩,但是如果 App 的图片过多,一个个压缩也是很麻烦的。因此,我们可以 使用 McImageTinyPngPluginTinyPIC_Gradle_Plugin 来对图片进行自动化批量压缩。但是,需要注意的是,在 Android 的构建流程中,AAPT 会使用内置的压缩算法来优化 res/drawable/ 目录下的 PNG 图片,但这可能会导致本来已经优化过的图片体积变大,因此,可以通过在 build.gradle设置 cruncherEnabled 来禁止 AAPT 来优化 PNG 图片,代码如下所示:

aaptOptions {
    cruncherEnabled = false
}
复制代码

此外,我们还要注意对图片格式的选择,对于我们普遍使用更多的 png 或者是 jpg 格式来说,相同的图片转换为 webp 格式之后会有大幅度的压缩。对于 png 来说,它是一个无损格式,而 jpg 是有损格式。jpg 在处理颜色图片很多时候根据压缩率的不同,它有时候会去掉我们肉眼识别差距比较小的颜色,但是 png 会严格地保留所有的色彩。所以说,在图片尺寸大,或者是色彩鲜艳的时候,png 的体积会明显地大于 jpg

关于 Webp 图片的转化与压缩,具体可以参考 这篇文章

So库优化

SoAndroid 上的动态链接库,在我们 Android 应用开发过程中,有时候 Java 代码不能满足需求,比如一些 加解密算法或者音视频编解码功能,这个时候就必须要通过 C 或者是 C++ 来实现,之后生成 So 文件提供给 Java 层来调用,在生成 So 文件的时候就需要考虑生成市面上不同手机 CPU 架构的文件。目前,Android 一共 支持7种不同类型的 CPU 架构,比如常见的 armeabi、armeabi-v7a、X86 等等。理论上来说,对应架构的 CPU 它的执行效率是最高的,但是这样会导致 在 lib 目录下会多存放了各个平台架构的 So 文件,所以 App 的体积自然也就更大了。

因此,我们就需要对 lib 目录进行缩减,我们 在 build.gradle 中配置这个 abiFiliters 去设置 App 支持的 So 架构,其配置代码如下所示:

defaultConfig {
     //so库兼容,大部分用armeabi即可,如需要其他再去增加
     ndk {
            abiFilters "armeabi"
        }
}
复制代码

资源混淆

同代码混淆类似,资源混淆将 资源路径混淆成单个资源的路径,这里可以使用 AndroidResGuard,它可以使冗余的资源路径变短,例如将 res/drawable/bg 变为 r/d/a

AndroidResGuard 的使用方式如下:

  1. 在项目的根 build.gradle 文件下加入下面的插件依赖:

    classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.17'
    复制代码
  2. 在项目 module 下的 build.gradle 文件下引入其插件:

    apply plugin: 'AndResGuard'
    复制代码
  3. 加入 AndroidResGuard 的配置项,如下是默认设置好的配置:

    andResGuard {
        // mappingFile = file("./resource_mapping.txt")
        mappingFile = null
        use7zip = true
        useSign = true
        // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
        keepRoot = false
        // 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
        fixedResName = "arg"
        // 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
        mergeDuplicatedRes = true
        whiteList = [
            // for your icon
            "R.drawable.icon",
            // for fabric
            "R.string.com.crashlytics.*",
            // for google-services
            "R.string.google_app_id",
            "R.string.gcm_defaultSenderId",
            "R.string.default_web_client_id",
            "R.string.ga_trackingId",
            "R.string.firebase_database_url",
            "R.string.google_api_key",
            "R.string.google_crash_reporting_api_key"
        ]
        compressFilePattern = [
            "*.png",
            "*.jpg",
            "*.jpeg",
            "*.gif",
        ]
        sevenzip {
            artifact = 'com.tencent.mm:SevenZip:1.2.17'
            //path = "/usr/local/bin/7za"
        }
    
        /**
        * 可选: 如果不设置则会默认覆盖assemble输出的apk
        **/
        // finalApkBackupPath = "${project.rootDir}/final.apk"
    
        /**
        * 可选: 指定v1签名时生成jar文件的摘要算法
        * 默认值为“SHA-1”
        **/
        // digestalg = "SHA-256"
    }
    复制代码
  4. 点击右边的项目 module/Tasks/andresguard/resguardRelease 即可生成资源混淆过的 APK。如下图所示:

在这里插入图片描述

生成的apk如下:

在这里插入图片描述

将优化过后的apk 拖入Android Studio 进行分析,结果如下图所示:

在这里插入图片描述

从上图可以看出,经过优化后,apk从原先的 22.1MB 减少到 14.2MB ,减少了 7.9MB ,幅度达到 35.75% ,而且,经过优化后,不仅对代码进行了混淆加密,而且对资源文件也进行了混淆压缩,增加了apk的反编译难度,加强了应用的安全性,由此可见,apk优化的工作还是非常有必要的。

总结

上述就是总结的目前在APK瘦身方面的做的一些尝试和积累,可以根据项目和自身情况取舍使用。当然除此之外,还有一些其他的发难比如说插件化来减少安装包的体积。最后提一点,砍掉不必要的功能才是安装包瘦身的超级大招。一个好的App的标准有很多方面,但提供尽可能小的安装包是其中一个重要的方面,这也是对我们Android开发者人员自身的提出的基本要求,要时刻保持良好的编程习惯和对包体积敏锐的嗅觉。

分类:
Android
标签: