当前位置: 首页 > news >正文

优化APK体积

该篇文章主要来介绍如何减少APK体积,以帮助用户更快地下载App,并加速安装/更新过程。

APK内容结构一瞥

要查看APK文件中都包含哪些内容,有两种方式。第一种通过Android Studio的Analyze APK功能查看,该工具不仅可以还原XML类型代码的原始内容和各类资源文件,而且连Dex文件也能还原,比起第二种方式手动解压查看要简单直观得多;第二种则是直接将APK文件解压。则两种方式解压同一个app都是一样的文件结构,如下所示。
在这里插入图片描述
apk的内容结构根据App功能和打包时的具体操作差异,文件结构可能会有所区别,但大体相当。
无论采用上述哪种方式,我们都可以发现,这个APK文件内部主要是由5个文件和文件夹组成。

  • AndroidManifest.xml:该文件对应源代码中的同名文件,它保存着一个App的名字、版本信息、所需权限、自定义数据、Activity配置信息等。唯一不同的是,解压后的文件是经过处理的,直接使用文本查看器是无法看到原始数据的。有两种方法可以解决这个问题:一种是使用RE浏览器,当然,使用该方法只能在手机上查看,但好处是无须解压APK;另一种方法是在计算机上使用AXMLPrinter2工具还原AndroidManifest.xml文件,具体通过执行:

java -jar AXMLPrinter2.jar AndroidManifest.xml

需要特别注意的是,该工具不仅能还原AndroidManifest.xml,还能还原layout中的XML布局文件以及drawable中的XML代码文件,它几乎对所有XML文件都适用。

  • classes.dex:想要允许Java代码,需要先将其编译,生成class文件。在class1中保存了字节码,再由jvm执行它们。在android中则做了进一步优化,即将java字节码转换成Dalvik字节码,有Dalvik虚拟机运行它。classes.dex文件保存了Dalvik字节码。
  • META—INF:该目录中存放签名文件,用于校验APK文件的合法性。它通常包含4个文件:CERT.RSA、CERT.DSA、CERT.SF以及MANIFEST.MF。其中,CERT.RSA是打包release版本时,开发者利用私钥对APK进行签名的文件;CERT.SF以及MANIFEST.MF文件描述了SHA-1哈希值。
  • res:该目录存放各种资源文件,包括图片、文本等。它和resource.arsc文件配合使用,访问时要通过resource.arsc中记录的ID和资源的映射关系找到对应的资源文件。
  • resource.arsc:该文件时编译后的二进制文件、通常包含ID和具体资源文件之间的映射关系以及源码中res/values的内容。

最后需要特别说明的是,由于不同APK在打包方式上有少许差异,文件结构可能与上述内容有所区别,但大多数只限于文件名或目录名的不同,其作用和结构仍保持一致。

多渠道打包

如果我们想要尽可能让程序在尽可能多的设备上正常收到推送消息,通常的做法是集成各个厂商独立的推送库。但这也带来了负面作用——各库之间是否有冲突、多个推送服务同时运行,拖慢app的运行速度、apk的体积增大。
面对这种现象,理想的做法是相应版本只集成相应厂商的推送服务,比如发布到华为应用市场的APK只集成华为的推送服务,发布到小米应用市场的APK只集成小米的推送服务。最后,在做一个集成通用推送服务的版本,比如极光推送,用来发布到通用的应用市场上,比如应用宝、百度应用市场等。

多渠道打包原理

减少APK体积的一种方法是分渠道打包,其主要思想是只保留该渠道所用的资源,去除其他渠道专用的资源。这里的资源包括程序代码、资源文件以及第三方库。下面我们介绍下多渠道打包的重要知识点。

  • buildTypes参数
    涉及多渠道打包的核心难点是build.gradle文件以及不同版本的代码、库、资源等文件的存放技巧。我们先来看build.gradle。
    要实现多渠道打包,主要通过build.gradle文件android节点下的配置参数进行定义。在默认新创建的工程中,可以在android节点下找到buildTypes子节点,该子节点又包含release和debug子节点。
