关于京东云:京东金融Android瘦身探索与实践

55次阅读

共计 8455 个字符,预计需要花费 22 分钟才能阅读完成。

作者:京东科技 冯建华

一、背景

随着业务一直迭代更新,App 的大小也在疾速减少,2019 年~2022 年期间一度超过了 117M,期间咱们也做了局部优化如图 1 红色局部所示,但在做优化的同时面临着新的增量代码,包体积始终持续上升 包体积间接或间接地影响着下载转化率、安装时间、磁盘空间等重要指标,所以投入精力挖掘更深层次的安装包体积优化是十分必要的。依据谷歌商店的外部数据,APK 体积每缩小 10M,均匀可减少~1.5% 的下载转化率,如图 2 所示:

图 1 京东金融 Android 版本 2019-2022 体积变动过程 (红色局部是期间做的局部优化,然而很快就反弹回去了)

图 2 谷歌商店利用转化率减少幅度 / 10M [1]

因而 2022 年 9 月开始咱们针对金融 APP 进行了瘦身专项整治,在不思考增量的状况,无删减业务代码的状况下实现从 117M 瘦身至 74M,在本次安装包瘦身过程中咱们遇到了不少坑,同时也积攒了些教训,在此分享给大家。

二、APK 剖析

接下来咱们会简略剖析下 Apk 内各组成部分,以及 Apk 作为 ZIP,其规范构造是什么样的,为包瘦身的指标设定及工作拆解提供数据撑持。

2.1 APK 内容分析

图 3 APK 构造

•classes.dex APK 中可能蕴含一个或多个 classes.dex 文件,应用程序内的 Java/Kotlin 源码最终会以字节码的形式存在于 classes.dex 文件中。

•resources.arscaapt 工具在编译资源会将一些资源或者资源索引打包成 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 所示:

图 4 SDK 大小排序(蕴含版本号)

图 5 SDK 中蕴含的 SO 库列表及大小

依据 SDK 剖析后联合业务,来判断哪些业务适宜做插件化,进而直观的升高包体积。

2.3 ZIP 构造剖析

能够用 zipinfo 命令输入压缩包中每个文件的详细信息日志,用法:zipinfo -l –t –h test.apk > test.txt

输入的日志文件关上如图 6 所示,每个文件的压缩信息一行,包含文件名、原始大小、压缩后大小等指标:

图 6 APK 内文件信息大小

对以上日志信息进行逐行解析,依据解混同后的文件名门路、文件类型进行归类统计,即可得出 Apk 的总览信息,包含各类型文件的数量、总大小、繁多文件大小等指标,并建设文件大小索引。

三、瘦身实际

整体施行门路如图 7 所示,次要分为:

1. 惯例技术计划,通过 Gradle 插件(代码无侵入、自动化)在编译期间实现 APP 瘦身;

2. 进阶技术计划,将局部业务线差别性的通过插件化或者 SO 动静下载的形式就行革新,业务革新的越多,收益越高;

3. 业务优化计划,针对业务线的数据埋点,生成拜访 UV 进行排名,将 UV 较低的业务线反馈架构委员会,评估是否能够进行下线或者通过进阶技术计划(2)进行革新,进而减小包体积。

图 7 整体施行门路

3-1 惯例技术计划

3-1-1 图片解决

通过上述的 APP 的分析,得出占用体积第一大的还是图片,因而将 APP 所有含 SDK 内所有图片在编译打包过程中通过瘦身工作主动实现图片优化解决,整体优化计划如图 8 所示:

图 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);

这种变动叫做内联,内联是 java 的一种机制(如果一个常量被标记为 static final,在 java 编译的过程中会将常量内联到代码中,缩小一次变量的内存寻址)。非主工程中,R 类资源 ID 以援用的形式编译进 class 中,不会产生内联。

setContentView(R.layout.activity_main);

产生这种景象的起因是 AGP 打包工具导致的。具体细节,大家能够去查阅一下 android gradle plugin 在 R 文件上的处理过程。论断:R 类 id 内联后程序可运行,但并非所有的工程都会主动产生内联景象,咱们须要通过技术手段在适合的机会将 R 类 id 内联到程序中,内联实现后,因为不再依赖 R 类文件,则能够将 R 类文件删除,在利用失常运行的同时,达到包瘦身目标,如图 9 所示,在编译实现后会产生大量的 R 文件:

