京东App安卓端瘦身实践

405次阅读  |  发布于1年以前

背景

京东App在2019年做了一轮瘦身,当时做了全方位的瘦身,但随着业务快速迭代和新技术框架引入以及管控力度不足,安装包体积出现了反弹。结合之前治而未管和业内一些同行的瘦身经验,本文介绍此次瘦身过程积累的一些实践经验以及管控方案。首先拆分安装包梳理数据,将安装包的成份数据做成线上化看板,根据数据找出安装包体积增长主要因素,结合京东App现状进行了一系列综合性的瘦身动作,也沉淀了一套新的管控规范及管控平台,最终实现了安装包体积下降30%以上。

安装包成份分析

瘦身未动,“数据”先行,包瘦身的第一步是要分析出App组成成份的大小数据,为包瘦身的目标设定及任务拆解提供数据支撑,达到可度量、线上化、统一数据标准的目的。如何进行安卓App的包成份分析?安卓开发者基本都会用到Android Studio自带的APK Analyzer工具,将Apk文件拖入Android Studio中即可查看,下图是京东App拖入Android Studio中显示的包大小数据,可以清晰的看出包体积里占比较大的主要是lib(存放动态库及安卓插件)、r(存放图片、xml等资源)、assets目录、dex和resources.arsc(资源映射表)几类文件。

APK Analyzer工具对于初步的、特定的包大小数据查看非常方便,但是对于需要进行的包瘦身项目来看,数据过于笼统,没有按模块维度进行划分,无法将包瘦身责任划分给对应的研发团队,不便于后续包瘦身项目的具体实施。为了度量各模块对包大小的影响,我们需要一套精细化的包大小分析方案。

首先来看一下京东App的工程结构,简化模型如下图所示。京东App整体采用了插件化的架构,开发迭代过程中逐渐按照功能、业务类型拆分成了很多模块,各模块有独立的项目工程、仓库,日常进行独立的开发维护,最终集成在Application工程中。功能型的模块基本上是以library依赖的形式引入到工程中的,比如常用的网络库、图片库、埋点库等;业务型的模块大多是以插件的形式存在,比如搜索、商详、购物车等;此外主工程中还存在少部分未拆分成模块的历史遗留逻辑,在日常迭代中占比非常少。

基于京东App模块化的现状,安装包的成份分析可以从三个方面进行:分析包成份的总览数据、分析插件数据和分析library数据。

01 Apk总览数据分析

安卓的包产物apk文件本质上是一个zip文件,可以用zipinfo命令输出压缩包中每个文件的详细信息日志,用法:

zipinfo -l --t --h  xxxx.apk > xxxx.txt

输出的日志文件打开如下图,每个文件的压缩信息一行,包括文件名、原始大小、压缩后大小等指标。

对以上日志信息进行逐行解析,根据解混淆后的文件名路径、文件类型进行归类统计,即可得出apk的总览信息,包括各类型文件的数量、总大小、单一文件大小等指标,并建立文件大小索引。

02 插件数据分析

插件产物的本质也是apk,目前京东App的预装插件有50+,App打包时各插件会以.so为后缀的形式放在lib目录中。对插件的分析可以直接基于包产物进行,解析时只需要将apk解压开来,按照文件名特征将插件从全部so文件中分离出来,逐一按照上述zipinfo相同方法进行解析即可。

03 library数据分析

各library模块大多是以aar/jar依赖的形式引入到项目工程中,目前京东App的全部项目依赖库有300+,包括直接依赖、间接依赖。对library数据的分析主要是在分析各依赖对包大小的影响和分析维护开发者两个方面。

01 分析依赖对包大小影响

安卓工程在构建时,gradle构建工具会自动将依赖库同步缓存到本地,与工程中的代码资源一起编译merge最终输出apk。通过编写gradle脚本任务在构建过程中可以取到aar/jar依赖文件,但是考虑到分析工具的独立性,尽量避免和打包流程耦合在一起,采用了自行解析依赖的方式来实现,整体流程如下:

a. 打包过程中执行gradle命令生成app运行时依赖树日志文件,用法:

./gradlew:app:dependencies--configuration xxxReleaseRuntimeClasspath > dep.txt

生成的日志文件部分如下:

b.解析日志文件生成N叉树模型,每一行日志生成一个依赖节点,包含groupId、artifact_name、version信息,日志中的->表示版本冲突取高版本,(*)表示重复依赖,在application和common_library中添加的依赖为直接依赖,对应根节点和common_library节点的子节点,其它节点为间接依赖。