buildTypes {
        debug {
            signingConfig signingConfigs.debug
        }
        release {
            signingConfig signingConfigs.debug
            minifyEnabled enableProguardInReleaseBuilds
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        }
    }

下面列举该节点下比较常用的配置参数。

参数名称说明
Debuggable定义该版本是否为可调试
minifyEnabled定义是否需要自动移除没有用的java代码
multiDexEnabled定义是否可以拆分多个dex包
signingConfig指定签名配置文件
versionNameSuffix定义VersionName的后缀
zipAlignEnabled定义是否启用zipAlign优化APk
shrinkResources当其值为true时,编译相应版本时会省略没有用到的素材

除了上述完全自定义版本参数的方法外,还可以继承某个版本,对某些参数进行自定义,类似java中的Override。举个例子,现要定义名为custom的版本,当我们想集成Debug版本时,可以按如下方式配置:

custom.initWith(Debug)
custom{
   zipAlignEnabled true
}

这样配置后,编译出的custom版本就是启用了zipAlign优化后的Debug版本,其他未在此配置的参数则与Debug版本的配置参数保持一致。

  • productFlavors参数
    除了buildTypes外,还可以在android节点下添加productFlavors字段,该字段定义了“分发渠道”。所谓“分发渠道”,即前文中所述专门发布到华为应用市场和小米应用市场的版本。该字段在默认配置中并不存在,需要我们手动声明它。
productFlavors {
        huawei {
        }
        xiaomi {
            
        }
    }

下面列举该节点下比较常用的配置参数。

参数名称说明
ApplicationId完整的程序包名
ApplicationIdSuffix包名的后缀
versionName定义了该渠道版本的VersionName的值
versionCode定义了该渠道版本的VersionCode的值
dimension指维度,所有的Flavor都必须包含dimension定义

实例解析

接下来,我们用一个实例来演示如何分渠道打包 APK。 项目需求如下:
为了统计用户对地图类 App的喜好,需要分不同渠道进行分发。渠道A 集成百度地图,在X应用市场上架:渠道B集成高德地图,在Y应用市场上架。

  1. 分渠道打包
    创建一个新工程,并命名为 AndroidMultiApkDemo.
    按照实际需求,我们需要编译两个版本,分别是集成百度地图版和集成高德地图版。因此,我们添加 productFlavors 节点并声明这两个渠道。具体代码如下:
productFlavors{
  baidu{
    dimension "default"
  }
  amap{
    dimension "default"
  }
}

buildTypes 不做处理,Sync 成功后,打开 Build Variants 视图,可以看到一共可产出 4 个版本。
然后,打开Project 视图,并以Project 方式查看项目文件。依次展开 AndroidMultiApkDemo->app->src,对于新建的工程,src目录中通常包含 main、 test 和 androidTest 三个子日录。我们在src 中新建amap 和 baidu 两个新目录:将main 目录下的内容完整地复制到这两个新目录中。
之所以创建 amap 和 baidu 两个目录,目的在于分别编写两个渠道版本的代码。在编译时,系统将根据 productFlavors 中的版本名称与 src中的目录名自动匹配,从而将不同渠道的代码分开处理。而这两个目录中的内容与main 中的代码是继承关系,这就解释了为何我们无论选择哪种 BuildVariants, main 目录下的内容都为启用状态,且图标样式不变:
接下来,按照百度地图和高德地图的官方文档分别进行集成。完成后,在Build Variants 中选择相应的版本,再执行 Build APK 即可编译出对应版本。如果读者偏爱使用命令行,也可在工程根目录下执行

./gradlew assemble [渠道版本名]
例如: ./gradlew assembleamapDebug

即可生产出集成高德地图的 Debug版本。至此,项目需求已经可以完整地实现,可以分发了。似还有哪里不对劲,因为这样做会集成高德和百度地图的so库和jar包,我们需要将其分开来。

  1. 分治第三方库
    其实要剔除其他渠道的jar和so库很简单,核心思想就是将它们分路径存放,然后在具体声明。
    对于本例,笔者的处理方式是将百度地图的 JAR 包放在 app 目录下的libs baidu子目录下;高德地图的 JAR 包放在app 目录下的libs armap 子目录,而不是将它们一起放在libs 中。对于so库,笔者将其放在丁 src目录下amap 子目录和baida 子目录各自的 jniLibs目录中。