图 9 我的项目 R 文件生成示意

整体计划如图 10 所示:

图 10 R 文件优化流程

注意事项:在替换阶段肯定要退出二次查看,避免替换完,运行时呈现 ResourceNotFind 异样,如下所示:

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 进行资源混同

1. 资源加载过程剖析

开发过程中咱们通过 aapt 生成的 R.java 中的常量来应用资源,而在编译之后应用常量的中央都会被替换为常量的值,如下所示:

final View layout = inflater.inflate(2131165182, container, false);

也就是说咱们通过 Resource 应用一个 int 数值来查找应用资源。那么 Resource 是怎么通过 int 数值找到具体的资源呢?咱们解压 apk 能够看到外面有个 resources.arsc 文件,这个文件也是由 aapt 生成,文件中保留着资源 id 和资源 key 的映射关系,Resource 就是依照这个映射关系找到资源的。

2.resources.arsc:

图 11 是 resources.arsc 的里存储的映射关系,resources.arsc 能够了解为一个资源映射数据库,依据 ID 映射其中具体的门路和名称。

图 11 resources.arsc 解析

通过解压 APK 后,将资源文件名进行短链解决比方 res/layout/hello.xml 转换为 r /l/a.xml 后,而后更改 resources.arsc 对应的 value 值,达到整体的瘦身成果。

AndResGuard[5]是微信推出资源优化工具,它的根本思维相似于 ProGuard 中的混同,能够实现以上计划。

3-1-4 7zip 压缩

7zip 命令解释:

-t: 指定压缩类型,反对 7z, xz, split, zip, gzip, bzip2, tar, ….

-m: 指定压缩算法,默认是 Deflate

具体流程如下:

第一步:应用 7z 命令将未签名包解压到指定目录:7za x ${未签名包} -o${7z 解压目录}

第二步:首先通过 7z 命令对解压目录进行全副压缩:7za a -tzip -mx9 ${指标 7z 文件名} ${7z 解压目录}

第三步:获取存储类型文件,通过 Android SDK 中的 aapt 命令获取压缩形式为 Stored 的文件列表:aapt l -v ${未签名包}

第四步:更新存储类型文件,通过 7z 命令将存储类型文件更新到第二步操作中生成的 7zip 安装包:7za a -tzip -mx0 ${指标 7z 文件名} ${存储类型文件目录}

3-1-5 配置 CPU 架构

依据不同的 CPU 架构,构建不同的类型的安装包,目前支流设施都是 64 位机器,因而安卓市场上次要投放的是根据 arm64-v8a 编译构建的安装包

ndk {abiFilters arm64-v8a}

3-1-6 arsc 压缩

resources.arsc 的压缩体积收益很高,但对其进行压缩会影响启动速度和内存指标。起因是:零碎在加载 arsc 文件时,若 arsc 文件未压缩,可应用 mmap 进行内存映射;若 arsc 文件被压缩了,则须要将其解压缩后读取到 RAM 缓冲区,会减少内存应用,也会拖慢启动速度。

官网出于同样的思考,从 targetSdkVersion>=30 后不能用这种形式 开始强制要求 resources.arsc,否则会间接装置失败,因而本文不在开展论述。

3-1-7 国际化语言解决

京东金融 App 目前仅在国内市场经营,然而接入的大量 SDK 中退出了几十种语言一样,导致整个体积变大,通过评估能够通过配置 resConfigs 去除无用的语言资源。

defaultConfig {resConfigs "zh","en"}

3-1-8 shrinkResources

shrinkResources:编译过程中用来检测并删除无用资源文件,也就是没有援用的资源

minifyEnabled:用来开启删除无用代码,比方没有援用到的代码,所以如果须要晓得资源是否被援用就要配合 minifyEnabled 应用,只有两者都为 true 时才会起到真正的删除有效代码和无援用资源的目标。

其作用是将未被援用的资源文件替换为一个体积很小的格式文件(仍存在占位体积,同时保留了该资源条目,所以 resources.arsc 体积并不会缩小),可通过 res/raw/keep.xml 文件配置 shrinkMode 和白名单。

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 行)

•

图 12 枚举类型编译后的字节码比照

•删除不必要的 LOG 日志输入

3-2 进阶技术计划

SO 库动静下载和插件化技术,实质上都属于动静下载的一个领域,两个计划能够在业务中长期继续应用,在具体应用过程中如何抉择,如图 13 所示:

图 13 业务如何抉择进阶计划

3-2-1 SO 库动静加载

APP 中有局部业务不适宜做插件化革新,通过拆解发现其中的 SO 库占比很大,因而能够思考采纳动静下载的形式进行革新,进而实现减小体积。

SO 库加载的两种形式

第一种形式咱们间接把 SO 库下载并放到指定目录就能够

第二种形式是通过环境变量设置的目录中进行加载 SO 库,因而咱们须要追加指定的目录到环境变量中,就能够失常加载 SO 库

System.load("{平安门路}/libxxx.so") 
System.load("xxx") 

1、如何设置 APP 中 SO 库的环境变量地位(借鉴 Tinker):

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 所示:

图 14 SO 库删除和加载流程

3-2-2 插件化

什么是插件化:

插件化是将一个 Apk 依据业务性能拆分成不同的子 Apk(也就是不同的插件),每个子 Apk 能够独立编译打包,最终公布上线的是集成后的 Apk。在 Apk 应用时,每个插件是动静加载的,插件也能够进行热修复和热更新。

•宿主:主 App 能够用来加载插件也成为 Host

•插件:插件 App,被宿主加载的 App,能够跟一般的 App 一样的 Apk 文件

什么模式的业务适宜插件化革新:

•业务绝对独立,与宿主 App 解耦彻底

•革新成本低,收益绝对较高

•占用体积较大

通过一些列评估,视频营业合乎以上几点,革新后的成果如图 15 所示:

图 15 视频营业厅插件化革新后成果

3-3 业务优化计划

随着业务越来越多,一些古老的业务 UV 越来越低,因而制订了一套业务下线优化流程,如图 16 所示:

图 16 业务优化计划流程

四、管控

瘦身计划的施行很重要,后续的管控不反弹更重要,咱们一边做瘦身治理,另一边摸索常态化的管控机制,最终积淀了一套管控标准和管控机制。管控的目标不是限度业务迭代或者新增代码,而是怎么做到在无限的代码中实现其性能,晋升工程师日常编码中的瘦身意识。

4.1 SDK 接入标准

为避免 SDK 无序扩张,制订了 SDK 准入标准,在保障性能的前提下严控 SDK 体积大小,最大水平管制 APP 体积反弹。

4.2 管控流程

图 17 管控流程

依据减少内容、删除内容、增大内容、减小内容、反复文件、代码治理等资源文件的变动状况联合治理管控标准等进行治理,打包构建实现会跟历史版本就行差量比照,获取变动的内容来评估是否具备优化空间,并给出优化指标,待优化后从新构建打包集成。

五、成绩与后续布局

5.1 成绩

通过以上措施,京东金融 Android 版本通过两个季度 5 个版本的迭代,从 117M 到当初的 74M(图 18),整体始终维持在可控的范畴内。同时在接下来的版本迭代中,咱们会将 APK 瘦身常态化,始终维持包体积在可控的范畴内。

图 18 金融 APP 瘦身成绩

5.2 后续布局

继续技术手段优化:

业务的一直沉积迭代,总会产生一些无用的资源,所以安装包瘦身要定期清理这些无用文件和代码;

做好各个版本的监控,比照版本之间的差别,发现能够在不影响业务状况下,应用技术手段优化。

线上管控平台搭建:

后期采纳线下的管控治理,施行起来有点耗时,后续咱们会欠缺线上管控平台的搭建,与整个 App 公布构建平台进行交融,造成流水线的机制,做好管控。

小结:安装包瘦身的摸索还有很长的路走,本文也只是列举了一些罕用的瘦身计划,对于宏大的我的项目除了优化外,还有做好我的项目之间的治理,继续对 APP 进行体积优化,晋升用户体验。

【参考资料】

[1] 包大小与装置转化率
https://medium.com/googleplaydev/shrinking-apks-growing-insta…

[2] ProGuardhttps://www.guardsquare.com/proguard

[3] R8https://r8.googlesource.com/r8

[4] ProGuard 与 R8 比照
https://www.guardsquare.com/blog/proguard-and-r8

[5] AndResGuardhttps://github.com/shwenzhang/AndResGuard

[6] AGPhttps://developer.android.com/studio/releases/gradle-plugin

[7] Pandora:基于去中心化技术的研发、测试阶段能效晋升工具

正文完
 0