c.将N叉树扁平化去重逐一解析依赖,下载aar/jar文件。解析过程中拼接url轮询仓库,为了加快解析速度,可以根据依赖的groupId区分依赖的来源,如果是jd内部的依赖,则优先请求内部的私服,否则优先访问常用的镜像仓库。此外可以加入LRU缓存机制,本地存在的依赖产物不再请求仓库,进一步加快解析速度。存在的一个潜在问题是快照版本的依赖会对应多个产物,解析时取的最新版本可能和app构建时用的不是同一个,通常发版分支上的依赖被规定不能用快照版本,所以这个问题也就间接规避了。

d. aar也是一种zip文件,同样可以用zipinfo命令来解析组成成份大小,建立内部jar、so、图片、xml、asset等文件的索引。

e.将aar内部的各文件根据名称与总览数据分析时建立的文件大小索引进行关联,即可知道aar内部的so、图片、xml、asset等文件最终在apk压缩包里的大小。由于代码文件jar最终经过编译、删减、混淆merge成了dex,无法精确的溯源,代码部分的大小计算采用了原始大小按比例折算的方式进行。折算比例是实验去掉比较独立的依赖后打包看包大小的差值,去除非代码部分的大小后,即可算出代码部分的折算比例,重复N次去除不同的依赖取平均值。

02 分析模块负责人

前述京东App有300+依赖,由于业务调整、人员变动等原因没有一个统一的library维护关系数据。对于直接依赖部分,初期通过git blame 命令输出app、common_library工程的build.gradle文件修改记录日志,正则匹配出依赖和维护者的对应关系;对于间接依赖部分,将前述N叉树的每一个间接依赖节点,向上寻找它的父节点直到直接依赖节点,则直接依赖的开发者也是此间接依赖引入的责任人,会存在多个直接依赖引用了同一个间接依赖的情形,即某个间接依赖库对应多个引入责任人。

通过上述分析方案,已经可以完整的分析出安装包的各成份大小及对应的负责人,为瘦身工作的实施提供了数据指引。分析方案全部是用python脚本的方式实现,已集成到公司内部的CD系统,数据看板效果如下。

京东App的开发者团队规模较大,在安装包成份数据线上化建设完成后,所有开发者统一了看数标准和渠道,每个App版本的增长与下降也可以归因到具体模块级别以及对应的开发者,同时这个数据分析看板也为瘦身的度量和管控提供了基础数据能力。根据数据分析我们发现App内资源文件、ReactNative、插件的数量和体积都比较大,所以瘦身重点围绕这几方面进行专项治理,同时基于之前瘦身出现过反弹的教训,分析发现在过往开发迭代中缺乏约束管控,技术选型和研发复用也暴露了部分问题,因此制定了精细化的管控规范并在研测流程中落地实施。

瘦身方案

借鉴业内的瘦身措施,结合京东App自身特性制定了瘦身方案,首先进行了常规化瘦身举措主要包括压缩资源、内置ReactNative转为下载、插件转下载、R8编译升级等,其次对App内的图片进行专项治理,构建了图片管理平台,最终使安卓安装包体积下降了近30%+,包体积瘦身趋势图如下图。

01 瘦身措施

京东mPaaS平台提供了安卓端插件转下载安装的能力,2022年初我们协同mPaaS团队、运维团队共同对其进行优化提升了其可用性和稳定性。将下载器的unknown host问题解决,加入了httpdns能力,支持切换域名能力等,运维团队通过拨测解决某些cdn节点异常导致的文件md5不一致问题,最终在真实用户网络环境下平均单次下载网络请求成功率在98%以上,由于在用户进入页面前存在多次触发下载的机会,进入页面而未加载成功场景的概率在十万分之4以下,此概率已低于App的崩溃率,体验上已完全可以保障,实践过程中也未出现用户体验异常反馈。通过业务模块的uv进行倒排序,最终甄选了10+插件进行了转下载安装。

在优化之初,京东大部分用React Native开发的业务模块均采用内置App的方式,峰值数量达到50+个。联合mPaaS团队进行优化,最初评估采用Brotli压缩本地内置包,经评估其解压耗时是gzip的2倍,内存使用是几十至几百倍,最终放弃这个方案。统一采用了将内置包转下载安装的方案,通过拆分基础包和业务包将JSBundle体积缩小,设置了预下载非强制更新、预下载强制更新以及增加某个RN业务打开则触发其他未下载业务下载等策略有效提升了单周新版更新覆盖率,提供h5降级、统一兜底降级等多重兜底体验,最终提升了RN相关能力的稳定性和可用性,保障了用户体验,目前内置的RN业务模块数已经降至个位数。

