Android 包体积优化思路总结

前言

包体积优化的文章很多,也有很多非常前沿的技术,很多产品都非常重视包体积,因为包体积不仅仅影响的是性能(主要是磁盘和内存I/O),而且还影响新装用户量。

关于包体积的优化,主要有以下手段:

  • 删除
  • 压缩
  • 动态化
  • 矢量图
  • 自绘制

包体积优化的手段包括常规手段,其中常规手段是最容易实现且收益最明显的手段,非常规手段是优化长尾数据非常有利的工具。

目前而言,各种工具都集中非常规手段方面的优化,其实,在非常规手段中,也有很多简单易用的手段,本篇会进行一些补充。

常规手段

在开始本篇之前,我们来梳理一下目前常用的手段。

删除无用资源

删除无用资源是获得无副作用净收益的最有效的手段,主要有以下几点

  • shrinkResources=true可以在打包时删除无用的代码和资源,此方法需要和minifyEnabled=true一起使用
  • Lint或者Android Studio中的 Code Clean、Unused resources查找,或者实现python脚本查找未使用的资源,提前手动删除资源
  • 删除帧动画中重复的图片,帧动画时间间隔不小于32ms(32ms为2个vsync信号,人眼每秒只能接受24帧,那就是每41ms一张图片,但是3*16=48ms显然不满足,只能在32ms)
  • 简单纯色图片删除,尽量使用代码实现
  • 只保留有限的so库的arm版本
  • 保留有限的语言资源

资源压缩

压缩分为有损压缩和无损压缩,显然,有损压缩能取得最大化的收益,因为不仅仅能优化包体积,还能减少i/O数据量和内存占用,但是有损压缩有一定的副作用,那就是容易出现图片质量下降,不过出现这种问题的概率也是比较低的,如果视觉走查阶段能有效处理的话。

有损压缩

  • 有损压缩一般分为质量压缩和尺寸压缩,首先常用手段有
  • 图片转码:JPEG(包含很多信息,而且色彩更加丰富)、PNG转为WebP、AVIF格式等,目前主流方式WebP占比较多,同时AVIF优势也很明显,后者受到很多大厂青睐。
  • 图片缩小:这种属于尺寸压缩,一般来说是转为缩小后转为.9.png图片,另外就是将图片全部转移至res/mipmap文件下,后者相比res/drawable的优势是在加载图片时会进行线性过滤,减少锯齿和抖动。
  • TinyPng、pngQuant压缩等工具压缩,下面是常用工具的对比
  • 代码混淆:代码混淆是一种收益非常明显的dex压缩方式,混淆dex压缩最有效的手段。

无损压缩

简单来说,无损压缩是一种减小磁盘空间占用和减少内存i/o的压缩方式,但是无法减少内存占用。常见的无损压缩工具有。

  • Bitmap#compress 无损压缩,Bitmap可以让图片质量更低,磁盘空间更小、i/o耗时更少的编码压缩,但是内存占用压缩前后一致。
  • zip 压缩:这是一种常见的压缩方式,比较适合CDN方式的,Android中如果想要接入的话,需要使用BitmapFactory#decodeStream + ZipInputStream进行解码

动态化

动态化本质是将资源部署在服务器上,动态下发,常用的手段有

  • CDN部署资源 + Glide加载
  • 图片资源插件化 + AssetManager加载
  • so插件化 + ClassLoader 加载

矢量化

矢量化也是一种非常有效的手段,常用的矢量化手段有SVGA、字体图标等,矢量化可以实现放大缩小不失真,但是缺点是比较受限于纯色图片。

自绘制

计算机中,算法和空间是一对兄弟,当然并不是此消彼长,但是在很多情况下,通过自绘制可以有效的减少包体积,同时还具备矢量性质,在一些情况下,自绘制还可以用于减少布局层级(层级少可以减少xml的体积),也属于收益很大优化方式,不过这点取决于开发者自身水平,当然,常用的手段有如下方式。

  • 自定义drawable.xml文件,相比图片,自定义的xml有很多优势,可以有效减少纯色效果的资源使用,如Layer-List组合多种图片,selector实现状态变化等。
  • 自定义drawable,Android系统中内置了一些Drawable,我们还可以自定义各种Drawable,如MoonDrawable(每月1-30日的月亮形状),至于自定义的Drawable如何加载,可以参考之前的文章《Android 利用换肤技术,实现自定义Drawable从Xml中加载
  • 自定义View,一些复杂的动画效果、帧动画效果实际上可以通过自定义View实现。同样,对于一些卡片,我们完全可以使用自定义View实现,不仅仅能减少布局层次,还能减少资源的使用。
  • 硬编码PaintDrawable,相比于drawable.xml 无法动态设置倒角的问题,可尝试使用PanitDrawable解决
  • 硬编码ColorStateList、StateListDrawable,对于复用性不强但是效果变化较多的drawable,可以使用硬编码方式
  • 使用Tint Color:Android提供了对于纯色图片的优化手段,因此,对于纯色图片,如需变色可以通过Tint Color实现。

以上都是常规优化手段,下面我们来看看非常高的优化手段。

非常规优化

非常规优化手段有一定的技术要求。

使用 AndResGuard

使用AndResGuard可以有效缩短资源名称和目录名称,从而减少resources.arc和res目录的体积。

facebook redex

redex是基于字节码的优化,属于javac的增强手段,如内联、无用变量删除等,可以一定程度上减少包体积。

减少String相关代码段

代码中不可避免的会有String,如日志的输出,实际上,dex中String的占比非常多,因此,解决这些办法最有效的手段还是减少日志输出。

