背景
前段时间我们项目开发工作完成后对项目进行一些优化工作,众所周知 , 性能优化是个持久长期的过程,纬度很多,包括启动、卡顿、内存、网络、电量等等,个人认为包体积优化投入产出比是最高的,流程简单,时间花销很少,但能带来体积大幅度缩减直观收益,而且具有通用型,能给每个项目优化提供参考。上周应用市场对接支付宝小程序和智能场景卡片,需要车机预装引擎包,邢捕头透露目前A1上提供的DC系统分区空间已经不足了,所以包体积优化应当引起重视。
优化前:11.9M
APK组成
文件描述
libjar,aar,so文件,不同的cpu架构
res编译后的资源文件,drawable、layout等
assets应用程序的资源、字体、音频文件等
classes(n).dexdx编译后的java文件
META-INF签名信息相关
resources.arsc二进制资源的映射信息,根据R文件找到对应资源
kotlin编译后的kotlin文件
AndroidManifest.xml清单文件
APK构建流程
优化思路
APK本质是一个压缩文件,是打包后的产物,那可以作为切入点的阶段就是打包前、打包中。
打包前,即减少打包的文件,比如无用的资源、代码;
打包中,对打包中的产物进行混淆压缩,比如资源文件、So文件;
基本操作
1、Lint检测无用资源文件
Analyze > Run Inspection by Name > Unused resources
//由于项目没有多余资源,提示如下:(如果有的话,确定无用删除即可)
注意: 因为lint是本地静态扫描,所以动态引用的资源文件并不会识别出来,也会出现在检测列表里。
2、Lint检测无用代码
因为本人电脑安装了alibaba java coding guideline,检测结果包含Lint检测代码。
注意: 因为lint是本地静态扫描,所以反射、动态引用的class并不会识别出来,也会出现在检测列表里。
lint优化后大小:11.9M →11.8M
3、图片压缩
推荐使用tinypng在线压缩。
原理: TinyPNG的压缩是通过减少颜色数量,将24-bit的图像文件转换成8-bit,来大幅度缩小图片体积。(有损,肉眼不可见)
4、TinyPngPlugin
手动压缩毕竟不高效,可以使用TinyPngPlugin一键压缩。plugins搜索TinyPng安装即可。(新版AS安装完plugin已经不需要重启了)
压缩结果:
3张图片,可以看到效果还是非常可观的。如果图片多,效果更加明显。
5、图片转WebP
那这3张图还能继续优化吗?可以,WebP格式的体积更小,而且AS也提供了一键转换支持,但经过TinyPNG压缩后的体积转为WebP格式不一定每次都会更小。
以load_empty.png为例:
load_empty.png优化后原始大小9KB经过TinyPng压缩再经WebP格式转换缩小为3.82K
可以看到,较原始大小减少了68%
注:尝试对task-lib的png图片进行压缩,提示9-Patch图无法converted webP
优化后大小为:11.9M →11.8M→ 8.8M
6、R8编译优化
R8采用D8 + ProGuard的形式构建,将Proguard(混淆、压缩、优化)和D8(Java字节码转化成dex代码,编译优化体积优化)工具进行整合,目的是加速构建时间和减少输出apk的大小。
-D8编译过程-
-R8编译过程-
desugaring脱糖、shrinking压缩、obfuscating混淆、optimizing优化、 dexing编译一步到位
开启R8的好处:
代码缩减(摇树优化):使用静态代码分析来查找和删除无法访问的代码和未实例化的类型,对规避65535 引用限制非常有用;
资源缩减:移除不使用的资源,包括应用库依赖项中不使用的资源。
混淆代码:缩短类和成员的名称,从而减小 DEX 文件的大小
优化代码:检查并重写代码,选择性内联,移除未使用的参数和类合并来优化代码大小
减少调试信息 : 规范化调试信息并压缩行号信息。
//AS版本升级到3.4以上
minifyEnabled true //启用R8代码缩减功能。
shrinkResource true //启用R8资源缩减功能
注:R8包含混淆,如果不想混淆的代码和资源需要配合自定义混淆规则使用
优化后体积缩减:11.9M →11.8M→ 8.8M→6.7M
7、zipalign启动优化
zipalign 是 Android 提供的一个整理优化 apk 文件的工具,原理大概是格式化Zip文件夹的二进制文件的序列进行优化重排,达到提升系统解析速度。提高系统和应用的运行效率,更快地读写 apk 中的资源,降低内存的使用。所以对于要发布的 APP,在发布之前一般要使用 zipalign 进行优化。
zipAlignEnabled true //启动优化
8、so文件缩减
android studio默认会打包成4种架构,ZEEKR车机端不管是A1还是BX1E都是arm64架构的,所以保留arm64-v8a一种即可,其他直接删除。
由于之前应用市场已经优化过此问题,如果还原发现,lib文件从166KB增大为667KB
只保留一种架构需要设置:
9、移除未使用的备用资源
很多出海的应用会做国际化,但也适配不了这么多的语言。除了自己app的之外,还有一些官方的、三方的,可以统一配置支持的语言。
应用市场没有做海外语音适配 & 从一开始就只有xhdpi一种分辨率
defaultConfig {
//三种语言
resConfigs("en","zh","zh-rCN")
}
资源文件同理,如果不进行唯一标识,默认会打包生成mdpi、xhdpi、xxhdpi3种
defaultConfig { "zh",resConfigs("xhdpi") }
10、小结
针对上面的基本操作做个小结,看看目前效果如何。
11.9M →11.8M→ 8.8M→6.7M 包体积减少43%,事实上应用市场自身特性也决定了对网络依赖性很强,本地资源很少,很多车机项目包体积减少效果会更大。
高阶操作
1、功能重复的三方库整合
1、比如glide和picasso,都是图片库,保留其一即可。
2、同一个三方库的不同版本。
File-Project Structure 查询依赖关系
3、其他一些特定业务操作。
2、so动态加载
├── armeabi-v7a/
│ ├── libmmkv.so
├── arm64-v8a/
│ ├── libmmkv.so
├── x86/
│ ├── libmmkv.so
└── x86_64/
├── libmmkv.so
很多三方库依赖的so文件占比比较大,而且由于适配不同架构包含多种一样的so文件,可以考虑so文件做按需远程下载&动态下发。也就是插件化的思想。
可以看出正常安装APK时:
PackageManagerService根据当前设备架构拷贝对应So文件到/data/app_libs/
启动APP,framework创建应用的ClassLoader实例,并将所有so文件所在目录注入 ClassLoader 字段中
调用 System#loadLibrary("xxx.so") , Framework从当前上下文classLoader的目录数组里查找so
native通过dlopen函数加载so
调用JNI方法
我们要做的就是把自定义的native库path插入nativeLibraryDirectories最前面,即使安装包libs目录里面有同名的so,也优先加载指定路径的外部so。
/** 例如Tinker源码中sdk25版本动态加载so的方案
*/
private static void install(ClassLoader classLoader, File folder) throws Throwable {
// step1: 反射获取 pathList
final Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
final Object dexPathList = pathListField.get(classLoader);
// step2: 拿到nativeLibraryDirectories
final Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
// step3: 如果 origLibDirs 内已有我们的路径了,移除掉
...
// step4: 将我们的路径放在集合首位,这样会优先加载,实现so的替换
origLibDirs.add(0, folder);
// step5: 获取 pathList 系统路径集合
final Field systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
// step6: addAll 两者所有路径
final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1);
newLibDirs.addAll(origLibDirs);
newLibDirs.addAll(origSystemLibDirs);
// step7: 生成一个新的 natieLibraryPathElements 集合
final Method makeElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class);
final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs);
// step8: 覆盖掉原有的路径集合
final Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.set(dexPathList, elements);
}
收益很大的同时,风险也很大,有很多case需要考虑到,比如下载时机、网络环境、线程进程,尤其是so库文件之间有依赖关系时有很多问题需解决。
3、插件化
宿主APP按需动态下发多个子apk。
任何软件工程遇到的问题都可以通过增加一个中间层来解决
其他方案
1、原生改用H5或小程序等方案
有些功能可能原生做就显得太重,比如各种促销活动,需要加载各种大图,原生既重又不够动态化,这个时候H5是一种很好的替代方案。但是如果你原本就不支持H5或者小程序的话,接入这种能力可能反而会加大包体积,做好对比。
2、砍功能
有些功能可能想的很美好,但上线之后收益并不大,是否需要重新思考价值点,最好找到数据依托,再跟产品打架。
3、修改三方库的源码,不需要的代码剔除
比如引入了一个功能很齐全的三方库utils,但实际只用到几个,对源码进行抽取也能减少包体积,同时还能减少网络下载的编译时间。弊端就是升级成本较大。
4、图片网络化
即把图片上传到服务器,通过动态下载的方式减少包体积,弊端就是首次加载的时候依赖网络环境,对加载速度、流量需要做一个平衡。图片可以预加载,但是流量消耗是无法避免了,如果比较在意流量指标,需要权衡了。
包体积监控
包体积监控应该作为发布流程的一个环节,最好是做到流程化,否则很难持续,没几个版本包体积又涨上来了。大致思想:当前版本与上一个版本的包大小做对比,尽可能不做依赖、大小超过2M需要审批并给出原因和后续优化方案等等。
Matrix Android ApkChecker · Tencent/matrix Wiki · GitHub(Matrix是微信终端自研和正在使用的一套APM(Application Performance Management)系统。 Matrix-ApkChecker 作为Matrix系统的一部分,是针对android安装包的分析检测工具,根据一系列设定好的规则检测apk是否存在特定的问题,并输出较为详细的检测结果报告,用于分析排查问题以及版本追踪。Matrix-ApkChecker以一个jar包的形式提供使用,通过命令行执行 java -jar ApkChecker.jar 即可运行。)
网友评论