Android-爱奇艺APK瘦身经验

APK瘦身的价值

用户常常避免下载太大的APP,尤其是使用移动流量的情况,而且太大的APP也会占用更多的内存并消耗更多的资源,导致安装速度和加载速度变慢,在低配手机上,这些情况尤其严重。

作为中国互联网领先的手机APP,爱奇艺非常重视APP客户端的用户体验,始终关注APK的体积,并持续的跟进优化。

目前爱奇艺Android APK大小指标在视频行业甚至整个移动互联网已经处于领先地位,下面是我们在APK瘦身之路上的一些经验分享。

APK组成结构

在使用一些很酷的方法,来减少应用程序的大小之前,必须先了解实际的APK文件格式。

简单地说,APK是一个包含文件/文件夹的压缩文件。作为一个开发者,我们可以很容易的通过打开压缩文件的方式查看到APK里面的内容。

7zip打开APK后的视图

20171107151006945474877.png

各个文件或文件夹的功能

文件/文件夹 作用/功能
res 包含所有没有被编译到.arsc里面的资源文件
lib 引用库的文件夹
assets assets文件夹相比于res文件夹,还有可能放字体文件、预置数据和web页面等,通过AssetManager访问
META_INF 存放的是签名信息,用来保证apk包的完整性和系统的安全。在生成一个APK的时候,会对所有的打包文件做一个校验计算,并把结果放在该目录下面
classes.dex 包含编译后的应用程序源码转化成的dex字节码。APK里面,可能会存在多个dex文件
resources.arsc 一些资源和标识符被编译和写入这个文件
Androidmanifest.xml 编译时,应用程序的AndroidManifest.xml被转化成二进制格式

爱奇艺 APK各组成部分的占比情况

20171107151006957888426.png

通过爱奇艺Android客户端APK组成的饼状图可以看出,APK里面占较大比重的是libs,res,dex这三块。

APK瘦身方案

通过上面的分析,已经了解了APK的基本构成。下面我们就采用多种手段进行APK瘦身

针对整体优化

插件化

从应用功能扩张的角度看,APK包体积的增大是必然的,然而插件技术的出现很好的解决了这个问题。

通过分离应用中比较独立的模块,然后以插件的形式进行加载,比如爱奇艺Android客户端有很多的相对独立的功能,游戏,漫画,文学,电影票,应用商店等,都是通过插件的方式,从服务器下载,然后以插件的方式加载到我们的主工程。

7ZIP压缩

一般情况下面,AS直接编译生成的APK里面,.arsc文件是没有进行任何压缩的,前文中APK组成部分的第一张图就可以看出。

下面,我们来解压APK,重新用7zip进行压缩,就会发现几乎所有文件都变小了,特别是.arsc文件,减小的比较多。

20171107151006967981234.png

对比7zip压缩前和压缩后APK里面文件的变化,可以看出通过7zip压缩,.arsc文件大概减小了2M多,其它文件/文件夹体积也减小了5%左右。

签名方式

Google在Android7.0系统提供了新的apksigner签名工具,相比使用java提供的jarsigner签名工具,APK体积可以减小约5%(依赖文件数量)。

我们来看一下两种不同签名方式所带来的APK体积变化

20171107151006973045110.png

第一个APK是未签名的,第二个是使用jarsigner签名的,第三个是使用apksigner签名生成。可以看出,使用apksigner签名比使用jarsigner签名生成的APK减小了1.1M。

那么再来看一下这两种APK签名后的文件大小差异在哪里

20171107151006976254798.png

上图中间是未签名的APK,左边是jarsigner签名的,右边是apksigner签名的。

对比未签名的APK,用jarsigner签名工具签名,APK里面所有压缩后的文件和文件夹体积都增大了;而apksigner签名工具签名,除了META_INF文件夹增大了以外,其它文件和文件夹的大小都没有改变。

产生上述变化的原因是:jarsigner是针对每个文件进行了签名,然后针对签名后的文件计算摘要,并写入到META-INF文件夹下的MANIFEST.MF文件里面;而apksigner直接计算所有文件的摘要,写入MANIFEST.MF文件。

新的apksigner工具,已经集成到Android 7.0 SDK中了,使用方法可以参考官方文档

瘦身前后APK对比

在不同的版本通过不同方式进行APK瘦身的详情图,如下:

20171107151006982752667.png

插件化是2年前所做的优化,7zip压缩和签名方式都是最近的优化方案,并且通过jenkins自动化脚本实现的。

针对资源优化

移除重复的资源

一套资源

