Android Apk瘦身指南大全

前言

       为什么Android应用Apk要瘦身?虽然安装包大小对应用使用没有影响,但应用的安装包越大,用户下载的门槛越高,安装等待时间也会越长,特别是在移动网络情况下,用户在下载应用时,对安装包大小的要求更高;对于产品本身,意味着下载转化率会越低,因为竞品中,用户有更多机会选择那个体验最好,功能最多,性能最好,包最小的,所以apk的瘦身优化也很重要,因此,减小安装包大小可以让更多用户愿意下载和体验产品。

Apk分析

       知己知彼,方能百战不殆。了解应用程序Apk的结构对于我们来说很有帮助。Apk文件由一个ZIP存档组成,其中包含组成应用程序的所有文件。这些文件包括Java类文件,资源文件和包含编译资源的文件。在Android Studio工具栏里,打开build–>Analyze APK, 选择要分析的Apk包;或者直接找到Android Studio项目中的app/build/outputs/apk目录下的apk直接双击打开,即可看到如下结构:

 

主要目录如上图所示,Apk包含以下目录:

  • assets文件夹
    存放一些配置文件、资源文件,assets不会自动生成对应的 ID,而是通过 AssetManager 类的接口获取。
  • res目录
    res 是 resource 的缩写,这个目录存放资源文件,会自动生成对应的 ID 并映射到 R 文件中,访问直接使用资源 ID。
  • lib
    包含特定于处理器软件层的编译代码。该目录包含了每种平台的子目录,像armeabi,armeabi-v7a, arm64-v8a,x86,x86_64,和mips。
  • META-INF
    保存应用的签名信息,签名信息可以验证 APK 文件的完整性。
  • AndroidManifest.xml
    这个文件用来描述 Android 应用的配置信息,一些组件的注册信息、可使用权限等。
  • classes.dex
    Dalvik 字节码程序,让 Dalvik 虚拟机可执行,一般情况下,Android 应用在打包时通过 Android SDK 中的 dx 工具将 Java 字节码转换为 Dalvik 字节码。
  • resources.arsc
    记录着资源文件和资源 ID 之间的映射关系,用来根据资源 ID 寻找资源,包含已编译的资源。该文件包含res/values/ 文件夹所有配置中的XML内容。打包工具提取此XML内容,将其编译为二进制格式,并将内容归档。此内容包括语言字符串和样式,以及直接包含在resources.arsc文件中的内容路径 ,例如布局文件和图像。

Apk瘦身

资源

使用一套资源

        对于绝大对数APP来说,只需要取一套设计图就足够了。鉴于现在分辨率的趋势,建议取720p的资源,放到xhdpi目录。
        相对于多套资源,只使用720P的一套资源,在视觉上差别不大,很多大公司的产品也是如此,但却能显著的减少资源占用大小,顺便也能减轻设计师的出图工作量了。
        注意,这里不是说把不是xhdpi的目录都删除,而是强调保留一套设计资源就够了。

去除无用资源
  • 首先我们可以使用lint工具,点击 Android Studio 工具栏 -> Analyze -> Run Inspection By Name如果有没有使用过的资源就会打印如下的信息:
     
  •  
  • 选择 Unused resources,再选择范围后就开始检测:
     
开启shrinkResources去除无用资源

        在gradle使用shrinkResources去除无用资源,效果非常好。

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            ...
    }
}
清理无用资源

        版本迭代过程中,不但有废弃代码冗余,肯定会有无用的图片存在。在build.gradle 里面配置shrinkResources true,在打包的时候会自动清除掉无用的资源,但经过实验发现打出的包并不会,而是会把部分无用资源用更小的东西代替掉。注意,这里的“无用”是指调用图片的所有父级函数最终是废弃代码,而shrinkResources true 只能去除没有任何父函数调用的情况,真正起效果只能通过Android Studio自带的 “Remove Unused Resources”小插件来实现了,直接上图。

 


 

        比较人性化是该查找结果可以“一键删除”。当然,可能图片是经过反射或字符拼接等方式获取,所以这个检测列表也不是全对,删除的时候还是要建立在熟悉业务及代码的情况下删除,防止出错,删除不该删除的资源。

