Android APK瘦身计划

844 阅读7分钟

Google 2016 年公布的研究报告显示,包体积每上升 6MB 就会带来下载转化率降低 1%,当包体积增大到 100MB 时就会有断崖式的下跌。对于应用商店,普遍有一个流量自动下载的最大阈值,如应用宝,下载的app超过100M,用流量下载时,会 弹窗 提示用户是否继续下载,这对下载转化率影响是比较大的。

虽然时间已经过去了7年,手机的内存已经从16g/32g扩展到256g/512g甚至1T,流量也不再成为制约我们下载大体积apk的因素,但是包体积或直接或间接地影响着下载转化率、安装时间、运行内存、磁盘空间等重要指标,所以投入精力扫除积弊、发掘更深层次的体积优化项是十分有必要的。

Android APK结构

APK主要由三个部分组成,分别是:

  • 代码相关: 我们在项目中所编写的 java 文件,经过编译之后会生成一个 .class 文件,而这些所有的 .class 文件呢,它最终会经过 dx 工具编译生成一个 classes.dex, 当超过65525方法数后会打包成多个Dex文件。
  • 资源相关: resassets、编译后的二进制资源文件 resources.arsc 和 清单文件 等等。resassets 的不同在于 res 目录下的文件会在 .R 文件中生成对应的资源 ID,而 assets 不会自动生成对应的 ID,而是通过 AssetManager 类的接口来获取。此外,每当在 res 文件夹下放一个文件时,aapt 就会自动生成对应的 id 并保存在 .R 文件中,但 .R 文件仅仅只是保证编译程序不会报错,实际上在应用运行时,系统会根据 ID 寻找对应的资源路径,而 resources.arsc 文件就是用来记录这些 ID 和 资源文件位置对应关系 的文件
  • so相关:lib目录下的文件,存放so库
  • META-INF: APK签名有关的信息

APK 解析

以我们CSD设置为例,解析APK内部如下图所示:

浅分析一波包的内容:

成分体积备注
assets文件夹418.4MB
res35.4MB
lib76.6MB
dex26.4MB
resources.arsc1.7MB资源映射问题
其他几十K或者更小忽略不计

根据上午的分析,我们体积优化的重点应该放在 assets目录,res目录,lib目录和dex文件中。

APK 体积优化方法

针对上述APK的体积情况我们可以按照几个部分进行优化

资源优化

  • 无用资源文件清理

可能在版本迭代过程中,UI的样式经过几次更新,但之前存放在项目中的资源图片没有删除,可以通过Lint检查出未使用的资源文件,然后进行删除。

  • 重复资源优化

在大型App的开发过程中,可能涉及到多个团队和多个人员进行开发,每个人员在提交代码时可能会导致资源名称相同,导致了资源文件覆盖,然后根据模块名称添加前缀,解决覆盖的问题,可能导致了相同的资源文件导入了多分,从而增大了应用的体积。这时需要将公共资源提取出来并去重。

  • 图片压缩,转webp

图片压缩可以使用TinyPng,AndroidStudio中也可以直接使用,官方术语就是:

使用智能的无损压缩技术来减少图片文件的大小,通过智能的选择颜色的数量,减少存储的字节,但是效果基本是和压缩前一样的。

  • 图片着色器

相同的图片只是颜色不同的话,完全可以只放一个图片,可以在xml中操作或者在内存里操作Drawable,完成颜色替换。

        <ImageView
            android:id="@+id/iv_select_music"
            android:layout_width="12dp"
            android:layout_height="12dp"
            android:src="@drawable/ic_select_course_subject_arr"
            app:layout_constraintBottom_toBottomOf="@+id/tv_select_music_text"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="@+id/tv_select_music_text"
            app:tint="@color/c_line_gray" />
        val drawableRes = ResourcesCompat.getDrawable(
            resources,
            resId, null
        ) ?: return
        val tintIcon = DrawableCompat.wrap(drawableRes)
        DrawableCompat.setTint(tintIcon, hintColor)
        setImageDrawable(tintIcon)
  • 图片动态下发

如果本地有大图,且使用要求为未压缩,或者压缩之后仍然很大,可以适当的选择动态下载该图。

so库优化

针对不同的平台我们可以分架构打包

  • 架构 配置

只配置armeabi-v7a

android {
  defaultConfig {
    ndk {
      abiFilters "armeabi-v7a"
    }
   }
  }

通过productFlavors配置动态包

flavorDimensions "default"  productFlavors {
    arm32{
     dimension "default"
      ndk {
        abiFilters "armeabi-v7a"} 
    }
    arm64{
     dimension "default"
      ndk {
        abiFilters "armeabi-v8a"} 
    }} 
}
  • 架构 打包

分架构打包能减少libs文件夹体积,libs文件夹里会包含不同架构的 so 库集合。

splits {
        // 基于不同的abi架构配置不同的apk
        abi {
            // 必须为true,打包才会为不同的abi生成不同的apk
            enable true
            // 默认情况下,包含了所有的ABI。
            // 所以使用reset()清空所有的ABI,再使用include指定我们想要生成的架构armeabi-v7a、arm-v8a
            reset()
            // 逗号分隔列表的形式指定 Gradle 应针对哪些 ABI 生成 APK。只与 reset() 结合使用,以指定确切的 ABI 列表。
            include "armeabi-v7a", "arm64-v8a"
            // 是否生成通用的apk,也就是包含所有ABI的apk。如果设为 true,那么除了按 ABI 生成的 APK 之外,Gradle 还会生成一个通用 APK。
            universalApk true
        }
    }
  • so压缩

分架构打包是减少so的数量,so压缩是减少so的单个体积。

android:extractNativeLibs="true"

android:extractNativeLibs = true时,gradle打包时会对工程中的so库进行压缩,最终生成apk包的体积会减小。

但用户在手机端进行apk安装时,系统会对压缩后的so库进行解压,从而造成用户安装apk的时间变长。

代码优化

  • 类优化,方法优化

通过Lint扫描,删除没有用到的类和方法,从而减小Dex的体积。

  • 使用ProGuard

混淆器的 作用 不仅仅是 保护 代码,它也有 精简编译后程序大小 的作用,其 通过缩短变量和函数名以及丢失部分无用信息等方式,能使得应用包体积减小

  • Dex分包优化

当我们的 APK 过大时,Dex 的方法数就会超过65536个,因此,必须采用 mutildex 进行分包,但是此时每一个 Dex 可能会调用到其它 Dex 中的方法,这种 跨 Dex 调用的方式会造成许多冗余信息,具体有如下两点:

  • 1)、多余的 method id :跨 Dex 调用会导致当前dex保留被调用dex中的方法id,这种冗余会导致每一个dex中可以存放的class变少,最终又会导致编译出来的dex数量增多,而dex数据的增加又会进一步加重这个问题
  • 2)、其它跨dex调用造成的信息冗余:除了需要多记录被调用的method id 之外,还需多记录其所属类和当前方法的定义信息,这会造成 string_ids、type_ids、proto_ids 这几部分信息的冗余

为了减少跨 Dex 调用的情况,我们必须 尽量将有调用关系的类和方法分配到同一个 Dex 中。但是各个类相互之间的调用关系是非常复杂的,所以很难做到最优的情况。所幸的是,ReDexCrossDexDefMinimizer 类分析了类之间的调用关系,并 使用了 贪心算法 去计算局部的最优解(编译效果和dex优化效果之间的某一个平衡点)。使用 "InterDexPass" 配置项 可以把互相引用的类尽量放在同个 Dex,增加类的 pre-verify,以此提升应用的冷启动速度