Android在适配图片资源的时候,如果只有一套资源,低密度手机会缩放图片,高密度手机会拉伸图片。我们利用这个特性,存放一套资源图就可以供所有密度的手机使用。

综合考虑图片清晰度,静态大小和内存占用情况,一般采用xhdpi下的资源图片。

重复资源

很多时候,随着工程的增大,以及开发人员的变动,有些资源文件名字不同,但是内容却完全相同。我们可以通过扫描文件的MD5值,找出名字不同,内容相同的图片并删除,做到图片不重复。

移除无用的资源

由于项目的迭代以及UI改版等各种因素,会导致工程项目里面有许多无用的资源的存在,定期扫描处理无用资源。

通过Lint工具扫描工程资源

当Lint工具扫描发现无用资源的时候,会输出如下的信息,就可以删除这种资源。

1
2
res/layout/preferences.xml: Warning: The resource R.layout.preferences appears
to be unused [UnusedResources]

需要特别注意的是,需要确保不存在反射,资源拼接等访问这些资源,才可以安全的删除掉这些资源,从而减小资源个数。

通过Gradle参数配置

如果工程比较大,由主工程和多个子工程组成的话,子工程里面也可能包含很多的无用资源。可以通过设置shrinkResources=true让Gradle移走无用的资源,否则默认情况下,Gradle编译只会移除无用代码,而不会关心无用资源。