删除无用的语言资源

        大部分应用其实并不需要支持几十种语言的国际化支持。还好强大的gradle支持语言的配置,比如国内应用只支持中文:

android {
    defaultConfig {
        resConfigs "zh"
    }
}
删除so包
  • 删除armeabi-v7包下的so
    基本上armeabi的so也是兼容armeabi-v7a的,armeabi-v7a的库会对图形渲染方面有很大的改进,如果没有这方面的要求,可以精简。
    这里不排除有极少数设备会Crash,可能和不同的so有一定的关系,请大家务必测试周全后再发布。
  • 删除x86包下的so
    x86包下的so在x86型号的手机是需要的,如果产品没用这方面的要求也可以精简。
    建议实际工作的配置是只保留armeabi、armeabi-x86下的so文件,算是一个折中的方案。
使用微信资源压缩打包工具

        微信资源压缩打包工具通过短资源名称,采用7zip对APP进行极致压缩实现减小APP的目标。

        详情参考: 微信Android资源混淆打包工具

图片

使用jpg格式

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

使用webp格式

        webp支持透明度,压缩比比jpg更高但显示效果却不输于jpg,官方评测quality参数等于75均衡最佳。

        相对于jpg、png,webp作为一种新的图片格式,限于android的支持情况暂时还没用在手机端广泛应用起来。从Android 4.0+开始原生支持,但是不支持包含透明度,直到Android 4.2.1+才支持显示含透明度的webp,使用的时候要特别注意。

减少动画帧数

        使用帧动画会大大增加APK的大小。可以减小帧率,减少图片数量。

重用资源

        Android提供了很多方式改变资源的颜色。对于Android 5.0及以上,使用android:tint和tintMode属性。对于更低版本,使用ColorFilter类。
        你也能够删除那些只是对另一个资源做旋转的资源。下面的代码片段提供了对一个箭头旋转180度。

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

        你也能通过代码绘制图像,从而减少APK大小。代码方式绘制图像不需要任何空间因为你不再需要在APK中存储图像文件。

设计缩小大图

        如果经过上述步骤之后,你的工程里面还有一些大图,考虑是否有必要维持这样的大尺寸,是否能适当的缩小。事实上,由于设计师出图的原因,我们拿到的很多图片完全可以适当的缩小而对视觉影响是极小的。

使用shape背景

        很多纯色的渐变的圆角的图片都可以用shape实现,代码灵活可控,省去了大量的背景图片。

矢量图

        矢量图是由点与线组成,和位图不一样,它再放大也能保持清晰度,而且使用矢量图比位图设计方案能节约30~40%的空间,现在谷歌一直在强调扁平化方式,矢量图可很好的契合该设计理念。

  • 优势
  • 占用存储空间小
  • 无极拉伸不会出现锯齿,可以照顾不同尺寸的机型
  • Android Studio自带很多资源,减小UI工作量
  • 劣势
  • 只支持5.0及以上系统
  • 与位图相比多了一层计算,需消耗更多性能
  • 不支持.9图
  • 不适合表现真实照片和复杂图形,一般使用在简单的icon和动画上

代码

开启minifyEnabled混淆代码

        在gradle使用minifyEnabled进行Proguard混淆的配置,可大大减小APP大小:

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

        proguard-rules.pro 文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle 文件旁)。
        在proguard中,是否保留符号表对APP的大小是有显著的影响的,可酌情不保留,但是建议尽量保留用于调试。

参数:

-include {filename}    从给定的文件中读取配置参数   
-basedirectory {directoryname}    指定基础目录为以后相对的档案名称   
-injars {class_path}    指定要处理的应用程序jar,war,ear和目录   
-outjars {class_path}    指定处理完后要输出的jar,war,ear和目录的名称   
-libraryjars {classpath}    指定要处理的应用程序jar,war,ear和目录所需要的程序库文件   
-dontskipnonpubliclibraryclasses    指定不去忽略非公共的库类。   
-dontskipnonpubliclibraryclassmembers    指定不去忽略包可见的库类的成员。