针对图片压缩,借助集成TinyPNG压缩工具对10K以上的图片进行压缩;受限于Android工程minSdkVersion=16,不能将所有的png图片都转成webp格式,只能将无透明通道的图片转成webp。后期对包大小更严格的要求,可以考虑将图片远程化。

借助瘦身脚本分析工具扫描安装包中方正纯色的小图片转成iconfont,包内扫描出800多张可转成iconfont,数量较多需要长期治理,虽然收益不明显但利于各个业务零散的小图标进行统一管理,同时有利于App整体视觉风格的统一。

通过自定义Gradle插件利用ASM对class文件进行字节码操作,将class文件中引用到R类资源id替换成对应的常量值并删除R文件,达到减少包体积的目的。由于京东App采用插件化的方式开发业务,每个业务插件都会引用公共资源,不能简单的将公共资源id添加到白名单中,所以结合京东App插件化方案(aura)的公共资源id固定的能力,实现插件化工程中的公共资源内联。目前开启R文件内联已完成灰度,后续会陆续上线,包体积整体收益5.5MB以上。

随Target31适配升级AGP到4.1版本,同步开启了R8编译,需要注意R8会忽略部分Proguard混淆规则,针对R8编译对混淆规则的变更点,编写了mapping文件检测脚本工具,对Proguard和R8编译生成的mapping混淆文件进行对比,检测网络解析、反射相关的类是否添加了正确的混淆规则,利用工具保障了升级R8编译后App的稳定性。R8编译对代码混淆优化效果要优于Proguard,开启R8后,包体积减少1.6MB,构建耗时也降低30%(3min)以上。

7Zip兼容Zip格式,支持Zip的Deflate算法,使用7zip极限压缩模式比Android打包默认的Zip压缩率高,可以将加固好的apk文件进行7zip解压缩后重新签名,包体积降低约2.2%(2MB)。为了验证其稳定性与可用性,计划对内部和外部市场都分别进行灰度,通过京东内部灰度渠道已灰度完成,对外部的各个渠道将包直接投递到渠道,如小米、华为等在灰度进行中。

在完成这些常规瘦身方案优化后,App安装包大小依旧较大。先前图片资源优化主要是通过图片压缩、使用一套分辨率xhdpi图片、图片转webp、资源名混淆等方式进行,这些优化方式都是将内置的图片进行瘦身优化,随着业务功能的不断迭代,瘦身收益将越来越少。京东App各个业务的开发方式是以插件化的形式进行,通过对这些业务插件包的分析,发现这些业务模块中内置的图片资源大小占各自插件包总大小的25%以上,针对内置图片资源过多问题,提出一种图片资源远程化的优化方案。

02 图片资源远程化

直接将图片资源转成在线加载可能会影响用户体验,将内置的图片资源从App应用内剥离,然后将图片上传到CDN获取到图片的网络链接,应用通过加载图片链接进行在线加载的方式来展示图片,在线加载图片需要经过下载图片的过程,考虑到用户所处的网络环境受多种因素的影响,常使用默认的兜底图在图片展示区域进行占位,或者使用全屏的加载中动画进行过渡等方式来优化用户体验。从优化用户体验角度出发,提出一种2层网络加载+3层降级措施的优化方案,可以提高网络图片的加载成功率,同时给用户展示图片的体验如同加载应用内置图片资源一样,整体方案如下:

该优化方案主要由图片管理CMS平台和获取图片信息的客户端组件组成,由业务研发在CMS配置各自模块的图片信息,客户端通过组件获取图片地址展示图片。

01 CMS图片管理

  1. CMS支持图片类型包括png、jgp、webp、点9图等,由业务模块梳理出内置的图片素材;
  2. CMS以用户为单位,创建业务模块,在模块内上传图片获取图片CDN链接,提供2x、3x两个分辨率设置,同时可以设置图片在客户端生效的版本范围以及生效平台Android或iOS;
  3. CMS提供将图片打包成zip压缩包的功能,在用户上传完图片后,会根据各个图片生效的客户端版本区间,生成不同版本区间以及2x、3x两种分辨率的zip包,会根据客户端请求接口中的分辨率参数下发不同分辨率的zip包链接;
  4. CMS提供zip包的预加载策略,分为App启动时预加载、App首页加载成功后x秒开始预加载、进入某页面时预加载等;
  5. 用户根据前面步骤配置完成后,将业务模块内的图片名和对应的CDN链接组成配置信息由CMS导出,然后将该配置信息文件内置到客户端,提供跟随安装包的兜底体验。

