关于程序员:5年磨一剑|优酷Android包瘦身治理思路全解

50次阅读

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

简介:稳定性、性能、包大小,在挪动端根底用户体验畛域“三分天下”,是 app 承载业务取得稳固、高效、低成本、快速增长的重要基石。其中,包大小对下载转化率、拉新拉活老本等方面的影响至关重要,这在业界曾经成为共识。本文聚焦于整体治理思路,以治理实际为依靠,讲述瘦身技术、治理模式、治理策略,以及背地的思考与取舍。
image.png

作者 | 谦风
起源 | 阿里开发者公众号

稳定性、性能、包大小,在挪动端根底用户体验畛域“三分天下”,是 app 承载业务取得稳固、高效、低成本、快速增长的重要基石。其中,包大小对下载转化率、拉新拉活老本等方面的影响至关重要,这在业界曾经成为共识,近年来头部 app 针对下沉市场的极小包策略,更是将包大小的价值晋升到了极致。优酷在 Android 包大小畛域,有长达 5 年的继续投入、实际和积攒,尤其是在近 2 年逐渐进入低成本可继续治理的衰弱状态。现将这些思考、方案设计、技术建设、治理实际对立汇总整顿成文并分享进去,心愿可能帮忙更多同学在所负责或参加的 app 中,更好的进行包大小治理。

本文聚焦于整体治理思路,以治理实际为依靠,讲述瘦身技术、治理模式、治理策略,以及背地的思考与取舍。

五年治理回顾
作为开篇,先给出优酷近 5 年包大小变动状况:

image.png

以 2020 年 9 月为分水岭,从治理模式角度,能够将前后划分为两个“格调迥异”的阶段:专项治理、常态治理。包瘦身治理也属于一种软件工程,接下来围绕“术”、“道”、“人”三个维度,开展回顾和总结:

1.1 专项治理 3 年:三次两反弹
自 2017 年初至 2020 年 9 月这 3 年工夫,共经验三次专项治理以及两次反弹。

image.png

2017.05 – 2018.03,第一次专项治理。在瘦身成果上,从最高点 73MB 升高到 51MB,瘦身比例约 30%,这次瘦身专项的最大价值,是积攒了贵重的实践经验:

技术手段。因为过后简直没有积攒,采纳的技术手段绝对惯例且具备单点性,次要包含:剖析并下线无用业务 / 功能模块、近程化边缘业务、图片压缩 & 矢量化等。
治理策略。不足整体指标掌控和拆解,对于头部问题进行单点革新。
组织模式。比拟涣散,波及范畴窄,参加人数少。
2018.04 – 2018.09,第一次反弹期。期间应用“模块级”包大小卡口作为管控伎俩,因为短少相干剖析技术撑持,申请方和审批方对存量 & 增量状况都短少清晰统一认知,导致管控逐步流于形式。与此同时,包瘦身治理优先级升高,后面负责治理的架构同学撤出,尽管架构团队仍然负责跟进相干事项,但简直没有被动投入治理,包大小靠近天然状态下的“横蛮成长”。

image.png

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 水位。

image.png

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

image.png

在这个阶段,包大小卡口能力实现了一次要害进化:与研发流程实现无缝联合,对超限状况实现及时感知,以及拦挡阻断,这让整体管控老本失去极大升高。同时,对剖析技术、瘦身技术的迭代、摸索和利用,始终没有停下脚步:dex 排布优化、7z 压缩、D8、R8 等整体瘦身技术陆续上线,so 无用导出符号等可瘦身项继续退出剖析工具 franky,相干技术也开始失去阿里外部更多 app 应用,这进一步促成了性能疾速倒退和丰盛。另一方面,治理策略也在逐步完善,客户端各研发团队围绕本人的包大小阈值,把包大小晋升为与稳定性、性能一样的日常研发迭代根底考量指标。

