最近几周一直在研究如何为APK瘦身,折腾了很久,是时候写篇博客总结一下了,虽然已经准备了下周一要在客户端周会分享用的PPT:APK瘦身探索。
价值
虽然说APK瘦身对于Android对应用可分配内存的限制影响不大,但是还是有一些影响的,就以图片
为例,将一些小图标替换为iconfont能有效减小内存的分配,防止OOM的出现。
另外,无论是iOS开发者还是Android开发者都应该尝试最好学会如何为IPA或APK瘦身,不仅仅是为了帮助用户省流量、减少下载时间、减少占用的存储空间等等,更重要的是为了提高转化率(注意:本文的转化率均指下载转化率)。
那转化率是什么呢?
举个栗子:你的应用大小是 18MB ,有100个潜在用户想要去下载尝试使用,结果有20个用户嫌弃安装包太大直接扬长而去,有20个用户在等待下载的过程中取消下载,最终只有60个用户真正下载安装,那么应用的转化率就是 60/100 = 60% 。
那为什么要提高转化率呢?
因为用户在纠结下载你的产品还是你的竞品的时候,往往会选择那个体验最好、功能最多、性能最好、包最小的。
工具
如何有条理的为APK瘦身,必须知道APK的目录结构,不过在讲述这个之前,我这里先介绍几个工具,在后文会用到。
AndroidStudio2.2.3
AndroidStudio升级到版本2.2.3之后提供了Analyze APK的功能(不过作为AS粉想必已经升到AS2.3了吧,哈哈),我们可以借助该工具清楚的了解APK的下载大小、解压之后的大小、内部各个文件夹或文件占用的大小等信息,进而得知那些地方可以优化。下图为目前我司测试版APK的内部信息:
另外使用该工具还可以反编译资源文件、还原layout中的资源id,分析DEX、显示每一个文件夹或文件的方法数,分析哪些第三方库方法数很多但实际只用到了一部分等其他功能。
最后如何使用该工具?有以下两种方式:
- 直接将APK拖拽进入AndroidStudio即可
- 点击菜单栏Build-Analyze APK...选项,再选择需要分析的APK即可
NimbleDroid
NimbleDroid 是美国哥伦比亚大学的博士创业团队研发出来的自动化分析Android app性能指标的系统,分析的方式有静态和动态两种方式:
其中静态分析可以分析出APK安装包中大文件排行榜,各种知名SDK的大小以及占代码整体的比例,各种类型文件的大小以及占排行,各种知名SDK的方法数以及占所有dex中方法数的比例,针对缓慢的方法,缓慢的第三方SDK和内存泄漏。
其中动态分析可以测量生成的速度、网络、内存和磁盘使用率。
我用了一下上面这个工具,感觉还是不错的,下面我们来看几张有逼格的图(图中数据来自于我司测试版APK):
APK内部各类型对应文件占用的大小
APK内部各个类库的方法数
APK启动过程中各个方法的执行时间
ClassShark
ClassShark 是一款查看Android执行文件(apk)的浏览工具,目前有两个android App(Apk)和桌面(jar)的版本。
使用这款工具,可以很方便的打开APK、Class、Jar、res等文件和分析里面的内容。
不过目前这个工具并不需要我们单独使用,因为AS内部的分析工具就是用的这个。
通过以上工具,我们可以方便快速的分析APK,找出那些可以优化的部分,为了更加有条理的进行讲解,下面我会按照APK的目录结构来逐条阐述。
APK目录结构
上述表格左边部分是APK内部默认存在的文件夹或文件,表格右边对于各个文件夹或文件进行了简要的概述,详细的说明会在之后根据左边的顺序一一讲解。
assets目录
assets目录用于存放需要打包到应用程序的静态文件,它包含以下几个特性:
使用AssetManager类管理资源
assets目录内部文件不会被系统编译
assets目录支持任意深度的子目录
-
获取资源需要使用/assets开始(不包含它)的相对路径名,具体代码如下所示:
String fileNames[] = context.getAssets().list(path);
那么,assets目录下都可以放什么文件呢?
说实在的,assets目录可以存放各种文件,不过正常情况下,一般只存放以下几种文件:字体文件、WEB页面、配置文件、某些图片。
上述几种文件除了配置文件之外,我们都可以进行适当的压缩处理:
字体文件
:可以使用字体资源文件编辑神器Glyphs进行压缩,其压缩方式其实就是通过删除不需要的字符从而减少APK的大小。
WEB页面
:可以考虑使用7zip压缩工具对该文件进行压缩,在正式使用的时候解压
某些图片
:可以使用tinypng进行图片压缩, 目前tinypng已经支持png和jpg图片、.9图的压缩
lib目录
lib目录用于存放通过C或C++编写编译生成的so文件(native库/JNI开发),其基本目录结构如下图所示:
因为目前市场上主流的架构还只是arm架构,所以如果不是必要的话,可以考虑不支持x86和mips架构,但这并不意味着CPU是x86或mips架构的手机就不能正常安装使用APK了,因为放在arm目录下的so库是可以兼容到其他架构的;
另外arm架构中的eabi-v7a相比于eabi只是在图形渲染方面有了很大的改进,所以如果so库对图形渲染没有很高的要求的话,完全可以把so库只存放在arm eabi目录中,这样可以大大减小APK的体积。
上面的说明可能比较笼统,下面就拿淘宝、微信的APK来说明一下:
可以看出淘宝和微信也是这样处理,所以我们其实也可以这样操作。
res目录
res目录用于存放应用程序的资源文件,主要包括布局文件、图片、XML配置文件等,它包含以下几个特性:
- 使用Resources类管理资源
- res目录内部文件会被系统编译
- res目录不支持任意深度的子目录
- 获取资源不需要通过相对路径找寻,因为文件会被系统编译,所以需要通过资源ID(注:资源ID被存放在resources.arsc文件中)查找
其基本目录结构如下图所示:
上图比较全面的列举了res目录下经常包含的子目录和文件,并解释了各个子目录或文件的含义。
根据上图我们显然可以发现,res目录就是我们APK瘦身里面的一大重要部分了,由于其包含的知识很大,下面我会分章节进行阐述,尽量一一阐述清楚。
只用一套图
首先我给出这么一个结论:一个APK尽量只用一套图片,从内存占用和适配的角度考虑,建议放在xhdpi文件夹下。
那么为什么要把图片放在xhdpi文件夹下面呢?
由于此处知识涉及到Android屏幕适配方面的知识,比较复杂。本文是关于APK瘦身的,所以就不详细讲解了。下面进行必要的阐述,首先让我们来看两张图:
从上面两张图我们可以得到以下两点信息:
- Android主要的设备分辨率为:1280*720、1920*1080
- iOS主要的设备分辨率为:1334*750、2208*1242、1136*640
可能有人要问了,得到这两点信息有什么用?
恩,单单这样看并不能知道什么,为了帮助大家了解,我绘制了下面这样表格。不过在展示表格之前,我先为大家灌输两个知识点:
1、ppi计算公式
2、在Android设备中,dpi 等价于 ppi
恩,此处请停留10秒......下面展示我绘制的表格:
有两个注意点:
- 表格中上两行代表的是Android的设备分辨率及相关数据,下三行代表的是iOS的设备分辨率及相关数据
- Android屏幕密度为320或480是由谷歌规定的,当然这里排除各大厂商自行修改的情况
根据上述表格我们可以清楚的知道iOS流行的设备分辨率正好对应Android流行的设备分辨率,那么知道这又有什么用?
除了一开始给出结论的内存和适配因素外,更重要的是可以节省设计资源和工作量。在现在的App开发中(iOS和Android),有些设计师为了保持iOS和Android的体验交互一致,可能会以iPhone手机为基础进行设计,包括后期的切图之类的。
不过根据上述表格,我们不是应该使用两套图吗?这里由于在Android设备中xhdpi和xxhdpi目录下的图片显示效果差异不大,所以完全可以只使用一套图。
现在我们的设备中只有一套图了,接下来该怎么为APK瘦身呢?
处理图片
当我们从设计师手中得到设计稿之后,之后的工作显然就是切图了,如果设计师切好图就更好了,得到图片之后我们一定要记得压缩
。
如果图片类型是.png或.jpg的话,我们可以使用tinypng进行压缩;如果图片类型是.gif的话,我建议使用PS进行压缩或裁剪,如果你不会PS的话,可以让设计师帮忙。
如果你对图片压缩质量不满意的话,还可以考虑使用不带alpha值的jpg图片、9Patch图片、同等质量下文件更小的WebP图片格式、或者使用SVG替换某些图片资源等其他方式。
下面对于最后两种方式进行简要的阐述:
首先是WebP
:AndroidStudio自2.3版本之后提供了Convert to WebP的功能,选中res目录后右击滑到底部即可看到此功能,下面的引用讲述了WebP的概念和支持度:
WebP是Google新推出的影像技术,它可让网页图档有效进行压缩,同时在质量相同的情况下,WebP格式图像的体积要比JPEG格式图像小40%,进而让整体网页下载速度加快。为了改善JPEG的图片压缩技术,他们使用了一种基于VP8编码的图片压缩器,利用预测编码技术,同时还采用了一种基于RIFF的非常轻量级的容器。这种容器只会给每张图片增加20字节,但能让图片作者保存他们想要存储的元数据。
android同样作为google的产品,minsdk为4.0即api14以上就可以支持webp,但是对透明的图片会存在一些问题,minsdk为4.2.1+即api17可以完美支持webp。
考虑目前市场4.2.1以下的手机占比已经非常稀少,采用webp格式代替jpg、png的方案非常可行。
下面展示一下我司某张图片png形势下和WebP形势下各自的大小:
其次是SVG
:SVG是可缩放矢量图形(Scalable Vector Graphics),它使用XML格式定义图像,可用于替换APK中的图标。
我们开发者不需要会如何制作,这是设计师的工作,当然设计师也不会傻乎乎通过写代码的方式绘制图标,显然是使用软件制作的。
目前我司也使用了SVG,不过它的表现形式是阿里巴巴提供的iconfont,它不仅允许设计师自己制作SVG图片上传,也提供了百万种图标供选择。
减少资源
经过上述的方式处理图片,想必在图片质量方面已经无计可施了,所以我们只能通过删除无用图片的方式来为APK瘦身了,当然接下来要介绍的两种方式可不仅仅是减少图片,应该称之为减少资源
。
这里有个问题:为什么会出现无用的资源呢?我认为有以下几种情况:
- 由于多人协作开发,没有好的规范,导致个人思维严重,随便添加或删除资源
- 目前公司有好的规范,但之前没有,历史遗留问题,没人愿意去管理
- 上一版需要该资源,下一版不需要,然后没有删除
那么,既然知道了原因,就应该想出办法去解决,首先是最简单的办法,我们只需要在主模块的gradle文件中配置shrinkResources
即可,具体配置方法如下图所示:
这就是为什么说它是最简单的减少资源方法的原因。
当然,因为简单必然存在缺陷,因为它只能从打包的应用程序和第三方代码库中删除未被引用的资源。如果在不同的资源文件夹下面有同名的资源文件,那么就没有办法删除了,即使该资源真的没有被使用。
所以,没有好的办法了,目前我能想到的只能是手动删除了,我们可以通过AndroidStudio提供的Remove Unused Resources功能预览删除真正未使用的资源,选中res目录右击选择Refactor-Remove Unused Resources...选项之后会出现下面这样的图:
然后根据查找到结果,逐条双击打开文件,按快捷键fn+option+F7进行查找,然后在分析是否应该删除,注意:此处定要慎重,务必自测!!!。
另外,我们还可以通过将某些图片转换网络图片的方式解决,但是这个操作也是需要慎重的,最重要的一点是不能影响用户的体验。这边路飞同学制作了一个Chrome插件来帮助我们快速上传图片到cdn中,可以通过此处下载:joyuploader。
最后,还有一种办法:使用AndroidStudio提供的Lint工具对工程做静态代码检查,它不仅可以找出我们在代码编写上面的失误,还能够列出那些未被使用的资源,甚至还可以指出哪些地方可能存在内存泄漏等等,功能非常庞大,所以如果工程较大的话,还是比较耗时的,当然这并不是问题,因为我们可以针对某个模块执行静态代码检查。我们可以通过选中菜单栏Analyze-Inspect Code...选项执行静态代码检查,执行完成的效果图如下所示:
资源混淆
目前我们不仅在资源(特指图片)质量方面做到了极致,在资源数量方面也做到了极致,看起来真的到极致了。其实不然,我们还可以使用资源混淆的方式为APK瘦身,通过压缩文件内容,减少文件名长度的方式实现。
目前比较出名的资源混淆方式是微信的AndResGuard和美团的修改AAPT,不过由于美团的资源混淆方法非常麻烦,还有如果需要通过getIdentifier
的方式获取资源时需要保证这些资源的名字不被混淆,美团很难实现,所以目前大家都在用微信的资源混淆方式,因为微信使用方便,而且提供了白名单。
下面是我司测试版APK未使用微信资源混淆和使用了微信资源混淆的差异:
可以清楚的看到,在使用了微信资源混淆之后,APK减少了0.7MB左右,效果还是十分明显的。
那么为什么使用了微信资源混淆之后可以实现APK瘦身呢?下面这两张图清楚的展示了瘦身的原因(摘自微信资源混淆官方文档):
总结,安装包大小减少的原因以下四个:
相对的,可得到影响效果的因素有以下几个:
可以说,直到现在,我们对于资源的压缩猜到了一个极致,下面是一些针对于某些资源的压缩办法。
其他资源
- 如果raw文件夹下有音频文件,尽量不要使用无损的音频格式,比如wav。可以考虑相比于mp3同等质量但文件更小的opus音频格式。
- 能不用图片的就不用图片,除了通过前面提过的使用9Patch图、iconfont实现外,还可以使用shape代码实现,也可以考虑引进VectorDrawable(矢量图) ,从5.0(API等级21)开始,android开始支持矢量图。目前矢量图兼容到API7,矢量图动画兼容到API11。这里就不详细讲解了。
终于讲完了res部分,码字好累,接下来就相对轻松了,首先我们要讲解的是classes.dex文件。
classes.dex
首先,什么是classes.dex?classes.dex文件是Android系统的可执行文件,包含应用程序的全部操作指令以及运行时数据。
这里稍微介绍一下classes.dex文件的生成过程及对应的命令:
java源代码 -> class字节码:[javac -source 1.6 -target 1.6 com/package1/*.java com/package2/*.java]
class字节码 -> jar包:[jar cvf abc.jar com/package1/*.class com/package2/*.class]
jar包 -> dex文件:[dx --dex --output ***/abc.jar ***/abc_dex.jar],***是abc.jar的绝对路径。
从jar包到dex文件的过程是通过dx工具完成的,它的目的是使各个类能够共享数据,在一定程度上降低了冗余,同时也使文件结构更加紧凑,实验表明,dex文件是传统jar文件大小的50%左右,具体表现形式可以看下图:
既然dx工具已经办帮我们压缩了那么多,那我们还有什么好压缩的呢?
当然有,因为dx工具并没有压缩class的内容,所以我们的源代码并没有得到压缩,下面我先介绍两种常见的办法来解决这个问题:
和之前最简单的资源压缩方式一样,我们也可以在主模块的gradle文件中配置
minifyEnabled
实现代码混淆,从而减小java文件的大小。因为混淆后的代码将较长的文件名、实例、变量、方法名等等做了简化,从而实现字节长度上的优化。我们也可以使用之前所说的AndroidStudio提供的Lint工具执行静态代码检查,进而删除无用的类、方法、变量等。
上面的两种方式虽然能够帮助我们减少APK的大小,但是实际上我们还可以做的更多。因为上面两种方式是自动化的,所以必然不像人那么智能。
有时候我们可能遇到这样的情况:我们引用了一个第三方类库,但是只用到了其中的几个功能,其他的大部分功能一直不用,这不是白白的浪费了用户的流量,降低了APK的下载转化率么?为了解决这样的问题,我们必须借助一些工具去找到这样的类库,比如在文章开头介绍过的工具:NimbleDroid,下面是我司APK目前存在的某个这样的第三方类库:
该类库在我司APK中只用到了扫一扫和生成二维码这两个功能,然而这个类库定义的方法数竟然有1428
,显然不合理,完全可以抽离其内部的扫一扫和生成二维码代码,单独实现。
ReDex
最后介绍一下Facebook开源的一个减少APK大小以提高性能的工具 -- ReDex,它通过内嵌以及清除僵尸代码这样的优化方式来减少字节码,其主要是对DEX做了优化,能够让APK运行更快,不过需要多测试是否会崩溃。
下面分别是ReDex的教程地址和GitHub地址:
Open-sourcing ReDex: Making Android apps smaller and faster | Engineering Blog | Facebook Code | Facebook
facebook/redex: A bytecode optimizer for Android apps
我个人由于时间问题,目前还未接入该工具,下面就展示一下网友u012124438的测试数据好了:
后来我在使用 Redex 压缩和优化 Android APK一文的帮助下,成功的安装并使用了ReDex,但是发现了以下两个问题:
- 用ReDex处理过的APK,其内部的META-INF目录会被删除,虽然可以将ReDex处理过的APK用AndResGuard处理一下,会重新出现该目录。
- 用ReDex处理过的APK,安装好之后会出现Crash的问题,目前我司测试版APK要Crash三次之后才能正常使用,而且它对于我司APK的贡献只有20~25K,所以并未打算接入。
resources.arsc
最后就是对于resources.arsc文件的压缩处理了,其实这里我们也不需要做什么,首先因为该文件记录的是资源文件和资源ID的映射关系,并没有什么值得压缩的地方;另外如果真的有值得压缩的地方,也只有资源文件的名字了,不过这个早就在之前因为我们使用了微信资源混淆解决了,所以此处就不再多讲了。
至此,针对于APK目录结构,逐条分析如何为APK瘦身已经讲完了,接下来介绍一下其他的压缩方式。
其他方式
使用APK Splits构建APK
虽然我们上面很好的使用了resource shrinker可以回收一些未使用的资源(v7、v4、google Service 等Libarry资源),但有些资源仍然未被清除。
例如:那些未使用的多套替代资源,或者是library内部隐患着引用着的资源而我们却没有使用到。或者是我们要根据用户的手机去提供不同版本的APK,如分辨率(xxhdpi,mhdpi等),so库等。那么我们可以使用APK Splits大大的减少一些无用的资源,这里我们主要参考了http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits文档。
使用多版本的APK
Multiple APK Support是一个在Google Play,可以发布不同的应用程序,分别针对不同的设备配置特征。每个APK是一个完整的、独立的应用程序版本,但他们分享在Google Play相同的应用程序清单,必须共享相同的包名和与签名。Google Play 会自动给你匹配相应的APK,这样我们的APK 就可以是分不同版本构建需要资源文件,从而减小APK的大小。
资源动态加载
我们可以在项目中使用资源动态加载形式,例如:表情,语言,离线库等资源动态加载,减小APK的大小。
支持插件化
未来对于一些独立业务模块,可以做成插件化动态加载,用户需要使用时,只需下载少部分插件。
使用shape背景
特别是在扁平化盛行的当下,很多纯色的渐变的圆角的图片都可以用shape实现,代码灵活可控,省去了大量的背景图片。
使用着色方案
相信你的工程里也有很多selector文件,也有很多相似的图片只是颜色不同,通过着色方案我们能大大减轻这样的工作量,减少这样的文件。
借助于android support库可实现一个全版本兼容的着色方案,参考代码:DrawableLess.java
其他方式...
总结
如果你不是一个Android开发人员,对于之前的阐述可能非常模糊,并不能明显的表现出APK瘦身这件事情的意义,下面我就来总结一下:
首先,针对于我司测试版APK做了哪些应用?
字体文件使用Glyphs进行压缩,图片使用tinypng进行压缩
只是用一套so文件
只使用一套图
使用WebP图片或SVG图片替换某些PNG或JPG图片
使用Google提供的Gradle插件实现代码混淆和资源混淆
借助NimbleDroid、AS Anylze/Lint工具分析查找删除不需要的代码或资源
使用微信提供的资源混淆工具(处于稳定性测试阶段)
使用Facebook提供的ReDex工具(处于调研阶段)
其次,APK瘦身的效果如何?
这里我们就直接看图好了:
可以明显的发现,我司的APK越来越小了,所以说,这个工作是很有意义的。
最后,做一下竞品分析?
根据上面图表,可以发现我们的APK的大小是十分有优势的,能够很好的提高下载转化率。而iOS那边显然就比较大了,虽然两者之间不能比较,但是还是可以进行一系列优化的。
忠告
最后的最后,我想对大家说:在APK瘦身的道路上,一定要掌握好度
,安排好事情的优先级,如果目前要做的事情、要优化的方面比较复杂,不仅需要花费很长的时间,而且最终效果也不明显,可以考虑之后再做,甚至不做。
参考链接
- 【Android技术专题】APK瘦身看这一篇文章就够了
- 那些你不知道的 APK 瘦身,让你的 APK 更小
- Android 性能优化系列 之 apk 瘦身
- APK文件结构和安装过程
- Android开发之资源目录assets与res/raw的区别分析
- Android开发之assets目录下资源使用总结
- Android开发:最全面、最易懂的Android屏幕适配解决方案
- 是时候使用 webp 给 apk 瘦身了!
- Dex文件格式详解
- Android应用瘦身,从18MB到12.5MB