本文正在参加「金石计划」

作者:京东科技 冯建华

一、背景

随着事务不断迭代更新,App的巨细也在快速添加,2019年~2022年期间一度超过了117M,期间咱们也做了部分优化如图1红色部分所示,但在做优化的一起面临着新的增量代码,包体积一向继续上升**。**包体积直接或间接地影响着下载转化率、装置时间、磁盘空间等重要方针,所以投入精力开掘更深层次的装置包体积优化是十分必要的。根据谷歌商铺的内部数据,APK体积每削减10M,平均可添加~1.5%的下载转化率,如图2所示:

京东金融Android瘦身探索与实践

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

京东金融Android瘦身探索与实践

图2 谷歌商铺运用转化率添加起伏 / 10M [1]

因而2022年9月开端咱们针对金融APP进行了减肥专项整治,在不考虑增量的状况,无删减事务代码的状况下完成从117M减肥至74M,在本次装置包减肥过程中咱们遇到了不少坑,一起也积累了些经验,在此共享给咱们。

二、APK剖析

接下来咱们会简略剖析下 Apk内各组成部分,以及 Apk 作为 ZIP,其标准结构是什么样的,为包减肥的方针设定及使命拆解供给数据支撑。

2.1 APK内容剖析

京东金融Android瘦身探索与实践

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

京东金融Android瘦身探索与实践

图4 SDK巨细排序(包括版别号)

京东金融Android瘦身探索与实践

图5 SDK中包括的SO库列表及巨细

根据SDK剖析后结合事务,来判别哪些事务适合做插件化,从而直观的降低包体积。

2.3 ZIP结构剖析

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

输出的日志文件翻开如图6所示,每个文件的紧缩信息一行,包括文件名、原始巨细、紧缩后巨细等方针:

京东金融Android瘦身探索与实践

图6 APK内文件信息巨细

对以上日志信息进行逐行解析,根据解混杂后的文件名途径、文件类型进行归类统计,即可得出Apk的总览信息,包括各类型文件的数量、总巨细、单一文件巨细等方针,并树立文件巨细索引。

三、减肥实践

全体施行途径如图7所示,首要分为:

1.惯例技能计划,经过Gradle插件(代码无侵入、主动化)在编译时期完结APP减肥;

2.进阶技能计划,将部分事务线不同性的经过插件化或许SO动态下载的方法就行改造,事务改造的越多,收益越高;

3.事务优化计划,针对事务线的数据埋点,生成拜访UV进行排名,将UV较低的事务线反应架构委员会,评价是否能够进行下线或许经过进阶技能计划(2)进行改造,从而减小包体积。

京东金融Android瘦身探索与实践

图7 全体施行途径

3-1 惯例技能计划

3-1-1 图片处理

经过上述的APP的剖析,得出占用体积第一大的仍是图片,因而将APP一切含SDK内一切图片在编译打包过程中经过减肥使命主动完结图片优化处理,全体优化计划如图8所示:

京东金融Android瘦身探索与实践

图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文件:

京东金融Android瘦身探索与实践

图9 项目R文件生成暗示

全体计划如图10所示:

京东金融Android瘦身探索与实践

图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映射其中详细的途径和称号。

京东金融Android瘦身探索与实践

图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{未签名包} -o{7z解压目录}

第二步:首要经过7z指令对解压目录进行悉数紧缩:7za a -tzip -mx9 方针7z文件名{方针7z文件名} {7z解压目录}

第三步:获取存储类型文件,经过Android SDK中的aapt指令获取紧缩方法为Stored的文件列表:aapt l -v ${未签名包}

第四步:更新存储类型文件,经过7z指令将存储类型文件更新到第二步操作中生成的7zip装置包:7za a -tzip -mx0 方针7z文件名{方针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行)

•

京东金融Android瘦身探索与实践

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

•删去不必要的LOG日志输出

3-2 进阶技能计划

SO库动态下载和插件化技能,本质上都归于动态下载的一个领域,两个计划能够在事务中长期继续运用,在详细运用过程中怎样挑选,如图13所示:

京东金融Android瘦身探索与实践

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

京东金融Android瘦身探索与实践

图14 SO库删去和加载流程

3-2-2 插件化

什么是插件化:

插件化是将一个Apk根据事务功用拆分成不同的子Apk(也便是不同的插件),每个子Apk能够独立编译打包,最终发布上线的是集成后的Apk。在Apk运用时,每个插件是动态加载的,插件也能够进行热修复和热更新。

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

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

什么方式的事务适合插件化改造:

•事务相对独立,与宿主App解耦彻底

•改造成本低,收益相对较高

•占用体积较大

经过一些列评价,视频经营契合以上几点,改造后的作用如图15所示:

京东金融Android瘦身探索与实践

图15 视频经营厅插件化改造后作用

3-3 事务优化计划

随着事务越来越多,一些陈旧的事务UV越来越低,因而拟定了一套事务下线优化流程,如图16所示:

京东金融Android瘦身探索与实践

图16 事务优化计划流程

四、管控

减肥计划的施行很重要,后续的管控不反弹更重要,咱们一边做减肥管理,另一边探究常态化的管控机制,最终沉淀了一套管控标准和管控机制。管控的意图不是约束事务迭代或许新增代码,而是怎样做到在有限的代码中完成其功用,提高工程师日常编码中的减肥意识。

4.1 SDK接入标准

为防止SDK无序扩张,拟定了SDK准入标准,在保证功用的前提下严控SDK体积巨细,最大程度控制APP体积反弹。

4.2 管控流程

京东金融Android瘦身探索与实践

图17 管控流程

根据添加内容、删去内容、增大内容、减小内容、重复文件、代码管理等资源文件的改变状况结合管理管控标准等进行管理,打包构建完结会跟前史版别就行差量比照,获取改变的内容来评价是否具有优化空间,并给出优化方针,待优化后重新构建打包集成。

五、效果与后续规划

5.1 效果

经过以上办法,京东金融Android版别经过两个季度5个版别的迭代,从117M到现在的74M(图18),全体一向保持在可控的范围内。一起在接下来的版别迭代中,咱们会将APK减肥常态化,始终保持包体积在可控的范围内。

京东金融Android瘦身探索与实践

图18 金融APP减肥效果

5.2 后续规划

继续技能手段优化:

事务的不断堆积迭代,总会发生一些无用的资源,所以装置包减肥要定时清理这些无用文件和代码;

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

线上管控渠道搭建:

前期选用线下的管控管理,施行起来有点耗时,后续咱们会完善线上管控渠道的搭建,与整个App发布构建渠道进行融合,构成流水线的机制,做好管控。

小结:装置包减肥的探究还有很长的路走,本文也仅仅列举了一些常用的减肥计划,对于巨大的项目除了优化外,还有做好项目之间的管理,继续对APP进行体积优化,提高用户体验。

【参考资料】

[1] 包巨细与装置转化率
medium.com/googleplayd…

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

[3] R8r8.googlesource.com/r8

[4] ProGuard与R8比照
www.guardsquare.com/blog/progua…

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

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

[7] Pandora:根据去中心化技能的研制、测验阶段能效提高东西