基本类型常量化

对于基本类型变量和字面量字符串,声明为final类型可以减少iget等操作。

kotlin方法内联优化

内联的正作用是减少方法寻址和跳转,但是缺点是会引发包体积增大。实际上,对于方法体内是单行代码的情况,内联是非常必要的,但是多行代码,就要考虑是否值得。

反-反混淆

在Android,你最常用的混淆规则是配置Serializable相关了,如果不配置可能引发Gson、持久化功能异常

java 复制代码
-keepnames class * implements java.io.Serializable

但是,Serializable范围可能比较大,一些没必要保留的类无法彻底混淆,如google-guava的一些数据结构,建议自定义规则,而不是笼统的使用Serializable

java 复制代码
public interface KeepGuard {
}

然后使用下面方式

java 复制代码
-keepnames class * implements a.b.KeepGuard

当然,如果是老项目,自定义KeepGuard 可能还有风险,那就使用,相当于通过黑名单方式反-反Serializable混淆

java 复制代码
-applymapping op_mapping.txt

最小化引用so库

对于一些简单的功能,如图片处理、动效等,如果只占引用库能力的1/3甚至更小,这个时候就要考虑其他手段和裁剪第三方so库,比如引入open cv、libjpeg等是否合适。

【死入口】代码删除

前面讲过shrinkResources可以删除无效引用资源和class,但缺陷是无法对【死入口代码删除】

很多开发者在下架某些功能之后,在xml或者直接用setVisiblity(View.GONE)隐藏了入口,或者是使用boolean变量写死了跳转逻辑,但是整体的代码引用链保持存在,这就无法导致入口后面的资源占用被删除。

这里建议对于下架的功能,要remove而不是hide,如果担心后续重新上架,那就是后续版本的问题,不可保留。你可以不相信自己,但你得相信Git。

字体裁剪

前面说过,引入字体图标可以有效减少包体积,然而,有意思的是,一些设计为了实现特殊字体,会让你引入好几兆的字体,但你仅仅需要的可能就那么几个字,这个时候你得找一款字体裁剪工具,删掉无用的字体。

ASM指令优化

除了facebook的redex,开源的byteX或者booster都可以做到一些字节码优化。

同时如果在代码实现中,减少变量的定义或者使用位运算也是减少指令有效的方式。

转视频编码

这种方式也是一种方法,通常意义上,将图片编码为小于0.5s的单帧视频,能获得更小的压缩比率,但是缺点是比较依赖MediaMetadataRetriever的解码速度,另一个限制点是必须符合【视频的宽高比】,同时建议不要大于720p,防止一些设备无法解码大于720p以上的资源。

减少面向未来的编程

一些开发者为了提前布局后续的需求,加入了很多后续版本才需要的资源或者库,这是不明智的,正确的做法是提高可扩展性,面向未来的编程要尽可能避免。

减少重复资源

通过md5、crc32、pHash(相似度为0)检索出重复资源,进行删除。当然这里给一个技巧,重复资源检索有很多种方式,如果追求速度的话那就使用size比较,两者相等的"一般"都重复,而且md5相同的资源,size是一般一样的。

我们可以利用下面结构进行检索,记录结果哦,再进行去重。

java 复制代码
Map<Long,List<String>>

但是size一样的图片,md5一定一样么?其实对于旋转后的图片,其md5是不一样的,而pHash却能检测出其一样,因此,对于使用md5或者crc32或许并不是最优的,因此,去重务必要注意"漏网之鱼"。

当然,这里建议使用python脚本,而不是定义asm插件,因为毕竟这种优化的频次不是很高。

减少相似资源

这里主要是指相同dpi中相似度较高的资源,在之前一篇文章中,我们提到过相似距离的pHash算法,请参考《Android Bitmap亮度调节、灰度化、二值化、相似距离实现》,不过这个比较依赖android平台,如果要优化包体积建议使用python实现。

相比md5,pHash还能检测出旋转Nx90度(N 为1,2,3)的重复图片,其次还能有效检测出相似图片。

我们的优化点是:

  • 删除被旋转后的图片,然后通过代码旋转
  • 删除差异不太大的图片
  • 删除仅仅存在透明度差异的图片,使用代码实现透明度

总结

APK包体积优化其实很成熟了,往往都是常规手段为主、非常规手段为辅,在这些过程中,往往大家都都能想到这些手段。对于非常规手段,也有些技术含量不太高,但容易忽略的手段,本篇对此进行了补充:

  • 死代码入口删除
  • 重复图片剔除
  • 相似图片
  • 字体裁剪
  • 反-反Serializable混淆
  • 最小化引入so资源

本篇就到这里,希望本篇对你有所帮助。

相关推荐
Face1 分钟前
路由Vue-router 及 异步组件
前端·javascript·vue.js
Nano2 分钟前
Axios 进阶指南:掌握请求取消与进度监控的艺术
前端
工呈士2 分钟前
Context API 应用与局限性
前端·react.js·面试
ArcX2 分钟前
从 JS 到 Rust 的旅程
前端·javascript·rust
胡gh3 分钟前
深入理解React,了解React组件化,脱离”切图崽“,迈向高级前端开发师行列
前端·react.js
技术小丁4 分钟前
使用 HTML + JavaScript 实现自定义富文本编辑器开发实践(附完整代码)
前端·javascript·html
移动开发者1号19 分钟前
Android 大文件分块上传实战:突破表单数据限制的完整方案
android·java·kotlin
移动开发者1号27 分钟前
单线程模型中消息机制解析
android·kotlin
Alla T29 分钟前
【前端】缓存相关
前端·缓存