治理模式
后面通过术、道、人三个维度对历史进行了回顾,通过比照不难发现它们有着截然不同的特色,据此能够将包瘦身治理分为两种模式:专项式、常态化,前者以短时间疾速瘦身为指标,后者以长时间可继续维持为指标(甚至逐渐升高)。看到这里,或者会提出一个疑难:治理模式和“术、道、人”三维度有什么关系?如果肯定要进行辨别,我认为既能够看作不同的思考视角,也能够认为前者是后者的更高层次形象:“术”的能力所达到的程度,“道”的抉择所遵循的准则、“人”的排布所提供的保障,独特决定了以后处于什么样的治理模式;反过来也实用,即治理模式对“术、道、人”的内容和边界,都有明确的要求。

2.1 专项式 vs 常态化
专项式治理,个别是在包大小持续上升至某个值后,成立专门我的项目集中工夫治理。个别会有多团队多人员参加,同时会有明确的我的项目负责人,来制订严格且固定的里程碑。此时的 apk 包因为通过一段时间积攒,会存在较多以无用和冗余性能为代表的可瘦身项,绝对容易辨认和解决,因而个别瘦身见效快,当然专项完结后如果不足无效的可继续管控,包大小反弹简直是必然的。专项式治理的“精力内核”是指标优先,这当然没有任何问题,但在这个过程中,往往很容易漠视瘦身革新所带来的其它负面影响,例如不适当的近程化革新会带来用户体验受损、apk 构建过程中采纳大量“瘦身黑科技”导致打包耗时明显增加等。这外面的取舍和均衡之道,没有标准答案,只有综合判断“此时此地此景”后作出的抉择。

常态化治理,是指在长期的版本迭代过程中始终可能管制好增量,并在维持住以后包大小水位前提下实现“稳中有降”。个别在常态化治理阶段,头部问题曾经根本不存在,须要在业务性能和代码源头进行更全面、精密、深刻的剖析和思考,从而在版本迭代过程中逐渐“消化掉”能够瘦身的中央。在治理所需人力投入上,具备较低的整体管控投入,并造成团队、业务、性能的开发者自治场面。在治理节奏上,整体包瘦身指标调整周期较长,同时不再进行细粒度的瘦身里程碑制订,采纳绝对宽松和灵便的形式,把自主权给到具体负责的团队和开发者。在瘦身成果上,能够较好维持住以后水位而不产生反弹,甚至是迟缓升高。常态化治理的“精力内核”是体验优先,将包瘦身这件事“融入”到日常研发迭代过程,与稳定性(crash/bug 等)、性能(启动速度 / 页面切换 / 晦涩度等)一样,独特成为研发团队(同学)在业务需要和性能之外关注并考量的技术项。

在工夫、人力、节奏、成果、精力“内核”这五个维度上,二者的比照状况汇总如下:

image.png

常态化和专项式的关系并非简略的“优于”就可能说分明,首先二者具备演进关系,相似人类文明的“石器、青铜、农业、工业”等代际进化,常态化治理也是在生产力(剖析 & 瘦身技术)一直进步的状况下,促使生产关系(治理模式)等产生改革(嗯,这个比喻不肯定精确)。其次,二者有着不同的实用状况,专项式治理用于疾速升高包大小,而常态化治理用于低成本可继续维持或者缓降。如果 app 无论与同类竞品还是本身相比,都显著处于较高的包大小水位,那显然须要先通过专项治理将包大小疾速升高上来,而后再连接上常态化治理来取得“长治久安”;如果 app 曾经处于常态化治理模式,然而因为某些起因须要进一步疾速升高,那么就须要切换到专项式治理模式,达成指标后再持续回到常态化治理模式。

常态化治理绝对专项式治理,更须要当作一个系统化工程来对待,整体治理思路如下:

image.png

由专项式到常态化,首先要做的转变是将关注点从“事”转移到“人”:每一个 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 是不是曾经瘦到极限,还有哪些地方能够瘦身?”