保留选项:

-keep {Modifier} {class_specification}    保护指定的类文件和类的成员   
-keepclassmembers {modifier} {class_specification}    保护指定类的成员,如果此类受到保护他们会保护的更好   
-keepclasseswithmembers {class_specification}    保护指定的类和类的成员,但条件是所有指定的类和类成员是要存在。   
-keepnames {class_specification}    保护指定的类和类的成员的名称(如果他们不会压缩步骤中删除)   
-keepclassmembernames {class_specification}    保护指定的类的成员的名称(如果他们不会压缩步骤中删除)   
-keepclasseswithmembernames {class_specification}    保护指定的类和类的成员的名称,如果所有指定的类成员出席(在压缩步骤之后)   
-printseeds {filename}    列出类和类的成员-keep选项的清单,标准输出到给定的文件

压缩:

-dontshrink    不压缩输入的类文件   
-printusage {filename}   
-whyareyoukeeping {class_specification}

优化:

-dontoptimize    不优化输入的类文件   
-assumenosideeffects {class_specification}    优化时假设指定的方法,没有任何副作用   
-allowaccessmodification    优化时允许访问并修改有修饰符的类和类的成员

混淆:

-dontobfuscate    不混淆输入的类文件   
-printmapping {filename}   
-applymapping {filename}    重用映射增加混淆   
-obfuscationdictionary {filename}    使用给定文件中的关键字作为要混淆方法的名称   
-overloadaggressively    混淆时应用侵入式重载   
-useuniqueclassmembernames    确定统一的混淆类的成员名称来增加混淆   
-flattenpackagehierarchy {package_name}    重新包装所有重命名的包并放在给定的单一包中    
-repackageclass {package_name}    重新包装所有重命名的类文件中放在给定的单一包中   
-dontusemixedcaseclassnames    混淆时不会产生形形色色的类名   
-keepattributes {attribute_name,...}    保护给定的可选属性,例如LineNumberTable, LocalVariableTable, SourceFile, Deprecated, Synthetic, Signature, and InnerClasses.  
-renamesourcefileattribute {string}    设置源文件中给定的字符串常量
清理第三方库和冗余代码

        版本迭代过程中,因为删减功能经常有冗余代码和第三方库留下,这或多或少都会增加包体,这种情况没有捷径,只能每个文件查找,这是苦力活。还有要查看第三方库有没可能精简,比如谷歌分基础、广告和分析包,网络库、supportv4等,这个就具体情况具体分析,不多阐述。

移除枚举

        一个枚举能让classes.dex文件增加1–1.4K。枚举的加入会快速增加应用体积。我们可以使用@IntDef注解和Proguard代替枚举,它能提供和枚举一样的类型安全转换。

减少Native库的大小

        如果你的应用使用了Native代码和Android NDK,你也能通过优化代码减少应用体积,这里介绍的两个技巧是删除调试符号和避免抽取Native库。

其他

使用provided编译

        对于一些库是按照需要动态的加载,可能在某些版本并不需要,但是代码又不方便去除否则会编译不过。
使用provided可以保证代码编译通过,但是实际打包中并不引用此第三方库,实现了控制APP大小的目标。
但是也同时就需要开发者自己判断不引用这个第三方库时就不要执行到相关的代码,避免APP崩溃。

支持插件化

        插件化技术支持动态的加载代码和动态的加载资源,把APP的一部分分离出来了,对于业务庞大的项目来说非常有用,极大的分解了APP大小。
        因为插件化技术需要一定的技术保障和服务端系统支持,有一定的风险,如无必要(比如一些小型项目,也没什么扩展业务)就不需要了,建议酌情选择。

在线化素材库

        如果你的APP支持素材库(比如聊天表情库)的话,考虑在线加载模式,因为往往素材库都有不小的体积。
        这一步需要开发者实现在线加载,一方面增加代码的复杂度,一方面提高了APP的流量消耗,建议酌情选择。