dependencies{
baiduImplementation files ('libs_baidu/BaidulBS Android.jar')
amapImplementation files ('libs_amap/...')
}

由于修改后的 jnilLibs 目录在编译时 可以自动识别,因此无须显示指定。
最后,分别再次编译这两个版本,可以看到APRK文件有明显的缩小。

  1. 注意事顼
    为了讲述多渠道打包的方法,我们以集成不同地图为例,且整个 App 仅包含单一的地图显示功能。但在实际开发中几乎不存在这样的场景,真实的需求很可能只是整个 App 中的某个小功能点在不同的渠道版本之间有所差异。就比如前面提到的推送服务,实际上仅仅是推送服务单个模块的实现不同,其他的功能点都是一样的。
    因此,在实际操作时,我们仅需单独实现有差异的功能模块即可,不要将所有的代码都按照渠道不同在不同的代码目录中都放置一份,而是把相同的代码放到main 目录下即可。否则虽然能做到多渠道分发,但维护起来和同时维护多个Project 的工作量相差无几。

优化资源文件

针对某些开发场景,应用多渠道打包技术已经可以减少APk的体积了。但这还不够,我们还可以从资源文件入手,进一步地缩小APK的文件大小。

图片格式的选择

  • 使用webp替代传统图片格式
    需要注意的是,webp并非全能,一下几点需要留意:
  1. 对于GIF格式,转换后的webp只能保留其静止状态。也就是说,如果GIF本身是动态图片,不要对其转换。
  2. App图标可能在上架时要求必须是采用png格式。
  3. webp格式在android3.2及以上版本受支持,无损、透明的webp格式在android4.3及以上版本受支持。如果你的app运行在较早版本的系统上,那么谨慎使用webp格式。
  4. webp格式与经过9-patch处理的图片不兼容。

排除了上述例外情况后,就可以放心地对原有图片进行转换了。Android Studio中提供了快速方便地转换工具,可以对单个文件进行转换,也可以对整个目录下的图片进行转换。并且可以根据情况做出如下排除:

  1. 有损和相关质量参数以及无损编码选择
  2. 跳过9-Patch处理的图片(必选)
  3. 跳过转换后比转换前体积更大的图片(可选)
  4. 跳过带有透明度的原始图片(可选,当API Level不符合要求时,此项为必选)
  • 压缩PNG/JPG图片
    推荐使用TinyPNG网站对图片进行压缩,除此之外,对于png图片aapt工具在项目编译过程中还将通过无损压缩的方式优化位于res/drawable中的图片资源文件。但是有可能压缩后的文件反而更大,为了避免aapt帮倒忙,需要在gradle配置文件中添加以下代码来规避此风险:
aaptOptions{
   cruncherEnabled = flase
}
  • 复用相似的图片
    这个想必就不必多说了吧。对于一些图片可以通过旋转等变换得到所需图片的就不需要美术再出一张图片了。

  • 应对多尺寸的9Patch处理方案
    这里罗列一些注意事项:

    1. 9Patch处理的图片,原图应是PNG格式而不是JPG格式。
    2. 在使用9Patch工具描绘边缘时,四边都要画,且尽量对称。
    3. 经过9Patch处理后的图片应放在drawable目录下,不要放在mipmap目录下。

合理使用矢量图

众所周知,矢量图是经过矢量计算绘制出来的图形,它的特点是无论怎样放大和缩小,图像都不会失真。随之而来的另一个优点是节省空间。分辨率为100x100的矢量图和分辨率为1000x1000的图片都可以使用同一个文件,而无论是JPG、PNG还是WebP格式,对于相同内容的图片而言,文件尺寸的分辨率往往成正相关的关系。另外,虽然矢量图绘制路径可由开发人员完全自定义,但是实际上很少有人去改动它,因为它不是很直观,一般的人还真是改不了。

资源文件后加载技术