剖析工具须要具备的另外两个重要价值是:领导和评判。这二者其实是一个事务的不同视角,即:对被动者给予领导,对被动者给予评判,在瘦身前用于领导,在瘦身后用于评判。

image.png

一个满足无效的度量、领导和评判需要的包大小剖析工具,该具备怎么的自我涵养,这就是本章所要探讨的内容以及会给出的答案。在优酷,这个 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)的。
依据下面的推导过程,咱们来进行提炼和总结:

image.png

当初,如果让你来答复以下几个问题,是不是就能够信手拈来,轻松惬意?

apk 为什么这么大,不同模块 / 业务 / 团队,别离奉献了多少?
每个团队 / 业务 / 模块,有哪些能够瘦身的中央,进行删除、优化、革新后,apk 能缩小多少?
在日常迭代中,以后版本绝对于上个版本,每个团队 / 业务 / 模块减少(缩小)大小是多少?
实际上,优酷自研包大小剖析工具 Franky,简直齐全实现了上述拆解后的需要。只有一点尚未做到:apk 中元素,目前还没有做到 100% 找到模块归属,在优酷 apk 中的剖析覆盖率是 99.8% ~ 99.9%(apk 100MB ~ 65MB)。

3.1.1 整体架构

Franky 次要由两局部组成:用于 application 工程的 gradle plugin,以及命令行(cli)剖析工具。此外,还额定依赖(非必须)两个内部数据:模块图谱数据,用于将模块大小,向上聚合为性能 / 业务 / 团队的大小;代码笼罩数据,造成可瘦身项剖析中的「代码 – 无用类」(SlimLady:类级别不插桩线上代码覆盖度统计框架[1])。整体架构如下图所示:

image.png

franky-plugin 的作用,是在 apk 构建过程中收集 apk 所有组成模块,以及模块中蕴含的各类元素,此外还蕴含类混同映射关系、无用资源剖析后果。这个剖析后果数据与 apk 文件,独特形成了命令行工具 franky(cli)的根底(必须)输出文件。接下来,执行 cli 命令进行最终的包大小剖析,产出具体的剖析报告。

纵观这套计划,可能会有一个疑难,为什么要蕴含一个构建插件,如果能通过一个命令行(cli)工具间接对 apk 进行剖析,应用更简略还能更具通用性,不是更好吗?这外面有一个十分要害的点在于,只有在 apk 构建过程中,才可能获取 apk 由哪些模块组成、每个模块又蕴含哪些元素,在 apk 构建实现后的 apk 文件中,这些信息曾经失落,所以构建插件必不可少。那如果是这样,为什么不把命令行工具的所有性能,都放在这个构建插件中来实现呢?这是一个好问题,目前的思考是这样的:尽量将构建插件做的比拟“薄”,这样能够缩小构建耗时,而将次要剖析性能放在独立工具,能够独立疾速迭代,而不必频繁在 app 工程中降级 plugin 版本。

3.1.2 关键技术

剖析工具看起来简略,然而为了获取实在大小,以及可能将 apk 中元素 100% 进行模块归属,在开发过程中还是会遇到不少辣手问题。

首先,利用于构建过程的 plugin 如何保障兼容性,并不是一个简略的问题。很多 Android Gradle Plugin 开发者不太器重兼容性,认为针对特定工程实现相干性能就高枕无忧,这里不深刻探讨此话题,间接给出 franky-plugin 思考并实现的构建环境兼容性,或者更可能对这个问题取得直观的认知:

image.png

接下来的外围艰难是,参加 apk 构建的原始元素,与最终 apk 元素之间,存在转换、新增、删除、不变这四种“变动”状况:

image.png

上图给出的是根本“变动”状况,还有一些非凡状况也须要思考,例如 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:类级别不插桩线上代码覆盖度统计框架[2],此处只是应用了前者的后果数据)。对于无用代码较多的模块,存在线上使用率低(或者齐全无应用)的问题,应该安顿下线或者应用 H5 等动态化形式实现。剖析报告中的示例后果如下:

image.png

第二项【代码】未混同类,是指因为 keep 规定存在,导致没有被混同的类。混同能够极大升高代码在 apk 中占用的大小,因而除了一些非凡应用场景,绝大部分类都能够进行混同。对于未混同类较多的模块,可能存在混同 keep 配置过于宽泛问题。剖析报告中的示例后果如下:

image.png

第三项【资源】超大,是指在 apk 中实在占用超过肯定阀值的资源(可配置,优酷始终应用的是 10KB)。超大资源,能够采纳近程化、从新设计更小的等效资源、如果是图片还能够采纳压缩率更高的图片格式(jpeg、webp)或者矢量图等形式来升高大小。剖析报告中的示例后果如下:

image.png

第四项【资源】无用,是指没有被间接援用的资源。从资源整体应用状况来看,一个资源可能在三个中央进行间接援用:java 代码,通过 R.resourceType.resourceName 形式援用(例如 R.string.app_name),或者通过资源 id 形式间接援用(例如 0x7fxxxxxx);清单文件 AndroidManifest.xml;其它资源。另外,资源还能够通过 Resoruces.getIdentifier 形式,通过传递资源名称和类型获取 id 值,运行时性能较差,因而官网并不举荐应用,须要留神的是,剖析工具对这种形式会呈现误检。对于无用资源,应该在确认未通过 Resoruces.getIdentifier 形式应用后,进行删除解决。剖析报告中的示例后果如下:

image.png

第五项【资源】多维度,是指蕴含大于两个配置的资源。这种资源会在不同配置下,存在多份数据(文件),一些非凡纬度须要做额定思考,例如:night、land & port。对于多维度资源,一些非必要的配置能够清理掉。剖析报告中的示例后果如下:

image.png

第六项【资源】无透明度 png 图片,是指 png 图片中蕴含了 alpha 通道,然而无相干数据,或者数据中的透明度值均为齐全不通明。对于这种类型的图片,个别能够通过应用不带透明度信息的其它图片格式(例如 jpeg),来升高大小。剖析后果中,曾经排除了.9 类型图片,在剖析报告中的示例后果如下:

image.png

第七项【资源】类似,是指资源值的类似程度较高。以后剖析只蕴含 file-base 类型资源(绝对的,值仅存在于 resources.arsc 中的资源,称为 value-base 类型资源)。对于非图片文件资源,仅筛选出齐全一样的资源(md5 统一,类似度为 1)。对于图片文件资源,额定计算了类似度,采纳 DHash 算法计算图片指纹,而后计算 hamming 间隔作为类似值。对于类似度为 1 的资源,内容完全一致,因而能够仅保留一份,对于类似度小于 1 的资源,因为有些图形简略的图片资源特色信息不显著,因而即便类似度较高,是否可相互代替的最终决策,依然须要依据所在业务场景进行人工判断。剖析报告中的示例后果如下:

image.png

第八项【so】不标准应用 STL,是指动态链接库 so 对 c ++ STL 库的不标准应用,包含两种状况:动静链接非对立 STL,官网倡议对立应用的 STL 为 libc++_shared.so,其它非对立 STL 包含 libgnustl_shared.so、libstlport_shared.so;动态链接 stl,通过革新为动静链接,能够实现较好的瘦身收益。剖析报告中的示例后果如下:

image.png