1
2
3
4
5
6
7
8
9
10
11
12
android {
// Other settings
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles
getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

需要特别注意的是shrinkResources依赖于minifyEnabled,必须和minifyEnabled一起用,即打开shrinkResources也必须打开minifyEnabled。

通过开源扫描工具

大家可能会发现Lint不是非常好用,当工程里面存在反射,过滤结果非常麻烦。

所以我们实现了一个资源扫描的工具(https://github.com/zhuzhumouse/ScanUnusedResouce ),可以过滤掉通过反射调用的资源。

原理就是把所有java和xml文件以字符串扫描到内存,然后拿到资源文件(xml,png,jpg等)名称做匹配查找,如果没有匹配到,该资源就是无用资源,可以直接删除。

该扫描工具可以解决反射调用的问题,但是不能解决资源拼接的问题,还有就是不能处理存在很多资源前缀相同的情况。

png图片压缩

可以通过使用图片压缩工具对png图片进行压缩,压缩效果比较好的工具有:pngcrush,pngquant,zopflipng等,可以在保持图片质量的前提下,缩减图片的大小。

还可以通过网站对图片进行压缩,如比较有名的www.tinypng.com,该网站对上传的图片自动选择合适的压缩算法,压缩比比较高,但是只支持500张免费图片,更多图片处理是要收费的。

采用WebP格式

WebP分为有损压缩,无损压缩以及包含透明度的有损压缩。

有损WebP是基于VP8视频编码中的预测编码方法来压缩图像数据;无损WebP基于使用不同的技术对图像数据进行转换;有损WebP(支持透明度)区别于有损WebP和无损WebP,这种编码允许对RGB频道的有损编码同时可对透明度频道进行无损编码。

目前4.2及以上的手机系统已经支持WebP的无损和有损压缩,但是4.0,4.1的手机系统只支持不含透明度的有损压缩。如果应用支持的最低版本(minSdkVersion)是4.0,那么就只能针对不含透明度的图片进行WebP转换了。

在Android Studio 2.3版本及以上,我们可以选中 drawable 和 mipmap 文件夹,右键后选择 convert to webp,将图片转为 WebP 格式。如果Android Stuido版本比较低的话,可以直接通过官方提供的cwebp工具,将png转换为WebP。

下面是两张png转WebP的详情对比图

png (KB) WebP 75 (KB) WebP 90 (KB)
120 2.7 5.78
10 15 27

从以上两张样图的转换结果看,不是所有的图片都有高压缩比,有些图片压缩后反而会增大,比如第二张样图。WebP对色差比较小的图片,压缩比会比较高,任何一种压缩算法只能针对具有某种特点的图片进行压缩,没用万能压缩方法。

大背景图处理

对清晰度要求高的大图片,采用单纯的压缩方法就不能满足UE的要求了,需要找到一种非压缩方式来解决这个问题。

纯色图+后台下载的方式很好的解决了这个问题,客户端先使用纯色图片,然后大图从后端下载,这样只是启动的前几次使用纯色图,以后都会使用大图。

Lottie动画库的使用

动画,尤其是帧动画,一直都是相当占用资源的。现在可以通过Airbnb公司开源的Lottie动画库,直接用json文件来描述动画,然后直接加载绘制出来。

具体使用参考

其它资源策略

  1. 首先考虑能否不用图片,比如使用shape代码实现。
  2. 其次如果用图片的话,能否优先使用.9图来简化图片。
  3. 采用svg矢量图和VectorDrawable类来替换传统的图片。
  4. 如果图片只是旋转角度或者颜色不同,可以用代码实现变换。

资源瘦身前后APK对比

爱奇艺客户端使用到的资源优化方案详情如图所示

20171107151007036555517.png

目前爱奇艺客户端使用了这四种资源优化方式。

原来客户端xhdpi和xxhdpi下面有一部分重叠的资源,删除后包体积缩减了1M;移除无用资源是通过自己的扫描工具,获取无用资源列表,然后确认处理;pngquart压缩是打包过程中通过gradle自动化脚本实现的;WebP格式是通过python脚本,遍历查找不含透明度的图片,然后进行WebP转化替换原图片的。

针对代码优化

上面已经详细的介绍了资源文件的优化方法,通过这些优化,包体积得到明显的缩减,下面我们再来讨论一下代码的优化。

代码混淆

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

1
2
3
4
5
6
7
android {
buildTypes {
release {
minifyEnabled true
proguardFile('groguard.cfg')
}
}

下面是代码混淆前后APK的详情

20171108151007057113444.png

尤其需要注意的是:在proguard中,是否保留符号表对APP的大小是有显著的影响的,可酌情不保留,但是建议尽量保留用于调试。

无用代码扫描

同无用资源扫描方式一样,可以针对无用的代码进行扫描,这里需要关注的一点就是在插件里面通过反射的方法调用的主应用的一些类和方法是不能删除的。

也可以使用SonarQube扫描无用类,以及不同类里面的重复代码。

详情请参考

剔除R文件

随着项目中资源的增加,会发现生成的dex文件里面R.class文件越来越大。我们知道真正使用资源的地方都是以R.xxx.xxx这种方式访问的,而R.xxx.xx是对应于.arsc文件里面的一个常量值。arsc里面的内容具体如下:

字符串资源在.arsc文件里面的存储方式

20171108151007068485270.png

Layout下面的Xml资源文件在.arsc文件里面的存储方式

20171108151007070585423.png

通过这两张截图我们可以看出,直接用ID替换资源访问代码R.XXX.XXX,这样R.class文件就没有任何作用了,可以删除它,并且代码里面的资源访问字符串也变成了常量,两个方面都减小了dex的大小。

剔除R文件可以参考开源工具

注解替代枚举

谷歌官方一直强烈推荐用注解替代枚举,一方面可以缩减包体积,另一方便可以节省内存开销。我们来对比一下,在使用注解和使用枚举两种情况下,生成的class文件内容。

枚举类型源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum MarkViewType3{
SIMPLE_TEXT_MARK,
DO_LIKE_MARK,
BOTTOM_BANNER1,
BOTTOM_BANNER2,
TL_GREY_BACKGROUND_RANK,
/**
*服务导航mark
*/
SERVICENAVIRIGHTMARK,
/**
*搜索页热点事件,标题、评论、事件
*/
BOTTOM_COMPOUND_TEXT_BANNER
}

编译生成dex后的class文件

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum MarkViewType3
{
static
{
DO_LIKE_MARK = new MarkViewType3("DO_LIKE_MARK", 1);
BOTTOM_BANNER1 = new MarkViewType3("BOTTOM_BANNER1", 2);
BOTTOM_BANNER2 = new MarkViewType3("BOTTOM_BANNER2", 3);
TL_GREY_BACKGROUND_RANK = new MarkViewType3("TL_GREY_BACKGROUND_RANK", 4);
SERVICENAVIRIGHTMARK = new MarkViewType3("SERVICENAVIRIGHTMARK", 5);
BOTTOM_COMPOUND_TEXT_BANNER = new MarkViewType3("BOTTOM_COMPOUND_TEXT_BANNER", 6);
$VALUES = new MarkViewType3[] { SIMPLE_TEXT_MARK, DO_LIKE_MARK, BOTTOM_BANNER1, BOTTOM_BANNER2, TL_GREY_BACKGROUND_RANK, SERVICENAVIRIGHTMARK, BOTTOM_COMPOUND_TEXT_BANNER };
}
}

通过对比可以看到生成的class文件里面,每个变量都是一个对象,并且还有一个value对象数组。

注解的实现源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MarkViewType1{
public static final int SIMPLE_TEXT_MARK = 0;
public static final int DO_LIKE_MARK = 1;
public static final int BOTTOM_BANNER1 = 2;
public static final int BOTTOM_BANNER2 = 3;
public static final int TL_GREY_BACKGROUND_RANK = 4;
/**
*服务导航mark
*/
public static final int SERVICENAVIRIGHTMARK = 5;
/**
*搜索页热点事件,标题、评论、事件
*/
public static final int BOTTOM_COMPOUND_TEXT_BANNER = 6;
@IntDef ({SIMPLE_TEXT_MARK, DO_LIKE_MARK, BOTTOM_BANNER1, BOTTOM_BANNER2, TL_GREY_BACKGROUND_RANK
, SERVICENAVIRIGHTMARK, BOTTOM_COMPOUND_TEXT_BANNER})
@Retention(RetentionPolicy.SOURCE)
public @interface MarkViewType1Anno{
}
}

生成的class文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MarkViewType1
{
public static final int BOTTOM_BANNER1 = 2;
public static final int BOTTOM_BANNER2 = 3;
public static final int BOTTOM_COMPOUND_TEXT_BANNER = 6;
public static final int DO_LIKE_MARK = 1;
public static final int SERVICENAVIRIGHTMARK = 5;
public static final int SIMPLE_TEXT_MARK = 0;
public static final int TL_GREY_BACKGROUND_RANK = 4;
@Retention(RetentionPolicy.SOURCE)
public static @interface MarkViewType1Anno
{
}
}

注解生成的class文件只是一些常量。

通过上面的代码对比可以看出,常量+注解的形式,一方面可以减小生成的class文件的字节数,另一方面可以减小内存开销。

代码瘦身前和瘦身后APK对比

爱奇艺客户端代码优化详情如图所示

20171108151007085241950.png

由上图可以看出,代码混淆可以很大程度的减小包体积,尤其是引入了比较多点的第三方库的情况。所以打包的时候,应该开启代码混淆,以及资源混淆。

注解替代枚举,经过尝试,发现大量修改之后,对缩减包体积帮助不大,所以爱奇艺客户端没有采用该方案。

arsc文件优化

在剔除R文件小节中,大家已经看到了.arsc文件内容格式。在整体优化小节中,已经对.arsc进行了比较大的优化,接下来分析一下其它优化方式。

可以采用混淆来缩减资源文件的名称,以及移除未使用的备用资源等方式来优化.arsc文件。如何移除未使用的备用资源,gradle里面

增加如下配置:

1
2
3
4
5
6
android {
defaultConfig {
...
resConfigs "zh", "zh_CN", "zh_HK", "zh_MO", "zh_TW", "en"
}
}

通过该方式,爱奇艺客户端包体积可以缩减100多KB。

lib目录优化

只提供对主流架构的支持,比如arm,对于mips和x86架构可以考虑不提供支持,系统会自动提供相应的兼容。爱奇艺客户端只在armeabi下面放置了一套so库文件。

除了插件化,客户端还是用了RN的方案,从而引入了RN的so库。由于RN的so库资源比较大,有2M多,进而引入了RN的so库的插件化。通过so库的插件化,来缩减包体积。RN库的插件化,包体积就缩减了1M多。

包瘦身详情总结

通过上面所有方式进行瘦身,APK变化详情,如下图所示:

20171108151007094932891.png

由上图可以看出,经过代码优化,资源优化,lib库优化,.arsc文件优化,及整体优化,包体积由54.2M缩减到28.2M。

瘦身过程中遇到的问题

WebP支持问题

WebP图片的转化过程中,一定要注意资源拼接的情况。

比如如果存在vip_1,vip_2,vip_3,vip_4,vip_5等五个资源,要么都转化成WebP,要么都不转,不能处理其中的一部分。

替换一些引导图的时候,一定要打包工具和客户端同时替换。如果客户端把引导图替换成了WebP格式,而打包的时候,由于不同步,该图片又被替换成png格式,就会导致资源加载不成功,进而程序崩溃。

签名方式

使用apksigner签名工具前,必须先执行zipalign操作;而使用jarsigner签名工具则是先签名,然后再用zipalign优化。

小结

目前爱奇艺Android客户端主要通过插件化、RN、签名方式、7zip压缩、保留一套资源、代码资源混淆、无用资源处理、剔除R文件、图片压缩等方式来缩减包体积,包体积整体缩减了20M多。

缩减包体积是一个长期的任务,未来还有很多事情需要做,比如定期扫描无用代码和资源资源、图片持续优化、矢量图、Lottie动画的大量使用等等,随着新技术的涌现,我们会有更多的方法去缩小包体积,使得应用更轻盈运行速度更快。