02 客户端图片预加载与缓存

  1. 客户端组件在App启动时请求CMS后台提供的查询接口,获取当前客户端版本对应的配置信息,与本地兜底配置信息进行比较,得到的差分信息进行本地持久化;
  2. 客户端组件同时解析后台接口下发的图片zip包信息,根据zip包的预加载策略下载对应的图片zip包,下载完成后解压到以模块名命名的本地文件夹中;
  3. 当展示图片时,客户端组件根据模块ID+图片ID优先查询本地zip包解压目录中该图片是否已存在,如果该图片已下载,则直接返回该图片的本地路径path,客户端直接使用该本地路径path展示图片;如果该图片还未下载完成,或不存在,则从内置的配置信息文件中获取该图片的CDN链接,同之前一样加载图片的网络链接;如果加载该网络链接还是失败,则展示默认的兜底图。

该优化方案中,2层网络加载的第一层网络加载是指业务模块的zip包,下载一次多版本复用;第二层网络加载是指图片zip包预加载未完成或失败时,通过安装包中内置的图片CDN链接兜底配置信息加载图片网络链接来展示图片,该兜底配置信息以图片名+图片CDN链接键值对的形式保存。3层降级措施的第一层降级是获取图片zip包预加载的本地缓存图片;第二层降级是当本地图片缓存失效时,获取内置的图片CDN链接加载;第三层降级则是默认的兜底图。瘦身业务模块开启图片zip预加载功能时,CMS支持业务模块设置zip包预加载策略,业务模块根据实际情况选择不同的加载策略,例如某些业务模块页面入口比较深,这些业务模块就可以选择用户在进入页面时触发zip包的预加载;某些业务模块进入页面入口比较浅就可以设置App启动后空闲时预加载,其他二级、三级的页面可以设置App启动后x秒(随机时间)才开始下载。通过设置这些预加载策略,将图片zip包分散在不同时间段下载,降低图片加载时的网络流量峰值。

上述方案需要业务研发积极配合,需要业务模块额外通过固定的模块ID+图片ID优先获取本地缓存的方式加载图片,使用过程较繁琐,缺乏灵活性,为方便开发者更容易使用,提供更多选择,降低接入成本,我们在CMS和客户端作了些改进,又提出了下面的优化方案:

CMS后台对图片zip打包做额外处理:取图片CDN链接中固定路径计算MD5值,并将该MD5值重命名图片名称,不保留图片类型后缀。例如链接:https://xxx/jfs/xxx/name.png,对链接中jfs开头和.png之间的路径字符串进行MD5值计算:MD5(/jfs/xxx/name) =MD5-Value ,以该MD5-Value值作为图片名称,并且移除图片类型后缀名(.png等), 其他图片同样按照该步骤进行重命名,最后打包成以模块名命名的图片压缩包。