第九项【so】无用导出符号。动态链接库的导出符号 (exported symbol),是指在 so 内定义的对象、办法、全局变量,被设置为可被内部代码援用(导入)。无用导出符号,则是指在依赖这个 so 的其它 so 中(apk 范畴内),未找到任何援用,当然这里存在以下状况须要非凡解决:JNI 办法、通过 dlsym 形式加载并调用的符号。对于的确无用的导出符号,能够在编译 so 时设置为不导出。具体操作形式并不惟一,比拟倡议应用编译选项 -fvisibility=hidden,同时显示对须要导出符号减少 attribute ((visibility (“default”))) 标记这套计划来实现,这样新增符号默认不会导出,不至于呈现一段时间没人管,无用导出符号继续累积问题。剖析报告中的示例后果如下:

image.png

3.3 卡口能力建设
后者在研发迭代过程中,低成本维持常态化治理模式的要害之一,其承载的三个外围价值如下:

image.png

去核心,须要可能对 apk 大小进行适当颗粒度的精准拆分,用于卡口阈值、检测以及不通过时进行拦挡。

促前置。包大小和代码标准、bug 等单点问题不同,无奈通过就地批改来实现瘦身,往往须要通过“拆东墙补西墙”的形式,寻找存量可瘦身空间来补救新性能带来的增量。所以,只有足够前置能力留出更多工夫给瘦身治理,从而保障最终公布到用户手中的 apk 大小保持稳定。前置要求卡口必须在代码变更后“第一工夫”发挥作用,辨认到包大小变动,如果超过阈值则进行拦挡。这个“第一工夫”依据不同 app 迭代模式差别,选取适当的节点即可,例如优酷的一个版本迭代能够分为“提测 - 集成 - 灰度 - 公布”四个流程节点,那么就抉择提测和集成两个节点部署卡口。

低成本。卡口是一个比拟含糊的概念,1 百个工程师可能会有 1 百个对卡口具体机制的了解和设计,对于包大小卡口自身肯定要具备的是低成本保护,包含以下几个方面:

与研发流程联合。包大小卡口不是一套独立的流程,而是要融入到整个研发流程中,尽可能减少额定应用老本。
100% 笼罩。肯定是要对所有可能的增量起源,都可能笼罩到,不能存在某种形式,能够绕过卡口机制。一旦被绕过,会拉高后续的辨认 & 治理老本。
100% 自动化。这一点看起来有点像废话,卡口难道还能手动执行?现实情况是,有些场景下所谓的“卡口”,其实自动化水平较低,还须要不小的人工参加。
优酷在 2018 年就建设了包大小卡口能力,后续的演进和调整都是朝着更贴合上述外围价值的方向进行,直至 2021 年初才达到稳固无效的成熟状态。包大小卡口由剖析工具 franky、卡口能力平台、研发流程管控平台(CI/CD 平台)三局部组成,示意图如下:

image.png

研发在应用流程(CI/CD)平台进行提测和集成时,都首先须要触发 apk 构建,franky-plugin 作为包大小剖析工具在构建期的一款 gradle 插件,会收集数据并将后果输入到构建产物。接下来,流程平台中的卡口插件负责收集 apk 和 franky-plugin 生成的文件,并上传到卡口平台备用。

之后,流程平台会执行准入检测,其中包大小卡口检测会触发能力平台中的包大小剖析工作,通过调用 franky 对应的命令行工具,生成 json 格局的包大小剖析报告。通过解析剖析报告,并与事后设置的团队阈值和 Buffer 值进行比照,以此断定提测 / 集成单中蕴含模块所在的团队(1 个或多个)是否超限。流程平台中的准入检测在获取检测后果后,通过或者阻断提测 / 集成流程。如果卡口未通过,能够通过进行瘦身革新来使卡口通过,但这个别无奈在以后版本实现,这时能够申请带时限的 Buffer 来长期通过卡口,从而实现提测 / 集成。

瘦身技术
后面讲了很多治理模式相干内容,看起来可能有些形象,接下来会回到具体的瘦身技术,侧重点不在于深刻技术原理和实现细节(大多会给出参考链接),而是尝试将每一项瘦身技术的优缺点进行一次概括性解说和巡展,便于造成一个整体认知,在对具体 app 进行瘦身时,可能依据理论状况进行抉择和优先级安顿。
瘦身技术,顾名思义,就是指能够用于包瘦身的任何技术(计划),依照所需技术、失效阶段、影响范畴综合评判,将其划分为以下三种类型:

image.png

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。
【代码】D8。在 apk 构建过程中,java 代码须要经验由 jvm 字节码到 dalvik 字节码的转换解决,DX/D8 就是承当这个责任的工具。在优酷的具体实际中,由 DX 降级到 D8 后,包大小升高约 9.5%(9.7MB),因为额定对 dex 合并进行了优化,dex 数量升高,导致包大小收益呈现一次跃升,因而比官网给出的 Benchmark 收益 5% 要更高。
【代码】R 类合并。将所有 < 模块 package>.R 类移除,并将 java 代码中对前者的援用对立替换为.R 类,以此来升高包大小的一种技术手段。在优酷当前情况下(模块 800 多个,dex24MB),R 类裁剪能够缩小 80 万个 java 类 Field,带来近 5MB 包大小收益。因为每个 dex 中 Field 数量也受到 65536 限度,因而 Field 数量大幅缩小所带来的 dex 数量缩小,是瘦身收益的次要起源。进一步,能够把所有 java 代码中 R.. 的援用,也全副替换为对应 id 值,这样.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[3]是 Android 官网提供的无用资源裁剪性能,在 apk 构建时间接对无用资源进行删除。在 app 中除了通过 R.xxx.xxx/0xffxxxxxx 显式援用资源,还能够通过 Resources.getIdentifier 在参数中指定资源名称来援用,因为后者能够拼接甚至是动静下发字符串,因而会导致此类资源的援用关系无奈被精确获取,对此 ShrinkResources 提供了两种模式:严格模式、失常(默认)模式。严格模式仅思考显式援用关系,失常模式则会采纳“平安优先准则”,如果资源名称以任何 java 代码中的常量字符串为前缀,那么会被标记为疑似援用而无奈失去删除,还有其它几种“疑似性标记逻辑”不再列举。
【资源】多维度(备用)资源裁剪。Android 资源能够配置不同维度,从而在运行时灵便适配各种不同状况(语言、屏幕尺寸、屏幕横竖状态、os 版本等),这里的多维度(备用)资源裁剪,就是在 apk 构建期将不须要的维度裁剪掉,AndroidGradlePlugin 提供了对应性能,通过 android DSL 配置(resConfigs)[4]即可间接实现。自研代码个别只会蕴含须要用到的资源,而二、三方 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[5]及以上设施中装置。
【资源】去重。对于值雷同的不同名资源仅保留一份,删除反复资源并将所有援用到的中央替换为保留下来的资源。和 ShrinkResources 一样,在利用这项技术时仍然须要留神,通过 Resources.getIdentifier 以资源名称作为参数形式应用到的资源,不能进行去重解决。优酷已经应用到了这项技术,瘦身收益在 MB 级别,当初曾经通过对应的「单点瘦身」计划,在源码层面间接解决。
【资源】混同。和 java 代码 Proguard 混同相似,是通过缩短资源名称以及文件类型资源寄存门路,实现包瘦身的一种技术计划。AndResGuard[6]是实现这项瘦身技术的一套开源框架,性能绝对成熟且齐备,起初也有些一些同类框架在根底的资源名称缩短之上,又进一步对 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 一直引入等等,都会带来新的时机和挑战。

参考文档:
[1]https://baijiahao.baidu.com/s…
[2]https://baijiahao.baidu.com/s…
[3]https://developer.android.com…
[4]https://developer.android.com…
[5]https://developer.android.com…
[6]https://github.com/shwenzhang…

基于开源我的项目 Serverless Devs 的创意利用

点击这里,查看详情。

原文链接:http://click.aliyun.com/m/100…
本文为阿里云原创内容,未经容许不得转载。

正文完
 0