如果你正在开发的App确实有很多图片显示需求,而且都是GIF图,无法应用WebP转换,也无法压缩,那该怎么办?典型的例子就是聊天软件中的动画表情,它们大部分是GIF动图,而且数量十分庞大。解决方案就是资源后加载。
首先,在APK内部以ZIP格式压缩一些常用的动画表情,放到assets目录中,随APK分发。然后,在适当的时机完成其他动画表情的下载。这样一来,既保证了用户体验,又达到了给APK瘦身的目的。

使用代码混淆

代码混淆可以帮助我们很好地隐藏代码,防止被反编译。同时,还可以达到缩减发布安装包大小的目的。在早期版本(3.4版本前)的Android Studio中,代码混淆时由ProGuard来完成的。现在,Android官方采用R8编译器来处理代码混淆,且与ProGuard规则配置文件相兼容。

R8编译器的优化原理

在使用 Android Gradle Pluein 3.4.0 及以上版本迸行编译时,R8编译器将会参与到构建流程中。当然,就提是我们启用了它,默认情况下是没有启用的。启用后,R8编译器将自动执行下列任务:

  • 代码压缩:该项任务将检测开发者自己编写的代码以及依赖库中没有使用到的类、变量,方法和宇段等,并合理地移除它们,达到减少代码量的目的。
    举个例子:开发者自己编写的代码分别用到了某个依赖库中名为A.class的X()、Y()方法,但完整的A.class不仅包舍这两个方法,还有Z()方法。但由于Z()方法并没有发生过调用,因此它将被移除。当然,如果Z()方法被依赖库调用,而我们又不十分确定的时候,可以查问依赖库的文档。文档中会对代码混淆做出详细说明,如果某个类中的方法不应移除,就需要使用-keep规则忽略这个类。还有,当我们通过反射的方式使用某段代码时,相应的代码可能会被认为是无用的代码,也应添加-keep规则,否则将可能导致App运行崩溃。
    另外,对于64KB引用限制,若进行代码压缩后可以避免,则无须再启用多dex的方式编译。
  • 代码混淆:该项任务通过缩短类和成员名称达到防止反编译和减少代码量的目的,进行过代码混淆的类、方法、成员名通常人们轻易无法理解和理清其调用关系。
  • 代码优化:该项任务将通过优化代码的逻辑结构达到减少代码量的目的,比如移除空的eise分支等。需要注意的是,使用R8编泽器后,之前的-optimizations 和-optimizationpasses可能会失效。这是因为RB编译器会忽略影响代码优化的混淆规则,它不允许用户修改优化行为。但是,你可以完全停用代码优化任务。
  • 资源压缩:参看上面部分。

启用代码混淆

前文中己经讲过,默认情况下代码混淆并不生效,需要开发者手动启用它。这是因为如果默认启用,每次编评都会耗费大量的时间。另一方面,如果开发者并没有制定哪些类需要排除,极易触发运行崩溃。
启用代码混淆的方法在之前已经大致介绍过,主要是编辑Project中的build.gradle 文件,示例如下:

buildTypes {
        release {
            // Caution! In production, you need to generate your own keystore file.
            // see https://reactnative.dev/docs/signed-apk-android.
            signingConfig signingConfigs.debug
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        }
    }

上述代码对release版本进行了混淆方式定义。

  • minifyEnabled是代码混淆的“总开关”;默认值为false。
  • shrinkResources是缩减资源文件的开关,默认值为false,作用是清理没有用到的资源素材文件。
  • proguardFilcs用于指定代码混清的规则,getDetaultProguardFile将返回SDK目录中tools目录下的proguard路径。在该目录中存在名为proguard-android-optimize.txt的文件,后面的proguard-rules.pro存在于每个module中。一般情况下,自定义的代码混淆规则会在此文件中配置。
    例如上述代码中的写法可将混滑配置文件结合到一起,我们可以在该字段添加多个混淆配置文件。需要注意的是,当项目中集成了第三方库且库中存在混淆配置文件时,则其中的混淆规则会追加应用到整个项目中。

添加混淆例外项的两种方式

(1)在Proguard配置文件中 (通常是module 中的proguard-rules.pro)使用-keep来添加例外。比如,要对名为 MainActivity java 的类原样保留,通常的写法为:

-keep public class Mainactivity

(2)在要添加例外的类中通过@Keep 注解的万式漆加例外。当@Keep 注解作用与类时,整个类的内容特被保留:当@Keep 注解作用于 某个方法或变量时,相应的方法或变量将被保留。该方法只有迁移到 Androidx 后可用,否则请按照方式(1)来添加例外。

提醒读者:如果一个 App在混淆后反复崩溃,但无论从代码逻辑还是资源文件都查不出问题,那么可以关闭混淆后打包试试。
如果在没有启用混淆的情况 下程序可以正常运行,问题就出在混淆的步骤之中。通常的做法是看崩溃发生在哪个类,然后尝试将其添加到例外项中最后再次尝试运行。以上操作可以解决大部分由混淆带来的异常问题。

相关文章:

  • 【初学者入门C语言】之函数(八)
  • 《Linux基本常识的介绍》
  • 【云原生】Kubernetes介绍
  • C语言自定义类型【结构体】
  • springboot请求映射原理,springboot版本2.3.4.RELEASE
  • 【数值分析+python】python生成稀疏对称正定矩阵
  • jave web开发(IDEA中配置maven)
  • 保存滚动位置的实现方法
  • 什么是数据库事务
  • 异步FIFO的原理及verilog实现(循环队列、读写域数据同步、Gray Code、空满标志、读写域元素计数)
  • 大数据_YARN的工作原理
  • anaconda,docker和Jupyter Notebook常见问题解答
  • 【Rust日报】2022-10-01 Rumqtt:基于rust的mqtt代理
  • STM32 GPIO模拟UART串口:外部时钟及TIM方式
  • (机器学习-深度学习快速入门)第一章第一节:Python环境和数据分析
  • [rust! #004] [译] Rust 的内置 Traits, 使用场景, 方式, 和原因
  • 《微软的软件测试之道》成书始末、出版宣告、补充致谢名单及相关信息
  • 〔开发系列〕一次关于小程序开发的深度总结
  • 4月23日世界读书日 网络营销论坛推荐《正在爆发的营销革命》
  • Date型的使用
  • echarts的各种常用效果展示
  • Effective Java 笔记(一)
  • java多线程
  • JS变量作用域
  • js如何打印object对象
  • js正则,这点儿就够用了
  • Python_OOP
  • SAP云平台里Global Account和Sub Account的关系
  • Vue源码解析(二)Vue的双向绑定讲解及实现
  • 对JS继承的一点思考
  • 关于 Cirru Editor 存储格式
  • 前端技术周刊 2018-12-10:前端自动化测试
  • 微服务核心架构梳理
  • 微信如何实现自动跳转到用其他浏览器打开指定页面下载APP
  • 问:在指定的JSON数据中(最外层是数组)根据指定条件拿到匹配到的结果
  • 用Visual Studio开发以太坊智能合约
  • 【运维趟坑回忆录 开篇】初入初创, 一脸懵
  • 湖北分布式智能数据采集方法有哪些?
  • !!java web学习笔记(一到五)
  • # 执行时间 统计mysql_一文说尽 MySQL 优化原理
  • #gStore-weekly | gStore最新版本1.0之三角形计数函数的使用
  • #多叉树深度遍历_结合深度学习的视频编码方法--帧内预测
  • (26)4.7 字符函数和字符串函数
  • (arch)linux 转换文件编码格式
  • (C++)栈的链式存储结构(出栈、入栈、判空、遍历、销毁)(数据结构与算法)
  • (C++17) optional的使用
  • (ctrl.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“
  • (Git) gitignore基础使用
  • (Java)【深基9.例1】选举学生会
  • (附源码)springboot车辆管理系统 毕业设计 031034
  • (三)docker:Dockerfile构建容器运行jar包
  • (五)大数据实战——使用模板虚拟机实现hadoop集群虚拟机克隆及网络相关配置
  • (一一四)第九章编程练习
  • .NET Core使用NPOI导出复杂,美观的Excel详解
  • .net mvc 获取url中controller和action