京东金融Android瘦身探索与实践
Tech导读
随着业务不断迭代更新,京东金融App(Android版本)的体积也在快速增加,2019年~2022年期间甚至一度超过了117M。2022年9月开始针对金融App进行了瘦身专项整治,最终实现从117M瘦身至74M。本文阐述了整个安装包瘦身过程中遇到的问题以及积累的经验,并详细介绍了具体的解决路径。
导读
随着业务不断迭代更新,京东金融App(Android版本)的体积也在快速增加,2019年~2022年期间甚至一度超过了117M。2022年9月开始针对金融App进行了瘦身专项整治,最终实现从117M瘦身至74M。本文阐述了整个安装包瘦身过程中遇到的问题以及积累的经验,并详细介绍了具体的解决路径。01 背景
在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!
随着业务不断迭代更新,京东金融App(Android版本)的体积也在快速增加,2019年~2022年期间一度超过了117M,期间也做了部分优化如图1红色部分所示,但在做优化的同时面临着新的增量代码,包体积一直持续上升。包体积直接或间接地影响着下载转化率、安装时间、磁盘空间等重要指标,所以投入精力发掘更深层次的安装包体积优化是十分必要的。根据谷歌商店的内部数据,APK体积每减少10M,平均可增加~1.5%的下载转化率,如图2所示:
02
APK分析
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
接下来会简单分析下 Apk 内各组成部分,以及 Apk 作为 ZIP,其标准结构是什么样的,为包瘦身的目标设定及任务拆解提供数据支撑。
2.1 APK内容分析
classes.dex APK 中可能包含一个或多个 classes.dex 文件,应用程序内的 Java/Kotlin 源码最终会以字节码的方式存在于 classes.dex 文件中。 resources.arsc aapt工具在编译资源会将一些资源或者资源索引打包成resources.arsc。 res/ 源码工程中 res 目录下除了 values 外的资源文件,这些文件路径同时会记录在 resources.arsc 中。 lib/ nativeLibraries,即源码工程 jni 目录下的 so 文件,二级目录为 NDK支持的 ABI。 assets/ 与 res/ 资源目录不同,assets/ 下的资源文件不会在 resources.arsc 中生成查询条目,且 assets/ 下的资源目录可完全自定义,在程序中通过 AssetManager 对象来获取。 META-INF/ 该文件夹下主要包含 CERT.SF 和 CERT.RSA 签名文件, 以及 MANIFEST.MF 清单文件。 AndroidManifest.xml 应用清单文件,用于描述应用基本信息,主要包括应用包名、应用id、应用组件、所需权限、设备兼容性等。
2.2 SDK大小分析
通过自研的能效提升平台Pandora[7],可以直观地看到SDK的大小,如图4所示:
2.3 ZIP结构分析
输出的日志文件打开如图6所示,每个文件的压缩信息一行,包括文件名、原始大小、压缩后大小等指标:
对以上日志信息进行逐行解析,根据解混淆后的文件名路径、文件类型进行归类统计,即可得出Apk的总览信息,包括各类型文件的数量、总大小、单一文件大小等指标,并建立文件大小索引。
03
瘦身实践
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
整体实施路径如图7所示,主要分为:
常规技术方案,通过Gradle插件(代码无侵入、自动化)在编译时期完成APP瘦身; 进阶技术方案,将部分业务线差别性的通过插件化或者SO动态下载的方式就行改造,业务改造的越多,收益越高; 业务优化方案,针对业务线的数据埋点,生成访问UV进行排名,将UV较低的业务线反馈架构委员会,评估是否可以进行下线或者通过进阶技术方案(2)进行改造,进而减小包体积。
图7.整体实施路径
3.1 常规技术方案
3-1-1 图片处理
经过上述的APP的剖析,得出占用体积第一大的还是图片,因此将APP所有含SDK内所有图片在编译打包过程中通过瘦身任务自动完成图片优化处理,整体优化方案如图8所示:1.多 DPI 优化
Android 为了适配各种不同分辨率或者模式的设备,为开发者设计了同一资源多个配置的资源路径,app 通过 resource 获取图片资源时,自动根据设备配置加载适配的资源,但这些配置伴随着的问题就是高分辨率的设备包含低分辨率的无用图片或者低分辨率的设备包含高分辨率的无用图片。
一般情况下,针对国内应用市场,App 为了减少包大小,会选用市场占有率最高的一套 dpi(google 推荐 xxhdpi)兼容所有设备。而针对海外应用市场的 APP,大多会通过 AppBundle 打包上传至 Google Play,能够享受动态分发 dpi 这一功能,不同分辨率手机可以下载不同 dpi 的图片资源,因此需要提供多套 dpi 来满足所有设备。在项目中,图片有的只有一套 dpi,有的有多套 dpi,针对上述两种场景,分别在打包时合并资源、复制资源,减少了包大小。
2.转换为webp格式:
WebP是谷歌提供的一种支持有损压缩和无损压缩的图片文件格式,而且可以提供比JPEG或PNG更好的压缩。在Android 4.0(API level 14)中支持有损的WebP图像,在Android 4.3(API level 18)和更高版本中支持无损和透明的WebP图像
因此:采用插件在编译时期仅保留针对图片通过Google提供的shell程序进行格式转换,转换成功删除旧的图片,进而达到APK瘦身的效果
3.png压缩
Pngquant是一个好用的png压缩工具,可以进行有损图片压缩的命令行工具,因此在1和2处理结束后,可以使用Pngquant进行二次压缩,达到更优的图片瘦身。
3-1-2 R文件内联优化
DEX里是Java/Kotlin 源码编译后的字节码文件,对DEX的优化其实就是怎么优化字节码文件,DEX中包含大量的资源索引R文件,这里主要讲下如何通过资源ID内联后进行R文件删除,达到APK瘦身的目的:
R文件瘦身的可行性分析
日常开发阶段,在主工程中通过R.xx.xx的方式引用资源,经过编译后R类引用对应的常量会被编译进class中。
setContentView(2131427356);
setContentView(R.layout.activity_main);
try {
int value = RManager.checkInt(type, name);
}catch (Exception e){
String errorMsg = "resource is not found(I),className="+className+",fieldName="+owner+"."+name;
throw new ResourceNotFoundException(errorMsg);
}
try {
int[] value = RManager.checkIntArray(type, name);
}catch (Exception e){
String errorMsg = "resource is not found(I[]),className="+className+",fieldName="+owner+"."+name;
throw new ResourceNotFoundException(errorMsg);
}
3-1-3 AndResGuard进行资源混淆
final View layout = inflater.inflate(2131165182, container, false);
也就是说通过Resource使用一个int数值来查找使用资源。那么Resource是怎么通过int数值找到具体的资源呢?解压apk可以看到里面有个resources.arsc文件,这个文件也是由aapt生成,文件中保存着资源id和资源key的映射关系,Resource就是按照这个映射关系找到资源的。
图11是resources.arsc的里存储的映射关系,resources.arsc可以理解为一个资源映射数据库,根据ID映射其中具体的路径和名称。
3-1-4 7zip压缩
3-1-5 配置CPU架构
ndk {
abiFilters arm64-v8a
}
3-1-6 arsc 压缩
3-1-7 国际化语言处理
defaultConfig {
resConfigs "zh","en"
}
3-1-8 shrinkResources
buildTypes {
release {
// 不显示Log
buildConfigField "boolean", "LOG_DEBUG", "false"
//混淆
minifyEnabled true
// 移除无用的resource文件
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig sign.release
}
}
3-1-9 编码约束
尽量少用枚举类型,因为枚举在编译成字节码后,会增加大量体积,如图12所示(22行代码编译后字节码是86行)
删除不必要的LOG日志输出
3.2 进阶技术方案
SO库动态下载和插件化技术,本质上都属于动态下载的一个范畴,两个方案可以在业务中长期持续使用,在具体使用过程中如何选择,如图13所示:
3-2-1 SO库动态加载
System.load("{安全路径}/libxxx.so")
System.load("xxx")
final Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
final Object dexPathList = pathListField.get(classLoader);
final Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
if (origLibDirs == null) {
origLibDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = origLibDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir)) {
libDirIt.remove();
break;
}
}
origLibDirs.add(0, folder);
final Field systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
if (origSystemLibDirs == null) {
origSystemLibDirs = new ArrayList<>(2);
}
final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1);
newLibDirs.addAll(origLibDirs);
newLibDirs.addAll(origSystemLibDirs);
final Method makeElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class);
final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs);
final Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.set(dexPathList, elements);
2、如何删除指定SO库和整个加载流程,如图14所示:
3-2-2 插件化
宿主:主App可以用来加载插件也成为Host 插件:插件App,被宿主加载的App,可以跟普通的App一样的Apk文件
业务相对独立,与宿主App解耦彻底 改造成本低,收益相对较高 占用体积较大
经过一些列评估,视频营业符合以上几点,改造后的效果如图15所示:
图15.视频营业厅插件化改造后效果
3.3 业务优化方案
04 管控
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目
瘦身方案的实施很重要,后续的管控不反弹更重要,一边做瘦身治理,另一边探索常态化的管控机制,最终沉淀了一套管控规范和管控机制。管控的目的不是限制业务迭代或者新增代码,而是怎么做到在有限的代码中实现其功能,提升工程师日常编码中的瘦身意识。
4.1 SDK接入规范
4.2 管控流程
根据增加内容、删除内容、增大内容、减小内容、重复文件、代码治理等资源文件的变动情况结合治理管控规范等进行治理,打包构建完成会跟历史版本就行差量对比,获取变化的内容来评估是否具有优化空间,并给出优化目标,待优化后重新构建打包集成。
05 成果与后续规划
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
5.1 成果
5.2 后续规划
业务的不断堆积迭代,总会产生一些无用的资源,所以安装包瘦身要定期清理这些无用文件和代码;
【参考资料】
[1] 包大小与安装转化率 https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2
[2] ProGuard https://www.guardsquare.com/proguard
[3] R8 https://r8.googlesource.com/r8
[4] ProGuard与R8对比 https://www.guardsquare.com/blog/proguard-and-r8
[5] AndResGuard https://github.com/shwenzhang/AndResGuard
[6] AGP https://developer.android.com/studio/releases/gradle-plugin
[7] Pandora:基于去中心化技术的研发、测试阶段能效提升工具
jvm中类和对象定义存储基础知识
关于聚合根、领域事件的那点事——深入浅出理解DDD
Mybatis-SQL分析组件(2.0)
求分享
求点赞
求在看