提供一个工具类,以模块名和图片CDN链接作为入参,对图片CDN链接进行相同的处理,获取CDN链接中固定路径字符串的MD5值,然后根据模块名和MD5值查找该图片本地缓存是否已存在,即图片zip包预加载是否成功,如果图片已成功缓存则将该图片的缓存路径(file://协议)返回给图片加载框架,如果图片缓存不存在,则原样返回图片CDN链接(http://协议)。同样也可以结合图片加载框架实现,新增以业务模块名作为别名的图片加载属性,该别名属性就是业务模块图片缓存本地的目录,如果设置了该属性,图片加载框架就会优先判断该图片是否已缓存成功,若图片已缓存成功,则通过本地路径展示图片,若缓存未成功,则直接在线加载图片,该方案需要客户端图片加载框架同时支持file协议和http协议。该方案也可以应用于大促等场景的图片预加载。

通过埋点数据查看zip包预加载成功率,图片zip包预加载在配置首页启动后x秒下载,iOS端的zip包加载成功率在98.9%~99.1%波动,Android端的zip加载成功率在96.7%~98.1%波动,剩余1%~3%的用户则通过内置的兜底cdn链接在线加载,俩者结合展示图片的成功率接近100%;客户端从本地缓存加载图片的效率要高于从网络在线加载图片,省去了等待图片下载的过程,降低了默认兜底图的展示概率,在体验上接近于本地内置图片加载的效果。

管控

京东App在2019年专项瘦身后经过一段时间又反弹了50%,一方面因为业务快速迭代发展以及新引入了一些比较大的基础库,另一方面原因就是重点做了瘦身治理却没有深入做管控准入防止劣化。因此在新一轮瘦身优化过程中,我们一边做瘦身治理,一边在探索常态化的管控机制,最终沉淀了一套管控规范和管控平台。

管控的目的不是为了限制需求迭代与增加代码,目的是做好把控让合理的代码放进来,不合理的拒绝掉以及淘汰陈旧的代码,提升开发者们的瘦身意识。管控对产研全链路提出了新的要求,对于以往粗放型的需求开发迭代方式会有所约束,产品侧需要及时的配合研发侧将ABTest的废弃实验代码下线,研发侧需要做好技术规划和技术选型,尽可能的精简代码和复用代码,加强技术团队之间的协同,不放置过大或冗余的资源文件等。

管控的前提是基于App已经充分的组件化解耦,且App开发人员规模相对比较大,比如京东App当前安卓端是由业务插件和AAR依赖库组成。App的组成成份整体上是由一个配置表来表示,配置表内描述所有组件的配置版本信息,一个组件是否允许集成到App内的前提是这个组件体积增长大小符合我们制定的规范,然后组件的最新版本才可以集成到配置表内。我们管控的核心机制就是控制配置表的更新集成权限,以此来约束每个组件的开发者,通过App架构师委员会(京东内由架构师形成的虚拟组织)制定的公共管控规范来评判每个组件体积增长是否合规,合规则准入更新配置表,否则走异常申请流程。

01 管控流程与方案

管控流程是伴随着开发、测试、发布整个流程进行的,整体的流程如图所示。开发者开发完成后,由开发者在京东内部的组件构建与发布平台(mPaaS)发布形成新的组件版本,在构建完成后开发者在打包平台(Bamboo)基于临时配置表进行打包并提测,在此过程中会基于包计算出对应组件的体积变化数据,同时打包系统会以邮件形式将数据同步至开发者,尽可能前置的告知开发者是否存在不合规;测试通过后开发者申请集成组件版本信息至配置表,若合规则更新配置表且流程结束,若不合规开发者可以进行代码优化后在申请集成,或者申请异常审批流程至App架构师委员会。常规的业务迭代大部分不会出现违规,但也会存在部分异常的情况,异常的处理往往会产生较多的争议,研发的安装包体积管控需要顶住产品和业务的压力,异常处理的流程需要客观、透明、灵活,异常处理流程如图所示。

某个组件违规了发起申请,对于三方库、国家要求的隐私整改、安卓系统升级适配等情况,可以审核准许通过,其他一些特殊情况也可以通过,由App架构师委员会来酌情把控。对于一些正常情况下产生了组件体积增长超限违规,短期无法优化瘦回可以申请设置未来n个版本后瘦回,或者A组件协商B、C组件来协助分担瘦回,最终瘦身方案由App架构师委员会判断合理性,后续在n个版本内由平台自动计算瘦身的阶段性进展数据告知瘦身发起的相关人员;在n个版本后若未达成瘦身既定计划的目标则进行线下专项讨论,重新制定计划或做一些公示性处罚。

判断一个组件合规与否依赖于管控规范,管控规范由App架构师委员会共同讨论制定,最初我们设定的管控规范比较粗糙:每个组件当前版本相对于上个版本应小于等于n KB(起初n设置为100),随着瘦身进展到后期发现其存在比较多的弊端,对于体积大的模块其往往是多个开发者协作经常容易出现违规,对于体积小的模块往往参与人少迭代变化也不频繁,所以规范根本对其不起作用。基于这个情况我们改进了规范:将组件分为A1和A2两类,A1类组件>=n MB,A2类组件<n MB,对于A1类组件限定其年增幅应小于等于x%,对于A2类组件限定其单个版本增量应<=yKB。A1类模块一般迭代频率高且开发人员多,限定年增幅度每个版本的回旋调整余地更大;对于A2类模块迭代频率一般不高,设定单个版本增量更小也可以对应的约束其增长;对于一些三方库、公共库设置白名单处理;从全局来看管控规范基本兼具一定的公平性和合理性。结合App实际情况,通过合理的设定n、x及y值,可以将增幅控制在计划范围内。

总结

随着业务和用户需求的发展,App的体积会保持向上增长的趋势,业内各个体积较大的App都在进行着瘦身。我们借鉴了业内一些优秀的瘦身方案,结合京东App的实际场景进行了多项措施相结合的综合性瘦身方案,提出了既要治理又要管控的思路,沉淀了一套管控规范,并将其落地到研发测试流程系统内。随着新技术的迭代更新,新的瘦身方案与措施会不断涌现,我们会持续保持跟踪,欢迎读者们交流拍砖。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8