稳定性、性能、包大小,在挪动端根底用户体验畛域“三分天下”,是 app 承载业务取得稳固、高效、低成本、快速增长的重要基石。其中,包大小对下载转化率、拉新拉活老本等方面的影响至关重要,这在业界曾经成为共识,近年来头部 app 针对下沉市场的极小包策略,更是将包大小的价值晋升到了极致。
优酷在 Android 包大小畛域,有长达 5 年的继续投入、实际和积攒,尤其是在近 2 年逐渐进入低成本可继续治理的衰弱状态。现将这些思考、方案设计、技术建设、治理实际对立汇总整顿成文并分享进去,心愿可能帮忙更多同学在所负责或参加的 app 中,更好的进行包大小治理。
本文聚焦于整体治理思路,以治理实际为依靠,讲述瘦身技术、治理模式、治理策略,以及背地的思考与取舍。
五年治理回顾
作为开篇,先给出优酷近 5 年包大小变动状况:
以 2020 年 9 月为分水岭,从治理模式角度,能够将前后划分为两个“格调迥异”的阶段:专项治理、常态治理。包瘦身治理也属于一种软件工程,接下来围绕“术”、“道”、“人”三个维度,开展回顾和总结:
1.1 专项治理 3 年:三次两反弹
自 2017 年初至 2020 年 9 月这 3 年工夫,共经验三次专项治理以及两次反弹。
2017.05 – 2018.03, 第一次专项治理。在瘦身成果上,从最高点 73MB 升高到 51MB,瘦身比例约 30%,这次瘦身专项的最大价值,是积攒了贵重的实践经验:
- 技术手段。因为过后简直没有积攒,采纳的技术手段绝对惯例且具备单点性,次要包含:剖析并下线无用业务 / 功能模块、近程化边缘业务、图片压缩 & 矢量化等;
- 治理策略。不足整体指标掌控和拆解,对于头部问题进行单点革新;
- 组织模式。比拟涣散,波及范畴窄,参加人数少。
2018.04 – 2018.09, 第一次反弹期。期间应用“模块级”包大小卡口作为管控伎俩,因为短少相干剖析技术撑持,申请方和审批方对存量 & 增量状况都短少清晰统一认知,导致管控逐步流于形式。与此同时,包瘦身治理优先级升高,后面负责治理的架构同学撤出,尽管架构团队仍然负责跟进相干事项,但简直没有被动投入治理,包大小靠近天然状态下的“横蛮成长”。
2018.10 – 2019.02, 第二次专项治理。在瘦身成果上,从最高点 80MB 升高至 40MB,瘦身比例 50%,除了实践经验的继续积攒,在技术手段上呈现出被动摸索、初步积淀几个特色:
- 技术手段。近程化大规模应用:近程 Bundle、近程 so,简直所有能近程局部都进行了相干革新;业界瘦身伎俩尝试:代码系列瘦身(混同精简、同功能模块对立、无用功能模块下线)、资源系列瘦身(裁减、混同)、整包瘦身(apk 的 7z 压缩、R 文件合并裁减),对其中约一半技术手段进行了利用;单点剖析技术摸索:次要集中在资源方面,包含无用、反复、类似、大尺寸、无透明度 png、图片矢量化、多维度,利用剖析后果作为瘦身点革新和散发的输出;
- 治理策略。核心式工作拆解、并行式承接落地;
- 组织模式。集中工夫 & 人力,外围业务根本都参加了进来。
2019.03 – 2020.03, 第二次反弹期。在这一时期的管控上,基于虚构性能组概念(多个模块聚合)建设了包大小卡口能力,然而未能与研发流程无效联合,无奈做到及时感知以及要害的超限拦挡阻断,同时申请方和审批方短少对“一个性能 / 业务,占用多大正当?有多少可瘦身点,别离具体是什么,瘦身空间是多少?”这些关键问题的共识性认知,导致沟通、推动、瘦身革新等老本居高不下,半年之后的管控开始举步维艰。
从增长曲线来看,显著能够分为两段:2019.09 之前的 6 个月工夫,属于迟缓增长(尽管两头有一个增长波峰,但很快就失去管制回落),一方面得益于围绕卡口的继续管控,另一方面也因为这段时间没有大型新框架、新能力、新业务接入;2019.10 之后的 6 个月,因为 Flutter 等新框架的集中爆发式引入,导致包大小呈现“疯狂飙升”,在 2020.03 甚至达到历史最高的 126MB 水位。
2020.04 – 2020.09, 第三次专项治理。在瘦身成果上,最终将包大小降至 100MB 以下。尽管这次仍然是专项模式,但与第二次专项治理齐全不同的是:参加团队更宽泛,不仅仅是外围业务团队,而是所有客户端团队;牵头同学和参加同学之间的合作形式,由“中心化调配”与“被动实现”(包工队模式),转变为辅助与输入(PVP 战队模式),即牵头同学提供更全面、更具体、更具备指导性的剖析能力、工具,以及用于升高革新老本和上线危险的各类辅助工具,各团队同学在为本人瘦身指标负责前提下,具备极高的过程自由度,能够集中火力进行瘦身 Action 的剖析制订和执行。另外,这一阶段在技术上的关注点,更多的聚焦到剖析 & 辅助技术,而不是那些可能间接减小包大小的技术:
- 技术手段。剖析技术成型:包大小剖析工具 franky 即诞生于此期间,初步实现“对 apk 大小实在奉献”的剖析能力,以及联合模块图谱数据将 apk 大小拆分到各研发团队,多个可瘦身项检测也逐渐积淀到剖析工具中;源头深度瘦身:无论是比拟惯例的无用和冗余性业务、性能、模块、甚至是办法级代码,还是 franky 蕴含的若干可瘦身项,都逐渐在源头代码层面,以更细粒度更治标的形式开展;
- 治理策略。中心化拆解,分布式治理。借助剖析工具,将瘦身指标逐个拆解到不同团队和业务,各自依据理论状况合理安排人员、计划、进度;
- 组织模式。专项模式,简直笼罩客户端所有团队和业务。
1.2 常态治理 2 年:稳重继续降
2020 年 9 月至今(精确的说是 1 年半多一点),进入常态治理阶段,包大小从期初 100MB,逐渐升高到 2022 年 3 月底的 64.9MB(截止本文实现的 5 月份为 64.4MB)。
在这个阶段,包大小卡口能力实现了一次要害进化:与研发流程实现无缝联合,对超限状况实现及时感知,以及拦挡阻断,这让整体管控老本失去极大升高。同时,对剖析技术、瘦身技术的迭代、摸索和利用,始终没有停下脚步:dex 排布优化、7z 压缩、D8、R8 等整体瘦身技术陆续上线,so 无用导出符号等可瘦身项继续退出剖析工具 franky,相干技术也开始失去阿里外部更多 app 应用,这进一步促成了性能疾速倒退和丰盛。另一方面,治理策略也在逐步完善,客户端各研发团队围绕本人的包大小阈值,把包大小晋升为与稳定性、性能一样的日常研发迭代根底考量指标。
治理模式
后面通过术、道、人三个维度对历史进行了回顾,通过比照不难发现它们有着截然不同的特色,据此能够将包瘦身治理分为两种模式:专项式、常态化,前者以短时间疾速瘦身为指标,后者以长时间可继续维持为指标(甚至逐渐升高)。看到这里,或者会提出一个疑难:治理模式和“术、道、人”三维度有什么关系?如果肯定要进行辨别,我认为既能够看作不同的思考视角,也能够认为前者是后者的更高层次形象:“术”的能力所达到的程度,“道”的抉择所遵循的准则、“人”的排布所提供的保障,独特决定了以后处于什么样的治理模式;反过来也实用,即治理模式对“术、道、人”的内容和边界,都有明确的要求。
2.1 专项式 vs 常态化
专项式治理, 个别是在包大小持续上升至某个值后,成立专门我的项目集中工夫治理。个别会有多团队多人员参加,同时会有明确的我的项目负责人,来制订严格且固定的里程碑。此时的 apk 包因为通过一段时间积攒,会存在较多以无用和冗余性能为代表的可瘦身项,绝对容易辨认和解决,因而个别瘦身见效快,当然专项完结后如果不足无效的可继续管控,包大小反弹简直是必然的。
专项式治理的“精力内核”是指标优先,这当然没有任何问题,但在这个过程中,往往很容易漠视瘦身革新所带来的其它负面影响,例如不适当的近程化革新会带来用户体验受损、apk 构建过程中采纳大量“瘦身黑科技”导致打包耗时明显增加等。这外面的取舍和均衡之道,没有标准答案,只有综合判断“此时此地此景”后作出的抉择。
常态化治理, 是指在长期的版本迭代过程中始终可能管制好增量,并在维持住以后包大小水位前提下实现“稳中有降”。个别在常态化治理阶段,头部问题曾经根本不存在,须要在业务性能和代码源头进行更全面、精密、深刻的剖析和思考,从而在版本迭代过程中逐渐“消化掉”能够瘦身的中央。在治理所需人力投入上,具备较低的整体管控投入,并造成团队、业务、性能的开发者自治场面。在治理节奏上,整体包瘦身指标调整周期较长,同时不再进行细粒度的瘦身里程碑制订,采纳绝对宽松和灵便的形式,把自主权给到具体负责的团队和开发者。
在瘦身成果上,能够较好维持住以后水位而不产生反弹,甚至是迟缓升高。常态化治理的“精力内核”是体验优先,将包瘦身这件事“融入”到日常研发迭代过程,与稳定性(crash/bug 等)、性能(启动速度 / 页面切换 / 晦涩度等)一样,独特成为研发团队(同学)在业务需要和性能之外关注并考量的技术项。
在工夫、人力、节奏、成果、精力“内核”这五个纬度上,二者的比照状况汇总如下:
常态化和专项式的关系并非简略的“优于”就可能说分明,首先二者具备演进关系,相似人类文明的“石器、青铜、农业、工业”等代际进化,常态化治理也是在生产力(剖析 & 瘦身技术)一直进步的状况下,促使生产关系(治理模式)等产生改革(嗯,这个比喻不肯定精确)。
其次,二者有着不同的实用状况,专项式治理用于疾速升高包大小,而常态化治理用于低成本可继续维持或者缓降。如果 app 无论与同类竟品还是本身相比,都显著处于较高的包大小水位,那显然须要先通过专项治理将包大小疾速升高上来,而后再连接上常态化治理来取得“长治久安”;如果 app 曾经处于常态化治理模式,然而因为某些起因须要进一步疾速升高,那么就须要切换到专项式治理模式,达成指标后再持续回到常态化治理模式。
常态化治理绝对专项式治理,更须要当作一个系统化工程来对待,整体治理思路如下:
由专项式到常态化,首先要做的转变是将关注点从“事”转移到“人”:每一个 Byte 都是由人(开发者)增加的,对产生的起因(技术、流程、心理等)进行全面剖析,并给出无效解决方案,才可能实现专项式到常态化的跃变。这个解决方案,次要包含技术撑持、治理策略两方面,二者相辅相成缺一不可。
2.2 技术撑持
整个技术支撑体系的外围是包大小精准剖析,即对 apk 内任意实体元素(类、资源、so 等)获取其在 apk 文件中理论占用值。因为编译过程会进行各种合并、裁剪、优化、格局转换等,同时 apk 中不同类型元素的压缩率也不雷同,如果应用原始大小作为度量规范,会在包大小治理的整个链路引入较大误差,导致难以抓住瘦身重点,也无奈提前精准预判瘦身成果,这对常态治理过程具备十分大的负面影响。
第一点可瘦身项,是指类、资源、so 等元素中的“不合理”项,对其进行革新或优化就能够升高包大小。这些可瘦身项细碎并且不易被人工发现,然而累积起来却不容小觑,通过工具化的剖析能力能够疾速找出这些可瘦身项,一方面提供瘦身领导:为逐渐升高包大小提供更多“方向”和“空间”,另一方面用来评判:一个性能 / 业务 / 团队,在这段时间内缩小 / 减少了哪些可瘦身项,以后是否曾经瘦无可瘦。
第二点归属聚合,是指将 apk 大小拆解到无效的责任实体,依据 app 波及到的研发团队和迭代模式不同,责任实体能够是组织构造中具体的团队,也能够是业务 / 性能 / 模块负责人。总之,拆解的目标是明确责任,即团队 / 业务 / 性能 / 模块对 apk 大小的“奉献”别离是多少。
第三点研发流程,是代码上线的“必经之路”,在这个过程中须要具备及时的增量感知,以及超限后的拦挡阻断能力。只有这样能力实现低(人力)老本继续管控,另外这也是去中心化策略的重要技术撑持,能够防止很多低效的增量定位排查、沟通、跟进等工作。
第四点辅助工具,是为研发同学对代码进行瘦身提供一系列切实有效的工具,用来提高效率以及升高危险。目前在优酷曾经积淀了援用剖析、代码归属 / 热度剖析、模块下载 / 版本号同步 & 查问 & 比照、progaurd 比照(mapping、usage)剖析、apk 信息查问 / 比照 / 反编译等共计十几项工具。
上述技术支撑体系在具体实现上,次要由“剖析工具”和“包大小卡口能力“承载,前面会做具体介绍。
2.3 治理策略
常态化治理模式下,治理策略的外围是去中心化。尤其是对于多团队参加研发的 app,对每个性能最相熟的人肯定是日常间接负责的(团队)同学,因而最高效的形式其实正是“各家自扫门前雪”的分布式治理模式。其中阈值划分,是实现分布式治理的第一步,即圈定“每家所负责的范畴,以及最多容许存在多少雪”。而灵便合作,目标是打造正当、公开、通明、高效、低成本的可继续治理场面,在为业务增长和翻新提供更多包大小空间的同时,将整包大小管制在预期范畴内。
剖析技术
剖析技术,是秉承 Byte 级“较真儿”精力,以包大小实在占用为领导准则,偏心公正童叟无欺,实现对不同颗粒度(元素、性能、业务、团队)的包体积占用度量,并在此之上提供切实有效的可瘦身项剖析能力,用于领导和评判瘦身状况。先来答复一个问题:剖析工具为什么很重要,很重要?
首先,既然要进行包瘦身,那么各种不同的“大小”就如影随形,例如:“xxx 模块多大?”、“xx 性能 / 业务占多大”、“最新版本 apk 绝对上个版本减少了 1MB,不同性能的变更带来的大小变动别离是多少?”、“我往年的指标是将 apk 缩小 10MB,能够通过哪些瘦身 Action 来达成指标?”。
工程畛域有几句驰名论断:“无度量不改良”、“无度量不治理”,包大小剖析工具的外围价值之一就是提供这个度量:各种不同颗粒度的度量,小到一个 java 类、资源、so,继而到一个模块(jar/aar)、再到一个独立性能、业务、甚至是团队。由此衍生开来,既然有了度量,那么就能够进行无效的责任归属、瘦身指标制订、成果预估等,是不是恍然大悟?
其次,在具备度量能力根底上,站在久远角度思考,理论瘦身治理过程中还须要可能答复“这个 app 是不是曾经瘦到极限,还有哪些地方能够瘦身?”,或者换个角度来看一个场景:你整体负责包瘦身工作,有三个业务,别离找负责同学沟通瘦身事项,可能会失去上面这样的回答:
通过下面这个场景,能够看到剖析工具须要具备的另外两个重要价值是:领导和评判。这二者其实是一个事务的不同视角,即:对被动者给予领导,对被动者给予评判,在瘦身前用于领导,在瘦身后用于评判。
一个满足无效的度量、领导和评判需要的包大小剖析工具,该具备怎么的自我涵养,这就是本章所要探讨的内容以及会给出的答案。在优酷,这个 Android 端包大小剖析工具的名字是 franky,潦草诞生于 2020 年初,逐渐迭代欠缺 / 加强至今已 2 年无余,趋于成熟,仍在前行,目前正在筹备开源中,心愿能给 Android 包瘦身治理带来一些帮忙。
3.1 方案设计
本章将围绕“度量、领导、评判”这几个外围价值进行方案设计。首先无妨采纳问答的形式,来进行具体分析和拆解的推导过程。
度量的对象是谁?度量的值又是一个什么样的值?在包瘦身不同场景,关注的对象也不一样,颗粒度最细的是 apk 中各种元素(java 类、java 资源、十几种不同类型的 Android 资源、动态链接库 so),再往下层的是模块(jar/aar),持续往下层则是性能(由多个模块组成的独立残缺性能)、业务(由多个性能组成的残缺业务)、团队(组织架构中一个团队负责的所有业务),再持续往上就是 apk 了(这个没有什么意义,一个文件的大小基本不必什么剖析工具),当然一个小型 app 可能在模块之上仅须要一层性能聚合就足够了。
至于度量的值,则是在 apk 中的实在大小占用,即删除后 apk 能够缩小的大小,对于某些元素原始大小和对 apk 大小的实在占用之间,存在着十分大的差距,这个差距会导致对瘦身 Action 的成果评估呈现不可漠视的误差,从而使瘦身 Action 的优先级排序、领导和评判失去根基。由此失去“度量”的需要拆解后果是:提供元素、模块、性能等不同颗粒度,在 apk 中实在占用大小的度量。
领导的内容有多具体?评判的根据又是否偏心、通明?瘦身的领导,如果只提供一个大略的方向,是远远不够的,须要十分明确、具体、可操作。
举个例子:如果我只通知你“充分利用 proguard,精简优化 keep 规定,让更多的类被裁减和混同,就能够无效瘦身”,那么如何让参加 app 开发的所有同学,都可能据此高效的实现这项瘦身工作?然而如果可能给出“你负责的业务 / 性能 / 模块,类未混同率是 80%,数量是 600 个”,相比前者显然更具备指导性,更进一步,假如还可能给出“每个未混同类,是被哪些 keep 规定所影响”,是不是理论瘦身过程变得更加有迹可循,置信任何一名开发同学都可能很好的实现这项工作。
再举个例子:“缩减或近程化大尺寸图片,能够无效瘦身”,与“你负责的这个模块,对 apk 实在大小占用超过 10KB 的图片,一共有 10 个,别离是 xxxx”,这二者相比显然后者更具指导性。再来说说瘦身的评判,不能依附人的能力和判断力,这样很难做到偏心、通明,须要通过可量化的数据作为根据。由此失去“领导 & 评判”的需要拆解后果是:提供明确、具体、可量化、可操作的可瘦身项剖析,用于对瘦身过程进行领导和评判。
此外,还有两个十分事实的问题,也不可避而不谈。剖析覆盖率(可能找到模块归属的元素大小之和,占 apk 大小的百分比)可能做到多少?对于剖析覆盖率,理论值就应该是 100%,apk 构建过程没有 magic,所有在 apk 中存在的元素皆有起源。当一个元素(比方资源、so)被多个模块(这里的模块是狭义上的模块,例如 app 工程、subproject 工程,具体可参考此文章)蕴含时,这个元素归属到每个模块的大小怎么计算?从偏心的角度,多模块蕴含的反复元素,归属到每个模块的大小应该是等比例分担(Proportional Set Size)的。
依据下面的推导过程,咱们来进行提炼和总结:
当初,如果让你来答复以下几个问题,是不是就能够信手拈来,轻松惬意?
- apk 为什么这么大,不同模块 / 业务 / 团队,别离奉献了多少?
- 每个团队 / 业务 / 模块,有哪些能够瘦身的中央,进行删除、优化、革新后,apk 能缩小多少?
- 在日常迭代中,以后版本绝对于上个版本,每个团队 / 业务 / 模块减少(缩小)大小是多少?
实际上,优酷自研包大小剖析工具 Franky,简直齐全实现了上述拆解后的需要。只有一点尚未做到:apk 中元素,目前还没有做到 100% 找到模块归属,在优酷 apk 中的剖析覆盖率是 99.8% ~ 99.9%(apk 100MB ~ 65MB)。
1. 整体架构
Franky 次要由两局部组成:用于 application 工程的 gradle plugin,以及命令行(cli)剖析工具。此外,还额定依赖(非必须)两个内部数据:模块图谱数据,用于将模块大小,向上聚合为性能 / 业务 / 团队的大小;代码笼罩数据,造成可瘦身项剖析中的「代码 – 无用类」(SlimLady:类级别不插桩线上代码覆盖度统计框架)。整体架构如下图所示:
franky-plugin 的作用,是在 apk 构建过程中收集 apk 所有组成模块,以及模块中蕴含的各类元素,此外还蕴含类混同映射关系、无用资源剖析后果。这个剖析后果数据与 apk 文件,独特形成了命令行工具 franky(cli)的根底(必须)输出文件。接下来,执行 cli 命令进行最终的包大小剖析,产出具体的剖析报告。
纵观这套计划,可能会有一个疑难,为什么要蕴含一个构建插件,如果能通过一个命令行(cli)工具间接对 apk 进行剖析,应用更简略还能更具通用性,不是更好吗?这外面有一个十分要害的点在于,只有在 apk 构建过程中,才可能获取 apk 由哪些模块组成、每个模块又蕴含哪些元素,在 apk 构建实现后的 apk 文件中,这些信息曾经失落,所以构建插件必不可少。那如果是这样,为什么不把命令行工具的所有性能,都放在这个构建插件中来实现呢?这是一个好问题,目前的思考是这样的:尽量将构建插件做的比拟“薄”,这样能够缩小构建耗时,而将次要剖析性能放在独立工具,能够独立疾速迭代,而不必频繁在 app 工程中降级 plugin 版本。
2. 关键技术
剖析工具看起来简略,然而为了获取实在大小,以及可能将 apk 中元素 100% 进行模块归属,在开发过程中还是会遇到不少辣手问题。
首先,利用于构建过程的 plugin 如何保障兼容性,并不是一个简略的问题。很多 Android Gradle Plugin 开发者不太器重兼容性,认为针对特定工程实现相干性能就高枕无忧,这里不深刻探讨此话题,间接给出 franky-plugin 思考并实现的构建环境兼容性,或者更可能对这个问题取得直观的认知:
接下来的外围艰难是,参加 apk 构建的原始元素,与最终 apk 元素之间,存在转换、新增、删除、不变这四种“变动”状况:
上图给出的是根本“变动”状况,还有一些非凡状况也须要思考,例如 java 资源能够“假装”为其它类型元素(详情参考这篇文章)、Android 资源蕴含 api level 大于等于 22 可用的 android:xxx 属性,且资源的 api 配置限定符小于 22,导致生成“-v22”资源文件、AAPT 内嵌资源生成独立资源文件等。对于删除和不变的元素,解决起来比较简单,转换和新增的解决则绝对简单一些:
- 新增。新增元素最大的问题在于“找到归属”,例如有些 java8 语法在脱糖后生成新的类。目前 franky 中基于生成类名称规定的形式,解决了 lambda 表达式、默认和动态接口办法的归属状况,然而对于办法援用的生成类则临时无奈找到归属(前面打算通过代码 Pattern 剖析来找归属);
- 转换。产生转换变动的元素,如果还进行了“合并”解决(例如 dex、resources.arsc),此时就须要将元素“拆”进去,外围准则是:拆出来后元素大小之和,等于拆之前文件大小。例如,将 class 从 dex 文件拆出来后,大小相加必须等于 dex 文件大小。这个事件的难度来自于两个方面:
<!—->
- 各种字符串、类型共享池,须要进行按比例(PSS)分担计算;
- 各种二进制构造数据的 Header、Padding,须要进行精准计算和归属。
<!—->
除了这些变动,还有一个细节也要思考:apk 中各文件的实在大小占用如何计算?apk 实质是一个 zip 压缩文件,其中每个文件均为一个 zip entry,zip entry 占用大小相加小于 apk 总大小,因而须要将用于记录每条 zip entry 的额定大小加进来(Local File Header、Central Directory Record),同时将共享局部进行按比例分担。以优酷为例,apk 大小为 65MB,zip entry 压缩大小相加是 63MB(97%),如果不计算额定数据大小,仅在计算 apk 中元素大小时,就曾经损失了 2MB 的实在大小!
3.2 可瘦身项剖析
可瘦身项是领导和评判价值的次要承载者,本章对 franky 蕴含的全副 9 个可瘦身项,解说根本技术原理、剖析成果、用于瘦身时的注意事项等。
第一项【代码】无用类,是指在运行时没有被应用到的类。以后无用代码的获取办法,是通过线上采样的形式,采集代码热度数据,并筛选出其中初始化次数为 0 的类(具体实现计划来自 SlimLady:类级别不插桩线上代码覆盖度统计框架,此处只是应用了前者的后果数据)。对于无用代码较多的模块,存在线上使用率低(或者齐全无应用)的问题,应该安顿下线或者应用 H5 等动态化形式实现。剖析报告中的示例后果如下:
第二项【代码】未混同类,是指因为 keep 规定存在,导致没有被混同的类。混同能够极大升高代码在 apk 中占用的大小,因而除了一些非凡应用场景,绝大部分类都能够进行混同。对于未混同类较多的模块,可能存在混同 keep 配置过于宽泛问题,能够参考这篇文章。剖析报告中的示例后果如下:
第三项【资源】超大,是指在 apk 中实在占用超过肯定阀值的资源(可配置,优酷始终应用的是 10KB)。超大资源,能够采纳近程化、从新设计更小的等效资源、如果是图片还能够采纳压缩率更高的图片格式(jpeg、webp)或者矢量图等形式来升高大小。剖析报告中的示例后果如下:
第四项【资源】无用,是指没有被间接援用的资源。从资源整体应用状况来看,一个资源可能在三个中央进行间接援用(具体能够参考这篇文章):java 代码,通过 R.resourceType.resourceName 形式援用(例如 R.string.app_name),或者通过资源 id 形式间接援用(例如 0x7fxxxxxx);清单文件 AndroidManifest.xml;其它资源。另外,资源还能够通过 Resoruces.getIdentifier 形式,通过传递资源名称和类型获取 id 值,运行时性能较差,因而官网并不举荐应用,须要留神的是,剖析工具对这种形式会呈现误检。对于无用资源,应该在确认未通过 Resoruces.getIdentifier 形式应用后,进行删除解决。剖析报告中的示例后果如下:
第五项【资源】多维度,是指蕴含大于两个配置的资源。这种资源会在不同配置下,存在多份数据(文件),一些非凡纬度须要做额定思考,例如:night、land & port。对于多维度资源,一些非必要的配置能够清理掉。剖析报告中的示例后果如下:
第六项【资源】无透明度 png 图片,是指 png 图片中蕴含了 alpha 通道,然而无相干数据,或者数据中的透明度值均为齐全不通明。对于这种类型的图片,个别能够通过应用不带透明度信息的其它图片格式(例如 jpeg),来升高大小。剖析后果中,曾经排除了.9 类型图片,在剖析报告中的示例后果如下:
第七项【资源】类似,是指资源值的类似程度较高。以后剖析只蕴含 file-base 类型资源(绝对的,值仅存在于 resources.arsc 中的资源,称为 value-base 类型资源)。对于非图片文件资源,仅筛选出齐全一样的资源(md5 统一,类似度为 1)。对于图片文件资源,额定计算了类似度,采纳 DHash 算法计算图片指纹,而后计算 hamming 间隔作为类似值。对于类似度为 1 的资源,内容完全一致,因而能够仅保留一份,对于类似度小于 1 的资源,因为有些图形简略的图片资源特色信息不显著,因而即便类似度较高,是否可相互代替的最终决策,依然须要依据所在业务场景进行人工判断。剖析报告中的示例后果如下:
第八项【so】不标准应用 STL,是指动态链接库 so 对 c ++ STL 库的不标准应用(基础知识能够参考这篇文章),包含两种状况:动静链接非对立 STL,官网倡议对立应用的 STL 为 libc++_shared.so,其它非对立 STL 包含 libgnustl_shared.so、libstlport_shared.so;动态链接 stl,通过革新为动静链接,能够实现较好的瘦身收益。剖析报告中的示例后果如下:
第九项【so】无用导出符号 。动态链接库的导出符号(exported symbol),是指在 so 内定义的对象、办法、全局变量,被设置为可被内部代码援用(导入)。无用导出符号,则是指在依赖这个 so 的其它 so 中(apk 范畴内),未找到任何援用,当然这里存在以下状况须要非凡解决:JNI 办法、通过 dlsym 形式加载并调用的符号。对于的确无用的导出符号,能够在编译 so 时设置为不导出。具体操作形式并不惟一,比拟倡议应用编译选项 -fvisibility=hidden,同时显示对须要导出符号减少 attribute ((visibility (“default”))) 标记这套计划来实现,这样新增符号默认不会导出,不至于呈现一段时间没人管,无用导出符号继续累积问题。剖析报告中的示例后果如下:
3.3 卡口能力建设
后者在研发迭代过程中,低成本维持常态化治理模式的要害之一,其承载的三个外围价值如下:
去核心,须要可能对 apk 大小进行适当颗粒度的精准拆分,用于卡口阈值、检测以及不通过时进行拦挡。
促前置。包大小和代码标准、bug 等单点问题不同,无奈通过就地批改来实现瘦身,往往须要通过“拆东墙补西墙”的形式,寻找存量可瘦身空间来补救新性能带来的增量。所以,只有足够前置能力留出更多工夫给瘦身治理,从而保障最终公布到用户手中的 apk 大小保持稳定。前置要求卡口必须在代码变更后“第一工夫”发挥作用,辨认到包大小变动,如果超过阈值则进行拦挡。这个“第一工夫”依据不同 app 迭代模式差别,选取适当的节点即可,例如优酷的一个版本迭代能够分为“提测 - 集成 - 灰度 - 公布”四个流程节点,那么就抉择提测和集成两个节点部署卡口。
低成本。卡口是一个比拟含糊的概念,1 百个工程师可能会有 1 百个对卡口具体机制的了解和设计,对于包大小卡口自身肯定要具备的是低成本保护,包含以下几个方面:
- 与研发流程联合。包大小卡口不是一套独立的流程,而是要融入到整个研发流程中,尽可能减少额定应用老本;
- 100% 笼罩。肯定是要对所有可能的增量起源,都可能笼罩到,不能存在某种形式,能够绕过卡口机制。一旦被绕过,会拉高后续的辨认 & 治理老本;
- 100% 自动化。这一点看起来有点像废话,卡口难道还能手动执行?现实情况是,有些场景下所谓的“卡口”,其实自动化水平较低,还须要不小的人工参加。
优酷在 2018 年就建设了包大小卡口能力,后续的演进和调整都是朝着更贴合上述外围价值的方向进行,直至 2021 年初才达到稳固无效的成熟状态。包大小卡口由剖析工具 franky、卡口能力平台、研发流程管控平台(CI/CD 平台)三局部组成,示意图如下:
研发在应用流程(CI/CD)平台进行提测和集成时,都首先须要触发 apk 构建,franky-plugin 作为包大小剖析工具在构建期的一款 gradle 插件,会收集数据并将后果输入到构建产物。接下来,流程平台中的卡口插件负责收集 apk 和 franky-plugin 生成的文件,并上传到卡口平台备用。
之后,流程平台会执行准入检测,其中包大小卡口检测会触发能力平台中的包大小剖析工作,通过调用 franky 对应的命令行工具,生成 json 格局的包大小剖析报告。通过解析剖析报告,并与事后设置的团队阈值和 Buffer 值进行比照,以此断定提测 / 集成单中蕴含模块所在的团队(1 个或多个)是否超限。流程平台中的准入检测在获取检测后果后,通过或者阻断提测 / 集成流程。如果卡口未通过,能够通过进行瘦身革新来使卡口通过,但这个别无奈在以后版本实现,这时能够申请带时限的 Buffer 来长期通过卡口,从而实现提测 / 集成。
瘦身技术
后面讲了很多治理模式相干内容,看起来可能有些形象,接下来会回到具体的瘦身技术,侧重点不在于深刻技术原理和实现细节(大多会给出参考链接),而是尝试将每一项瘦身技术的优缺点进行一次概括性解说和巡展,便于造成一个整体认知,在对具体 app 进行瘦身时,可能依据理论状况进行抉择和优先级安顿。
瘦身技术,顾名思义,就是指能够用于包瘦身的任何技术(计划),依照所需技术、失效阶段、影响范畴综合评判,将其划分为以下三种类型:
4.1 近程化
近程化是指将本来在 apk 中的性能,剥离进去放到服务器,app 运行时进行下载、加载等一系列动作后,才可能失常应用性能的一种技术计划,其外围特点能够演绎为“本地剥离,近程下载”。近程化瘦身效果显著,也因而容易为了谋求瘦身后果而被适度应用,但这并不是近程化自身的原罪,实际上一些边缘、非核心、实验性业务,都比拟适宜进行近程化革新。近程化框架波及的关键技术,在业内曾经有很成熟的解决方案,然而具体到代码实现,还是有不少须要认真思考和重复打磨的中央,例如:apk 构建体系的兼容性是否足够宽泛,近程化革新的代码限度和革新难度是否足够低,app 唤端、多过程、用户磁盘占用、下载线程占用、apk 降级复用、下载带宽老本等。
依照近程化元素类型,以及业界广泛应用状况,将其分为近程 so、近程 bundle、近程资源三种。
首先来看近程 so。动态链接库 so 与其它代码的耦合度低,在 apk 中具备较强的独立性,同时占用 apk 体积绝对较大,因而独自将 so 进行近程化往往具备很高的瘦身投入产出比。
第二种近程 bundle,个别是指能够残缺的将一块性能进行近程化,近程局部相当于一个迷你 apk(dex、resource、so)。近程 bundle 相干技术“历史悠久”,动态化、插件化、组件化等尽管有语境、性能以及设计思维上的区别,但在技术上有着很多类似的中央,随着新版本 os 增强了对系统 API 调用、拦挡和替换等方面限度,这个畛域的支流技术计划逐步演变为对系统侵入越来越轻量的方向。绝对于近程 so,近程 bundle 在实现上要简单很多:在运行时阶段,尽管对系统 API 的侵入性比拟小,然而对唤端、组件路由跳转、后盾 Activity 销毁重建等状况仍然须要小心解决;在构建阶段,因为要“拆散”出一个迷你 apk,因而对构建体系的兼容十分艰难,目前业界有一些同类框架,很多时候并不是运行时无奈满足需要,而是在构建侧无奈做到很好的兼容性和易用性。
最初一种近程资源,是指针对资源文件的近程化。能够通过将资源上传到文件托管平台,获取文件 url 后间接下载并应用(优酷采纳的就是这种形式)。近程资源的理论利用场景较少,投入产出比也不高,所以目前优酷没有专门研发一个这样的框架。当然,如果有大量资源须要进行这种近程化革新,那可能有必要开发一个专用框架。
4.2 整包瘦身
整包瘦身,是指在 apk 构建阶段整体进行解决的一类瘦身技术,对全副 apk 元素均可失效(包含无源码的二、三方 sdk),新增代码也能够立即失去同样的解决,其外围特点能够演绎为“两头拦挡,整体失效”。也正是因为上述特点,这类瘦身的影响范畴较广,因而在首次利用到 app 时,如何管制好验证老本和线上危险变得十分要害,当然在瘦身成果上,个别能够空谷传声的取得较大收益。自定义的一些整包瘦身计划,往往容易呈现解决逻辑考虑不周全而导致的稳定性问题,留神这不属于技术计划自身的特点,而是具体实现代码的问题。在工程效力方面,对代码品质无影响,构建耗时则个别会有减少,有些整包瘦身技术会扭转 apk 中指标元素状态,因而对各类相干问题剖析会带来肯定水平的效率升高。
这里划分了 14 项整包瘦身技术,其中 Android 官网没有提供的能力,都曾经积淀到了优酷自研 gradle plugin 中,目前正在开源筹备中。
- 【代码】Proguard/R8。利用 Proguard 工具,对 java 代码进行裁剪、混同、优化解决,从而实现无用代码删除、符号(类、变量、办法)混同、代码逻辑优化,包体积升高成果十分显著。值得注意的是,google 官网曾经在近几年的 Android Gradle Plugin 中,应用自研的 R8 代替了 java 畛域传统的 Proguard 工具,裁剪和优化成果更为弱小,进一步压缩包大小的同时解决耗时更低,优酷从 proguard 切换到 R8 后,包大小升高约 4.8%(3.2MB),构建耗时升高约 25%(2min),当然这也和原 progaurd 的全局配置无关,尤其是优化次数 -optimizationpasses。对于 proguard 更多基础知识能够参考文章《向工程腐化开炮:proguard 治理》;
- 【代码】D8。在 apk 构建过程中,java 代码须要经验由 jvm 字节码到 dalvik 字节码的转换解决,DX/D8 就是承当这个责任的工具,对于二者的比照以及更多相干常识,能够参考文章《向工程腐化开炮:java 代码治理》。在优酷的具体实际中,由 DX 降级到 D8 后,包大小升高约 9.5%(9.7MB),因为额定对 dex 合并进行了优化,dex 数量升高,导致包大小收益呈现一次跃升,因而比官网给出的 Benchmark 收益 5% 要更高;
- 【代码】R 类合并。将所有 < 模块 package>.R 类移除,并将 java 代码中对前者的援用对立替换为 <app package>.R 类,以此来升高包大小的一种技术手段。在优酷当前情况下(模块 800 多个,dex24MB),R 类裁剪能够缩小 80 万个 java 类 Field,带来近 5MB 包大小收益。因为每个 dex 中 Field 数量也受到 65536 限度,因而 Field 数量大幅缩小所带来的 dex 数量缩小,是瘦身收益的次要起源。进一步,能够把所有 java 代码中 R.<type>.<name> 的援用,也全副替换为对应 id 值,这样 <app package>.R 类也能够删除,然而在曾经实现 R 类合并的状况下,这个解决的收益比拟无限,因而优酷并没有理论投入研发和应用,然而如果谋求极致瘦身的确能够这么做!对于 R 类的更多相干常识能够参考文章《向工程腐化开炮:资源治理》;
- 【代码】Dex 排布优化。Dex 排布优化是指通过合理安排 dex 中蕴含的类,从而尽可能减少常量池冗余度以及 dex 数量,进而升高 dex 整体大小的一种瘦身技术。因为历史起因,Dalvik 字节码中调用 method 和 field 指令的操作数是 16 位,因而一个 dex 中 method 和 field 数量下限均为 65536,而古代 app 个别都会蕴含多个 dex,dex 数量过多会导致各类常量池冗余度变高,从而导致包大小减少。事实上,Dex 排布优化不仅能够用于升高包大小,还能够通过抉择不同的优化策略,来晋升 app 运行时的性能,Facebook 的 Redex 即是这一畛域的驰名开源框架。优酷并没有应用简单的排布优化策略,而是自定义了简略的 Dex 合并能力,取得了约 2MB 左右的包瘦身收益;
- 【代码】字节码指令精简。精确的说这并不是一项瘦身技术,而是一类瘦身技术的汇合。通过更精密的字节码上下文剖析,以删除、合并、转换等形式精简指令序列,从而达到瘦身目标,例如删除冗余赋值指令(值与类型默认值统一)、access$xxx 办法打消(批改 private 办法为 public,防止 access 办法生成)、常量 / 短办法内联等等。不晓得大家是不是会有个疑难,proguard/R8 没有进行这些解决吗?这些“民间”自定义的字节码指令精简计划,能够看作是对前者的一种“极致性”扩大,因为前者在进行字节码优化时对正确性的要求极高,如果有些优化策略存在危险,或者违反原代码设计用意(比方批改 private 办法为 public),那么就不会利用。当须要应用自定义的字节码指令精简之前,倡议先把 proguard/R8 各种优化配置选项钻研透彻并充沛利用,可能你会发现通过配置就能够实现同样成果,并且处理过程更稳固、高效、可信,如果不得不走到须要进行自定义解决的地步,也肯定要审慎应用;
- 【资源】无用资源裁剪。ShrinkResources 是 Android 官网提供的无用资源裁剪性能,在 apk 构建时间接对无用资源进行删除。在 app 中除了通过 R.xxx.xxx/0xffxxxxxx 显式援用资源,还能够通过 Resources.getIdentifier 在参数中指定资源名称来援用,因为后者能够拼接甚至是动静下发字符串,因而会导致此类资源的援用关系无奈被精确获取,对此 ShrinkResources 提供了两种模式:严格模式、失常(默认)模式。严格模式仅思考显式援用关系,失常模式则会采纳“平安优先准则”,如果资源名称以任何 java 代码中的常量字符串为前缀,那么会被标记为疑似援用而无奈失去删除,还有其它几种“疑似性标记逻辑”不再列举;
- 【资源】多维度(备用)资源裁剪。Android 资源能够配置不同维度,从而在运行时灵便适配各种不同状况(语言、屏幕尺寸、屏幕横竖状态、os 版本等),这里的多维度(备用)资源裁剪,就是在 apk 构建期将不须要的维度裁剪掉,AndroidGradlePlugin 提供了对应性能,通过 android DSL 配置(resConfigs)即可间接实现。自研代码个别只会蕴含须要用到的资源,而二、三方 sdk 为了进步兼容性会蕴含尽可能多的维度,在优酷实际中对语言维度进行了裁剪(仅保留中文),瘦身收益约 3%(2MB);
- 【资源】图片压缩。图片压缩,是指在 apk 构建期批量对图片进行压缩,或者格局转换的一种瘦身技术。优酷自研 turbo-plugin 中蕴含了这项性能,在解决上的考量包含:提供配置项对压缩品质(quality)进行设置,这决定了压缩率(图片有损水平);有些图片压缩后,尺寸反而会变大,通过查看压缩后果,当发现这种状况时对这些图片不应用压缩。在优酷实际中,图片压缩带来的瘦身收益约 0.8MB,收益较低的起因,次要是有很多模块中的图片,提前曾经进行了压缩解决;
- 【资源】resources.arsc 压缩。resources.arsc 文件在 Android 原生构建流程中不会进行压缩,而运行时 os 辨认到 resources.arsc 被压缩后,存在兼容逻辑对其进行解压解决,所以能够通过压缩 resources.arsc 来进一步升高包大小。须要留神的是,os 在运行时解压缩 resources.arsc 会导致资源查找耗时减少,官网也不倡议这么做,并且当 apk 的 targetSdkVersion 大于等于 30 时,无奈在 Android11 及以上设施中装置;
- 【资源】去重。对于值雷同的不同名资源仅保留一份,删除反复资源并将所有援用到的中央替换为保留下来的资源。和 ShrinkResources 一样,在利用这项技术时仍然须要留神,通过 Resources.getIdentifier 以资源名称作为参数形式应用到的资源,不能进行去重解决。优酷已经应用到了这项技术,瘦身收益在 MB 级别,当初曾经通过对应的「单点瘦身」计划,在源码层面间接解决;
- 【资源】混同。和 java 代码 Proguard 混同相似,是通过缩短资源名称以及文件类型资源寄存门路,实现包瘦身的一种技术计划。AndResGuard 是实现这项瘦身技术的一套开源框架,性能绝对成熟且齐备,起初也有些一些同类框架在根底的资源名称缩短之上,又进一步对 resources.arsc、xml 资源等进行了更精细化的瘦身解决,具体能够参考相干公开文章。在利用这项技术时,仍然须要留神解决 Resources.getIdentifier 带来的问题;
- 【so】debug 信息裁剪。debug 信息裁剪,是指将 so 中携带的 debug 信息删除掉,这并不会影响 so 失常性能,只是会导致无奈源码调试 so。在 Android 官网 apk 构建过程中,默认有一项 StripDebugSymbol 的解决逻辑,正是用于对 debug 信息进行裁剪,之所以作为一项整包瘦身技术放在这里,是因为如果构建环境中未蕴含 NDK(或者 NDK 未在可辨认门路),那么这项解决就不会执行,然而 apk 还能够失常生成,这一点须要特地留神;
- 【so】abi 分包。abi(application binary interface)是指利用二进制接口,不同 Android 设施应用不同的 CPU,而不同 CPU 反对不同的指令集,CPU 与指令集的每种组合都有专属的利用二进制接口 (ABI)。在 Android 生态中 arm CPU 是相对支流,指令集按反对的 CPU(指令寻址)位数可分为 32 位(armeabi、armeabi-v7a)和 64 位(arm64-v8a)两大类,32 位设施只能运行 32 位的 so,64 位设施既能够运行 32 位 so 也能够运行 64 位 so。尽管目前市场中 64 位设施曾经成为相对支流,但 32 位设施也还没到能够舍弃的量级,因而 apk 如何同时反对 64 和 32 位设施就成了一道选择题:合包,即 apk 中同时蕴含 32 和 64 位两套 so;分包,即分为 32 位和 64 位两个 apk,各自仅蕴含一套对应的 so。显然,后者能够极大减小包体积,但也会带来 app 散发的一些问题,须要辅以额定解决逻辑。对于分包计划,在 apk 构建时应该保障一次构建间接生成两个分包的形式,这样能够防止屡次构建不统一带来的 32 位和 64 位包代码差别问题;
- 【apk】7z 压缩。7z 是一种压缩格局,同时也是一个压缩工具。这里的 7z 压缩是应用 7z 工具代替 Android 工具链,应用能够被 Android 零碎所兼容的压缩算法,对 apk 中(实质就是一个 zip 文件)本来就会压缩的文件进行成果更好的压缩解决,从而实现瘦身的一种技术计划。因为并未扭转 apk 元素内容值自身,因而根本无需验证即可稳固上线应用。在优酷的实际中,包大小升高约 4%(3.5MB)。
4.3 单点瘦身
单点瘦身,是指在源代码层面,通过去除无用、合并冗余、修改不合理等形式实现瘦身,其外围特点能够演绎为“源头解决,轻爽衰弱”。因为须要在源码级别操作,因而只能针对有源码工程的自研代码,对于无源码的二、三方 SDK 则无奈施行(其实也能够在字节码层面革新 sdk,非常规计划)。另外,之所以称为“单点”瘦身,是因为须要对每一个具体的可瘦身点进行革新、验证并上线,因而最好是对代码最相熟并负责的同学间接上手革新,这类瘦身的利用难度整体较低,然而波及研发同学范畴很广,革新周期通常也十分之久,同时在瘦身成果上个别会比拟迟缓。
这里划分了 9 个单点瘦身技术,在优酷自研的包大小剖析工具中,均实现了对应的检测剖析能力,具体能够参考前文「剖析技术」章节,这里简略列出:
- 【代码】线上无用类
- 【资源】超大
- 【资源】无用
- 【资源】多维度
- 【资源】无透明度 png 图片
- 【资源】类似
- 【so】动态链接 C ++ STL
- 【so】链接非标准 C ++STL
- 【so】无用导出符号
另外,无用和冗余的去除,自身就是一种代码品质的晋升,也能够明显降低工程腐化水平,同时对构建耗时也会有正向收益,对于工程腐化这个话题,能够参考《向工程腐化开炮》系列文章。
还能做些什么
包瘦身是挪动 app 畛域长期存在的一个工程问题,无论是否关注和治理,其影响始终客观存在。接下来聊聊一些相干的思考,心愿可能给感兴趣的同学带来一些有价值的参考和启发。
5.1 信心
任何新需要迭代简直不可能做到 0 代码减少,因而包大小人造是一个与代码增量“反抗”的事件,但又不像稳定性、性能一样能够产生立刻、间接的影响,所以在写代码时很容易被忽视。如果包瘦身的重要性并没有在 app 全开发团队高低,取得一致性的认可以及足够的信心,即便相干技术、卡口能力、治理策略再怎么欠缺,也无奈在这场“包瘦身持久战”中始终利于不败之地。
前文所属的常态化治理模式下,各种技术撑持以及治理策略,究其实质都是为了将“对包大小的考量”融入到每一名研发同学的代码思维中,这样才可能在 coding 阶段就尽可能减少包大小不敌对代码的产生。“不产生”比“产生了再治理”,在对研发同学技术能力的要求上,恐怕要高出不止一个段位。在谋求卓越工程师的路上,无妨把代码对包大小的影响也纳入进来吧。
5.2 以包大小为支点
“穷则独善其身,达则兼济天下”,当包瘦身治理曾经处于良好的常态化治理场面时,因为包瘦身实质还是对 app 工程中不合理代码的改良,因而无妨以包大小为支点,撬动用户体验、工程(代码)品质等其它方面的晋升。各种以瘦身作为“导火索”的代码清理、优化、革新,实际上是对 app 整体工程和代码衰弱度的无效晋升,也是促使业务间性能复用的重要推动力量。而这些代码和业务功能设计层面的进步,长期来看也会对 app 稳定性、性能、研发效率等的全面晋升,具备很好的促进作用。
5.3 摸索实际永不止步
尽管优酷的包大小治理,曾经处于可继续的常态化治理模式,但瘦身相干的技术摸索,以及现有技术的残缺落地实际,还没有到完结的时候。很多存量技术问题仍有待开掘,例如:对于动态链接库 so 的检测剖析技术,还有不少能够摸索的方向;对于混同规定精简,如何可能提供更无效的辅助工具,进一步升高剖析、革新、验证的老本和危险,也是一件很有挑战的事件;对于各种中间件,如何可能作出对包大小更敌对的设计和迭代,这也曾经超出集体、单个组织所可能实现的范畴,然而如何可能对此带来更好的影响和扭转也值得思考。新技术的趋势和影响也须要及时关注:AndroidX 蕴含的新组件、新开发模式,各种手机厂商的特色能力 sdk 一直引入等等,都会带来新的时机和挑战。