• 【性能优化】如何让APK瘦成一道闪电


    转载请标注来源:http://www.cnblogs.com/charles04/p/8547273.html


     如何让APK瘦成一道闪电

    0、目录

    1. 背景介绍
    2. 分析与探索
    3. 总结与反思
    4. 参考文献

    1、背景介绍

    随着业务的不断迭代,项目中的APP会不断引入新的技术框架,第三方SDK,资源文件,业务逻辑等,导致APK包的体积不断增大。最近正在研发的一个APP,短短一个月的时间,当前版本相比上一个上线大版本,当前版本的APK体积已经从18M增大到34.6M。

    APK体积过大,从编程规范角度讲,影响APK的预制(APK 占据预制存储空间尺寸有限制);从2C的角度讲,APK体积的增大,将增大对用户流量的消耗,延长下载和安装的时间,影响APP下载和安装的成功率,导致用户体验下降,影响用户的留存率。

    所以,科学的APK瘦身技术是一件非常有意义的事情。

    2、分析与探索

    2.1. 系统分析

    在进行实际的APK瘦身操作之前,首先对APK包的组成进行分析,从而找准切入点,宏观把控,有的放矢。

    通过Android Studio自带的工具分析,当前版本和上一个上线版本的APK组成结构如下所示:

    图1. 当前版本的APP的主程结构

    图2. 上一个上线版本的APP的主程结构

    其中上图中相关标签的含义如下:

    Raw File Size:原文件大小,对应APK占物理硬盘的容量(也即通常说到的,apk大小);

    Download Size:经过Google Play处理压缩后的apk大小。

    从上图中,可以发现

    Apk内部主要由res,class.dex,lib,assets等文件组成,其中res,class.dex的占比最大,二者加起来占比在90%以上。

    进一步对比当前版本和上一个上线版本,并对两者取差,得到结果如下:

    可以发现,对比上一个上线版本,当前版本增加最明显的为res,classes.dex,resource.arsc,这几部分尺寸增加分别如下:

    文件名称 尺寸增加(M)
    res 14.9
    classes.dex 3.5
    resource.arsc 1.7

     

     

     

     

     

    了解这些数据之后,接下来要对APK包内的每项组成进行进一步分析,根据每项在Android工程中的对应关系,进行针对性瘦身。具体如下:

    Res Res: 存放资源文件。包括图片(drawable)、raw文件夹下面的音频文件、各种xml(layout,string,array等)文件等等。
    resources.arsc 编译后的二进制资源索引文件
    AndroidManifest.xml Android项目的清单文件,它描述了应用的名字、版本、权限、引用的库文件,注册的四大组件等等信息
    classes.dex java源码编译后生成的java字节码文件,其中java源码包括本地编写的代码,和SDK中包含的java代码。在Android项目中,每超过65535个方法数,就会新增加一个classes.dex文件,所以,在当前项目中,classes.dex文件有多个
    META-INF 存放的是签名摘要信息,用来保证apk包的完整性和系统的安全
    lib 存放的是so文件,用于本地混合调用c/c++代码库,可能会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips
    assets 存放一些配置文件(比如webview本地资源、图片资源等等),这些文件的内容在程序运行过程中可以使用AssetManager来获取

     

    根据APK包中每项的实际存放内容,针对性的设计APK瘦身策略如下:

    2.2. Res瘦身

    Res文件的瘦身主要涉及资源文件的瘦身。主要从以下几个方面来考虑:

    2.2.1. 冗余资源的删除

    项目中的图片资源一般是非常多的,人肉搜索和删除费时费力;好在可以通过工具或配置来自动化完成相关工作,我自己比较喜欢用两种方法:

    • Lint

    冗余资源可以通过Android Studio的Lint工具来扫描发现,扫描出来之后再批量删除。但是Lint扫描似乎无法扫描出SDK中的资源引用,所以有可能会导致误删的情况,所以删除之后,一定要重新编译,通过之后再提交相关的代码。

    • shrinkResources

    gradle配置中将shrinkResources设置为true可以在打包的时候不打包使用不到的资源,shrinkResources是在buildTypes内配置的,具体如下:

    1 android {
    2 3     buildTypes {
    4         release {
    5 6              shrinkResources true
    7         }
    8     } 
    9 }

    设置之后会延长编译打包的时间,所以不建议在debug环境下使用。

    另外,也有一些情况反映将shrinkResources设置为true后有些图片无法正常显示,这个我倒没有遇到过,等我遇到了再回来补个分析。

    2.2.2. 资源复用

    对于类似的图片,不需要放置多张不同的图片资源,而只需要通过同一张图片,在不同的透明度,缩放比例,旋转等方式来实现复用。这样可以在一定程度上减少需要的图片资源的使用,达到瘦身的目的。

    2.2.3. 分辨率瘦身

    这个名字是我取的。所以这里想要稍微介绍下,什么叫分辨率瘦身。

    在Android开发的过程中,同一张图片一般会适配不同的分辨率(DPI),也即在不同分辨率文件夹中存放不同尺寸的图片资源。实际上,只保留一种source分辨率下的图片,在不同target分辨率的设备上,相比适配不同的source分辨率,二者:

    • 不会存在显示上的差异;
    • 图片本身的内存资源占用也是一致的。

    只是在分辨率不匹配的时候,会额外增加一定的图片换算的资源消耗。这里,之前写过两篇相关的博客,有兴趣的可以参考下:

    • 内存占用分析:http://www.cnblogs.com/charles04/p/6804422.html
    • 内容显示分析:http://www.cnblogs.com/charles04/p/6914859.html

    考虑到额外的内存等资源的消耗,分辨率瘦身还是要慎重使用,但是在一些特殊的场合,分辨率瘦身兼职是量身定做的。比如说EMUI预制,也即APP预装到指定的手机,手机的Target分辨率是固定的,所以只需要适配一套Source分辨率就可以啦。

    关于分辨率瘦身的具体实现,最简单的是将其他分辨率资源的图片全部删掉。但是这样改动比较大,而且如果在有些疏忽的情况,没有在指定的分辨率上添加资源,也有可能会造成ResourceNotFound的错误,风险比较大。

    实际上,可以通过Gradle的分包策略来简单实现分辨率瘦身。

    既然说到这里,那就简单介绍下Gradle的分包策略。Gradle分包是指通过Gradle中的配置,实现自定义的资源或逻辑打包,目前用途比较广泛的主要包含两方面的内容:

    (1) 多渠道打包

    多渠道打包是通过Flavor来实现,可以实现资源文件,方法类,so,甚至是Manifest等配置文件的编译隔离。这里先简单说一嘴,按下不细表,后面会专门给这个开专题。

    //todo:专题传输门

    (2) splits编译时资源分包

    顾名思义,这种分包策略就是在编译的时候,根据编译的资源文件的不同,分别打出不同的APK包。本文提到的分辨率瘦身就是通过这种分包方式来实现的。

    这里简单介绍下splits分包中Gradle配置的具体使用,如下:

    • splits:关键词,表示当前是分包的配置啦
    • density:具体分包的内容,当前支持density和abi,其中density就表示资源的分辨率,abi表示的是so库的架构类型,敲黑板,abi后面会用到,这个先不细说了;
    • enable:使能,true表示当前的配置生效,false表示不生效;
    • reset():初始化,调用后,当前的gradle相关配置会清空,一般要搭配后续的include和exclude使用;
    • include:想要使用的类型(分辨率/架构子类,下同);
    • exclude:在整体集合中想要去除的类型;
    • universalApk:是否要同时生成一个包含全部类型的APK,true表示是,false表示不是;

    所以,如果只保留xx分辨率,具体分包配置如下:

     1 android {
     2   ...
     3   splits {
     4     density{
     5       enable true
     6       reset()
     7       include "xxhdpi"
     8       universalApk false
     9     }
    10   }
    11 }

    2.2.4. 资源压缩

    目前Android中主流的图片格式为png和.9,在某些对透明度不做要求的场景中也会使用jpg格式。但是实际上,png和jpg对图片的压缩都很难再尺寸和质量上取得双向突破。

    然而,google推出webp压缩之后,一切将被改变。Webp可以在大幅降低图片体积的情况下,保障图片的质量(肉眼几乎感受不到图片质量的下降),关于png和webp压缩的比较,有如下示例:

    经过实践,在APK瘦身中,推荐使用65%-80%的有损webp压缩。

    Webp压缩可以通过专门的工具来实现,也可以通过如下的在线转换工具。

    1 http://zhitu.isux.us/

    2.3. Dex文件瘦身

    2.3.1. 概况分析

    在分析class.dex瘦身之前,首先介绍下dex文件时如何生成的。Dex文件是Android虚拟机上的可执行文件,在工程中是从Java-Class-Dex的生成过程,具体如下:

    1. Java文件:Java文件是在工程中通过Java高级语言编写出来的代码逻辑文件,这是离软件开发人员最近的文件;
    2. Class文件:Class文件时通过编译器编译生成的目标文件;
    3. Dex文件:Class文件只是编译过程的中间目标文件,不可以被虚拟机运行,而Dex文件正是通过Class文件生成的Android虚拟机上的可执行文件,虚拟机通过ClassLoader可以直接加载Dex文件,为用户展示代码效果。具体ClassLoader加载过程是一套很有挖掘价值的技术体系,有机会会做一些相关的总结。

    另外,通过APK解压我们会发现,APK中的Dex文件可能会有多个,这里又涉及到另外一个话题,叫做Dex分包技术,在后续热修复插件化的相关话题中会重点介绍,这里不做赘述。

    2.3.2. 具体策略

    好了言归正传,知道了Dex文件的生成原理之后,就可以做针对性的瘦身,那就是对工程中的Java代码进行瘦身。

    总结来说,Java文件瘦身大概有以下途径:

    (1) 冗余代码删除

    可以通过Android自带的Lint工具进行冗余代码的检测和删除。不过Lint只能排查出不再调用的代码逻辑,实际上有很多不再使用的代码逻辑还在被各种调用,这些就依赖软件开发人员的敏感度和质量意识啦。

    (2) 代码混淆

    ProGuard可以对代码进行混淆,优化和压缩,在Android工程中,可以在gradle中通过如下的配置进行混淆:

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

    其中proguard-rules.pro表示自定义的ProGuard规则。

    (3) SDK优化

    Android业界有相当多经典的轮子,有了这些轮子可以更加快捷的借鉴前人的技术结晶,更加快速的构建自己的项目。

    但是,为了保障接口的稳定性,功能的完整性,SDK的作者一般会Release出来一些大而全的SDK,实际上,我们可能只需要使用其中很小一部分功能,却要继承一个厚重的SDK,这是很不合理的。

    遇到类似的情况时,推荐对SDK进行二次重构。从既有架构中抽离出项目中想要依赖的部分。

    在实际项目中,我也有过类似尝试,曾经将华为移动服务的AAR完全拆开,抽离其中的PUSH功能,而将支付,钱包,登录等功能去除掉,重组出一个新的Jar包,既满足了项目的实际需求,又减少了APK Size,完美。

    不过,在实际的开发过程中,实现SDK的优化并不简单,这是因为SDK的代码的类和方法大都经历过混淆,可读性较差,要从有限的信息中理解代码的逻辑,并抽离出项目中需要的那部分,是一件技术难度较大的事情,所以可以根据实际情况,相机行事。 

    (4) 动态加载

    对于有些特定用户才会使用到的,并且代码量较大的功能,可以通过动态加载的方式,展示给用户。

    也就是说,在APK打包的时候,不将这部分代码打包到APK,而在用户触发了某些动作,例如安装了某些设备,注册了某个功能的时候,再去服务器动态下载这部分代码,然后动态加载这部分下载的功能。

    其实这就是插件化的概念。目前插件化已经发展的相当成熟,业界也有相当多功能和性能都非常优异的插件化解决方案,例如360推出的Replugin,任玉刚的Dynamic-load-apk,林光亮的Small,等等。关于插件化的知识后续也会陆续推出,同样的,这里暂时按下不表(快按不住了)。

    2.3.3. 小结

    对于代码优化这块,想顺便扯一下。通过观察发现,身边很多开发者有个误区,那就是没有去全面熟悉早期的代码架构,在进行功能迭代的时候,为了不影响之前的功能,特别喜欢在早期的代码上打补丁,导致整体的代码架构臃肿不堪。这样导致的问题是,不仅增加维护的成本,而且APK的体积也会因为class.dex文件的增大而随之增加。

    就个人习惯,在接手新的项目或新的模块的时候,会习惯性的对代码进行整体的优化重构,对逻辑进行归纳整合,去除由于方案变更等原因而不再使用的冗余的代码块和代码分支。这样,不仅代码逻辑更加稳健,而且对APK的瘦身也是有所裨益的。

    2.4. resource.arsc文件瘦身

    resource.arsc是一个索引文件,表征着资源id和资源之间的对应关系。这里的资源文件包括图片资源,xml资源,string资源等各项资源。所以,前面讲到的清楚无效的资源,减少资源引用都会对resource.arsc进行瘦身。

    另外,对于字符串资源,如果只用到部分小语种,也可以通过gradle来配置语种的支持,避免多余的资源索引的生成,例如只需要支持中英港台四种语种的时候,可以设置如下:

    1 defaultConfig {
    2     resConfigs "zh-rCN", "zh-rHK", "zh-rTW", "en"
    3 }

    2.5. Lib文件瘦身

    2.5.1. 概况分析

    Lib文件夹中主要保存的是动态加载的so库。对于不同的CPU架构可能需要适配不同的so文件,如下所示:

    Android系统目前支持以下七种不同的CPU架构:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起)

    每一个CPU架构对应一个ABI:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。对于不同的架构,有如下的兼容性:

    1. x86设备一般对arm类型的函数库支持度良好(部分及其老的设备不考虑);
    2. 位数向下兼容,64位的设备支持32位的函数库,但是会丢失64位函数库上优化过得性能,例如ART,webview,media等;

    就目前市场份额而言,绝大多数手机设备都已经是ARM(armeabi-v7a占较大的市场比重)类型的架构了,而很少使用mips和x86类型的架构。有人问为啥(谁问了?明明是你自己想说。。),这里简单说一下,其实很简单,高性能+低功耗。

    前面提到的主流的三种CPU架构有如下特点:

    1. ARM:体积小,低功耗,低成本,高性能,这意味着可以在处理复杂的操作,同时保持较高的续航能力,这种CPU架构简直是为手机设备量身定做的;
    2. MIPS:学术化的成果,学院派发展与风格导致在商用上不是很成功,目前在高清盒子,打印机等设备上运用较多,在手机上用的较少;
    3. x86:高性能,高功耗,目前属于PC市场的王者,虽然也在朝着移动端设备发展,但是由于续航等问题,暂时还是处于被ARM架构碾压的状态。

    书接上文,言归正传。再讨论回CPU架构指令的兼容问题,MIPS架构虽然不支持运行ARM指令,但是由于主流市场上几乎没有MIPS架构设备,所以可以不作考虑。而x86架构兼容ARM指令,ARM架构对ARM指令集向下兼容,所以实际上,可以只保留armeabi架构的so文件即可。

    在具体实现上,可以通过如下方法:

     2.5.2. 具体策略

    (1) 直接删除非armeabi的so和相应的文件夹

    但是要注意,必须要删除所有的非armeabi文件,因为如果只删除部分非armeabi文件夹下的so文件的话,APK打包的时候还是会生成armeabi-v7a等文件夹,在实际运营过程中,如果设备是armeabi-v7a架构的话,首先会去armeabi-v7a文件夹下寻找对应的so,如果已经存在armeabi-v7a文件夹,就不会去其他兼容性文件夹下面去寻找,这样就有可能会包so资源无法找到的错误。

    (2) Gradle中属性配置

    可以通过在Gradle中配置ndk属性和splits属性来分别实现so的瘦身,二者实现的效果是类似的。

    其中ndk的具体配置如下:

    1 defaultConfig {
    2     ndk {
    3         abiFilter "armeabi"
    4     }
    5 }

    同样可以使用前文提到的分包(splits)的方法(终于接上了…),具体来说,就是通过配置splits中的abi属性来实现,如下:

     1 android {
     2     ...
     3     splits {
     4         abi {
     5             enable true
     6             reset()
     7             include "armeabi"
     8             universalApk false
     9         }
    10     }
    11 }

    除此之外,AndroidManifest.xml清单文件,META-INF文件,assets等在APK中体积占比较小,且可压缩可见不大,一般在APK瘦身中不做重点考虑。

    3、总结与反思

    (1) APK瘦身涉及到用户切身的体验问题,是APK性能优化领域一件非常有意义的探索;

    (2) 在进行一件系统性事务的时候,应该从源头着手,做系统性分析,然后根据分析进行针对性突破,例如,在进行APK瘦身的时候,首先要对APK进行分析,找准努力的方向;

    (3) APK瘦身主要的着力点是资源文件的瘦身,dex文件的瘦身,资源索引resource.arsc的瘦身,以及so库的瘦身;

    (4) 组件化和插件化与APK瘦身也是息息相关,对于功能比较分散的大型APK,在应用市场放置一个壳子,在用户使用到相关功能的时候,动态下载和加载相关的功能代码,是一个不错的瘦身方案,同时,通过版本管理,还可以对插件功能进行热更新;

    (5) 代码质量意识一定要增强;整洁而不冗余的代码和资源,可以极大地防止APK体积快速挣增长。

    4、参考文档

    (1) https://techblog.toutiao.com/2017/05/16/apk/

    (2) https://tech.meituan.com/android-shrink-overall-solution.html

    (3) http://www.cnblogs.com/tianzhijiexian/p/4505312.html

    (4) https://zhuanlan.zhihu.com/p/21962184

    (5) http://mobile.51cto.com/aprogram-493310.htm

    (6) https://www.jianshu.com/p/02cb9a0eb2a0

     

  • 相关阅读:
    对象数组输出学生信息
    对象数组实现添加和显示客户信息
    控制台输出模拟注册登录幸运抽奖
    对象数组和for循环遍历输出学生的信息
    控制台输出<迷你DVD管理>
    CF524B 题解
    优先队列的重载运算符
    [洛谷日报第19期]Codeforces游玩攻略(转)
    最短路(三种基础算法)
    P2032 扫描
  • 原文地址:https://www.cnblogs.com/charles04/p/8547273.html
Copyright © 2020-2023  润新知