关于android:向工程腐化开炮-治理思路全解

57次阅读

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

作者:刘天宇 (谦风)

系列文章回顾《向工程腐化开炮 | proguard 治理》《向工程腐化开炮 | manifest 治理》《向工程腐化开炮:Java 代码治理》《向工程腐化开炮|资源治理》《向工程腐化开炮|动态链接库 so 治理》。本文为系列文章最初一篇文章,聚焦于整体治理思路,方案设计,以及背地的思考与取舍。

工程质量是任何一个产品,可能疾速、高效、稳固地进行业务性能迭代的根底,也是给用户带来良好产品应用体验不可漠视的因素,更是任何一位优良工程师的冀望和卓越谋求。而工程腐化,却是任何一个大型工程都不得不面对的问题,其宽泛而细碎,暗藏在不易被觉察的“角落”,对工程方方面面均有所影响。

工程腐化与工程自身相伴相生,贯通工程生命周期的每一阶段,工夫、人、代码、流程、规定,任一因素的变动都会导致腐化产生,从觉察到修补、系统性剖析到应答计划制订、再到坦然承受与常态化可继续治理,本文对此逐个道来。

源起

在一个工程趋于成熟之前,腐化问题深深暗藏于代码中,个别会明显降低研发效率,然而引发的线上问题却并不频繁,因而很容易当成单点问题进行修复。然而随着腐化水平加剧,同一类型问题呈现的频率越来越高,才逐步嗅到淡淡的“腐化滋味”,也因而才有了后续一系列的剖析、方案设计、工具 & 平台研发,以及治理实际。咱们来看上面这张图,可能很多研发同学会有切身感受:

1.1 嗅到腐化滋味

笔者在 Android 架构畛域有多年教训,间接负责或者间接参加了稳定性、启动性能、包瘦身、工程效力、新版本 os 适配等多个方向,随着各治理项的不断深入以及工夫的推移,遇到过各种各样的问题,例如:抵触资源导致即便代码无变动,屡次构建后的 apk 中也会呈现资源值不统一,最终引发线上问题;java 代码批改导致不兼容调用,最终引发线上 java 异样;线程随便应用,不足对立管控,一方面性能堪忧,另一方面过多线程数量超过某些设施的自定义限度,从而引发 OOM 异样;无用代码 & 资源 & 功能模块,导致包体积继续减少;apk 构建耗时越来越长,重大影响研发效率;这样的例子,能够举出几十项,此处不再一一赘述。

当尝试以一个整体的视角去对待和思考这些问题时,才发现背地暗藏着的弱小敌人——工程腐化。工程腐化,简略来说就是无用 / 冗余 / 不合理代码的继续沉积,从而更容易出问题,出了问题更难定位,而且迭代越快腐化越快,即便无任何迭代,随着新版本 os 上市、隐衷合规监管态势日趋严格等等外部环境变动,都会导致存量代码呈现问题。接下来,深刻到研发迭代过程,看看腐化自何而来。

1.2 剖析腐化产生

后面讲到有很多因素会导致工程腐化产生,但最源头因素只有两个:工夫和人。工夫意味着工程外部环境的变动,例如:指标设施中 os 版本号会一直降级、研发工具链、IDE 等迭代更新,一份静止不动的工程代码,会随着工夫的推移缓缓腐化。相比工夫对工程腐化带来的慢变性影响,由人主导的疾速工程迭代,才是工程疾速腐化的最大起源。既然如此,咱们就重点看看一个 app 版本迭代 & 交付过程中,都有哪些角色参加,其外围诉求别离是什么,工程腐化又如何在这样的“土壤”中一直积攒。

上图是一个典型的挪动端 app 版本迭代 & 交付过程,对于大型 app 和研发团队,可能每个角色都有专门的岗位和人来负责,而对于小型 app 和研发团队,则可能 1 人分饰多个角色:

  • 产品和设计,负责性能、UI、交互设计,关怀的是创意和性能给用户带来的价值,以及视觉和交互的晦涩炫酷;
  • 研发和测试,在接到产品需要以及设计稿后,负责代码开发、实现、成果 & 品质保障,研发和测试同学,往往心愿需要和设计一旦确定后不要总发生变化,此外还心愿尽可能复用现有的逻辑和性能,对一直推倒重做式的需要和设计有着人造的“抗拒”,最初还心愿能多点工夫,再多点工夫,来保障代码品质和验收成果;
  • PMO 和 PTM 负责版本节奏、管控公布过程,关怀整体的需要吞吐量,以及过程和线上品质;
  • 渠道和经营负责将新版本 app,通过各种渠道准时交付到用户手中,并通过层出不穷的经营伎俩,来获取新用户以及用户对 app 性能应用的全面、快速增长;
  • 在后面这个过程中,平安和法务须要保障 app 的安全漏洞失去及时解决,隐衷合规等相干事项不呈现风险性问题。

最终,用户获取或者降级到最新版本 app,其外围诉求是这个新版本 app“好用吗?好玩吗?”。随之而来的除了用户,还有各方监管 & 检测机构,在获取到新版本 app 后,会查看依据以后法律、法规,仔细检查 app 应用过程中是否存在“违规”景象。

在这样一个 app 版本交付过程中,能够看到各角色的侧重点并不相同,同时所有角色的诉求最终都要通过代码来承载。工程腐化间接来源于开发者的代码生产流动,开发者自身的志愿、技能和教训,的确会极大影响代码品质,但古代企业级 app 的性能之简单,绝不可能所有参加其中的开发者,都可能对 app 所有代码一目了然,因而这种对工程或者说代码把握的局部性,可能是工程腐化产生的更重要因素。

1.3 拆解腐化问题

剖析完腐化产生,咱们再进一步对 Android 工程腐化项,进行更细粒度的拆解。从 Android 工程蕴含所有“代码”的类型来看,能够分为以下五种:

其中,工程配置是指在 apk 构建过程中应用到的相干配置,配置内容自身并不会进入到最终 apk,这种工程配置腐化,次要是影响工程自身的复杂度,甚至是构建过程耗时,例如大量的 proguard 配置项。其它四种类型,manifest、java 代码、资源、动态链接库 so,也是组成 apk 的所有可能“元素”,本身或者相互之间都可能存在各种各样的腐化问题,间接导致 apk 稳定性、性能、包大小、UI& 性能异样、隐衷合规危险等等,或者进步这些问题呈现的可能性。

在理论工具开发和治理实际中,也正是依照上述类型实现分而治之。

应答计划

在实现腐化产生剖析,以及按类型拆解后,接下来须要制订无效的应答计划。

首先,必须明确并时刻牢记的领导准则是:“用正确的形式,做正确的事,无论简略还是艰难”。“正确的事”往往比拟容易界定,并达成共识,然而“用正确的形式”却有些艰难,因为有时候“不正确的形式”意味着捷径,能够疾速获得指标成绩,例如:假如咱们须要将 app 中所有线程应用切换到对立线程池实现,有两种形式能够实现,一种是间接应用构建时 aop 技术对线程调用代码间接进行替换,另一种是建设非对立线程池应用的检测 & 卡口机制,在保障无效防控增量代码状况下,逐渐批改存量代码。显然,第一种形式能够疾速达成指标,然而却会减少 apk 构建耗时,同时如果这个 aop 处理过程自身,一旦呈现问题导致替换不胜利,或者替换过程异样终止导致字节码替换不残缺,那么又是另一种“工程腐化”。第二种形式无奈疾速达成指标,然而能够无效止住腐化趋势,并逐渐消化存量问题,尽管卡口自身须要日常审批评估,并且存量代码清理也并非欲速不达,但代码源头上的间接改过,才是解决工程腐化问题的”正确形式“。

2.1 人 vs 流程

工程腐化来自于人在版本迭代流程中,对工程代码进行的不合理变更,因而,工程腐化治理须要围绕“人”和“流程”来进行。

对于人这个因素,业界曾经有十分成熟无效的做法,例如:进行代码 review、制订代码标准、定制 IDE 的 Lint 规定、继续进行技术培训等,这些都可能进步开发者的代码设计和编码程度,从而在源头缩小腐化代码产生。此外,可能耳濡目染的进步研发团队整体工程质量和素养,对工程质量带来更为全面的晋升。然而,这种形式有一些问题,也绝不能漠视:参加到一个工程的开发者,其技术认知、程度、理解能力并不统一,这些标准 / 规定的执行成果难以保障,带来的潜在老本可能也会很高。

对于工程腐化来讲,齐全依附这些围绕人的计划,不确定性十分高,而腐化的防治须要一种确定性的机制来“守好这道门”,同时,防治自身须要做到较低的老本,因而,咱们将重点放在流程下面。流程具备主观、固定、有保障的个性,一方面以全面的 apk 检测剖析技术为外围,对腐化项精准定位并在流程要害节点部署卡口,及时感知,有问题就地解决,从而实现零新增。另一方面,对于存量腐化项,提供多样化的辅助工具,升高整改危险和老本,提高效率。冰冻三尺,非一日之寒,因而冻结的过程,也不可能搞成大跃进式的清理模式,而是须要在尽量不影响日常研发流动前提下逐渐迭代,最终实现存量清零。

围绕人和流程的这些应答计划,并不是二选一而应该是相辅相成,前者重在从源头全面缩小腐化项产生,后者重在无差别的阻止其中可能无效检测的腐化项进入到最终 apk,同时加强开发者防腐化意识,并促成代码 Review、代码标准等无效执行,从而造成良性循环。

2.2 剖析工具

作为外围的 apk 检测剖析技术,到底蕴含哪些具体的能力呢?来看上面这张图:

上图是以后检测剖析技术汇总,能够分为冗余抵触、要害配置、援用关系、辅助提效四个类型。前三种类型间接对应具体的腐化项,最初一种则是帮忙开发者在日常研发过程中,更好的定位和剖析问题。对于每一项检测能力,此处先不详述,在“向工程腐化开炮”系列文章中,别离与具体实际相结合进行了相干解说。

2.3 卡口体系

这些检测能力,是如何与流程相结合的呢,来看上面这个流程卡口示意图:

对于开发 / 测试同学,在提测、集成、灰度 / 正式版本公布这些要害节点,都须要进行 apk 构建,同时,会主动触发曾经部署好的各项检测剖析。如果是本地打包,检测不通过,会间接构建失败,并在失败起因中,给出相干信息;如果是 CI/CD 平台打包,卡口后果会以平台页面模式出现;无论哪种模式,都会中断流程,待研发同学修复问题后,再持续进行。这样,就实现了腐化问题的及时感知,就地批改。

以平台模式为例,每次提交测试 / 集成时,apk 构建都会触发卡口检测,如果有卡口项未通过则阻断流程。卡口后果示例如下:

在具备了这样一套机能力和机制后,咱们接下来看看,如何对各类腐化问题进行治理和防控。首先,先明确“模块”这个概念,对工程腐化与治理的影响,以及工具建设和治理实际。

模块治理

一个残缺 apk 的产生,能够认为是一个“拼积木”的过程;每一块积木,都可能蕴含 java 代码 / 资源、Android 资源、AndroidManifest 文件、动态链接库 so、proguard 配置,将这些积木依照肯定规定拼接,同类元素混合 & 压缩,即成为最终的 apk 文件。上述这些“积木”,用更贴近技术的术语来讲,就是模块。模块为性能复用提供可能,也为并行研发模式提供根底,一般来讲,越大型和简单的工程,其模块化水平也越高。

工程腐化的产生,实质是由性能的复杂度以及代码变更导致,模块化自身尽管会带来肯定的腐化问题,但更重要的是,为工程腐化问题治理提供便当。试想一下,一个由上百人划分为十多个团队,独特参加迭代的 app,如果都在一个 app 工程中开发代码,先不说如何解决代码合作,一旦产生腐化问题,如何进行调配自身就是一个极大的挑战。在事实工程畛域,模块化水平个别(失常的工程抉择)都会随着性能和开发人员的减少而一直进步,在这个前提下,工程腐化治理首先要做的事件,就是要明确晓得每一个具体的腐化问题,来自哪几个模块,这是将问题进行散发和解决的前提。接下来,首先会给出模块的分类,而后讲述针对模块开发的几个“辅助剖析能力”,以及在此之上的治理实际。

3.1 模块分类

app 工程中以内部依赖模式引入的 jar/aar,以及与 app 工程平行的 subproject,可能是日常研发过程中接触最多的模块类型,除此之外,Andriod 原生还反对其它类型模块。从 apk 构建视角来看,模块的残缺分类图如下:

上图展现了 5 种模块类型,以及几个维度:在 apk 构建过程中是否须要经验源码编译、是否在 maven 仓库中存在,以及可能存在的依赖关系。上面别离进行解说:

  • app-project 有且仅有 1 个,用于生成 apk,蕴含源代码,因而须要源码编译。能够依赖 sub-project、local jar、flat aar、external module;
  • sub-project 能够有 0 或多个,个别与 app-project 平行,同样蕴含源代码,能够依赖 sub-project、local jar、external module;
  • local jar 不能独自存在,java 代码曾经以编译后的 class 字节码模式存在,不能依赖其它类型模块;
  • flat aar 是 Android 原生提供的一种引入非 maven 中 aar 的形式,同样无需源码编译,并且不能依赖其它类型模块;
  • external module,即内部依赖模块,无需源码编译,能够依赖其它内部模块,依赖信息位于 maven 仓库对应 pom 文件中。

一般来讲,一个 app 的“出世”,是从一个 app-project 工程开始的:所有代码、资源都写在此工程中,当然也会以内部模块模式引入(依赖)一些二、三方库;随着 app 承载性能减少,复杂度随之回升,此时也很可能会有更多的开发者退出进来,继续迭代一段时间后,可能会迎来第一次模块化“改革”:将通用性能拆分为多个 sub-project;开发人员的增多,会引发代码合作老本进步,此时可能须要从单个代码仓库拆分为多个,便于并行化开发,此时迎来第二次模块化“改革”:代码仓库拆分,以及更细粒度的模块拆分,研发并行水平持续进步。最终,会演进为模块化的究极状态:app-project 成为用于打包 apk 的一个“壳子”,简直所有代码全副拆分到独自模块和仓库,在 app-project 中以内部模块模式对其进行依赖(引入),研发高度并行化。

很多大型 app,根本都实现了上述这样的演进过程,同时也引发了新的问题。接下来,就来逐个讲述在模块这个维度,研发了哪些工具,进行了哪些治理。

3.2 辅助剖析能力

辅助剖析能力,次要是站在 apk 残缺构建角度,为开发同学提供模块及其依赖信息,用于解决各种日常问题,例如:

  • “我更新了一个模块的版本号,为什么 apk 中的代码还是旧的?”—— 查看本次 apk 构建,指标模块最终应用的版本号是多少,如果没有更新,那么必定会呈现这个问题。
  • “我删除了模块,为什么 apk 中还有相干代码 / 资源?”—— 查看本次 apk 构建,指标模块是否参加到 apk 构建过程,是 app 工程间接依赖引入,还是其它模块间接依赖引入,疾速定位起因。
  • “我在一个模块工程中,应用了另一个模块中的办法,然而在 apk 中却找不到此办法,是什么起因?”—— 查看本次 apk 构建,依赖的另一个模块版本号是多少,降级指标工程中对此模块依赖的版本号,从新编译指标工程,看是否办法已被删除,转移或者签名有变动。

接下来,别离对每项辅助剖析能力进行简略介绍。

内部依赖模块列表

内部依赖模块列表,对立输入所有参加到本次 apk 构建的内部依赖模块,及其版本号、类型。示例后果:

com.youku.arch:testlib:0.1-SNAPSHOT@aar
com.youku.arch:testlib2:0.3@aar

被依赖关系检测

在 apk 构建过程中,有一些内部依赖模块是通过间接依赖(没有在 app 工程中间接申明依赖)引入进来的,这个间接依赖关系,存在于 maven 仓库中模块对应的 POM 文件。通过被依赖关系检测性能,能够不便的找到一个模块,被哪些其它模块所间接依赖,用于进行模块下线,或者归属关系断定(依据依赖关系,判断模块属于哪个下层业务)。示例剖析后果:

com.youku.android:y-core
|-- [provided] com.youku.android:ct-ad
|-- [compile] com.youku.android:catl
|-- [runtime] com.youku.android:MtRec

com.tb.android:z_dev
|-- [compile] com.tb.android:zcore

留神,这里的剖析后果,是被依赖关系。在这个例子中,com.youku.android:ct-ad 模块以 provided 形式,申明了依赖 com.youku.android:y-core 模块;com.youku.android:catl 模块以 compile 形式,申明了依赖 com.youku.android:y-core 模块;其它内容以此类推。其中,依赖类型个别包含以下几种:

  • compile。此类型依赖,如果不额定增加 exclude 设置,会导致模块被打入 apk;
  • provided。此类型依赖,不会导致模块被打入 apk;
  • runtime。此类型依赖,不会导致模块被打入 apk。

当然,模块在公布到 maven 仓库时,能够定制 pom 文件内容,所以如果模块公布时,并未正确的将工程中对其它模块的依赖关系写入到 pom 中,那么上述检测后果,也会存在对应的错误信息,例如:漏掉实在依赖模块、依赖类型与理论不符、蕴含多余依赖模块等。

不匹配依赖关系检测

在模块化开发模式下,各个模块独立开发,并最终参加 apk 构建,这会导致很难感知到其依赖的模块进行了降级:模块本人在进行构建时,应用的还是对应依赖模块的旧版本,所以能够编译通过,然而在 apk 编译时,很可能其所依赖的模块曾经进行了版本号降级,从而导致一些不匹配援用状况产生。不匹配依赖关系检测,正是为了便于各模块开发同学,清晰的把握模块编译时依赖的其它模块版本号,与 apk 编译时这些模块应用的版本号之间的差别,从而及时在模块工程中进行依赖模块版本号的降级操作。示例剖析后果:

com.youku.android:YTask
|-- com.youku.android:BFra:1.0.0-SNAPSHOT ==> 1.0.0.44
|-- com.youku.android:BUIKit:20190617-SNAPSHOT ==> 1.0.1.66
|-- com.youku.android:YUI:1.4.2.16-SNAPSHOT ==> 1.4.10

在上述示例中,YTask 模块在编译时,依赖的 BFra 模块是 1.0.0-SNAPSHOT 版本,而在 apk 构建时应用的 BFra 模块是 1.0.0.44 版本,其它以此类推。此外,还提供额定性能,将所有内部依赖模块的 pom 文件,对立输入到 apk 构建产物文件中,便于集中查看和定位问题。

3.3 治理实际

在上述几项辅助剖析能力的根底上,有两种状况会对构建出的 apk 带来不确定性隐患,因而,也成为模块腐化的间接治理指标。

snapshot 版本号

在 apk 构建开始阶段,间接从 maven 仓库下载内部依赖模块对应版本号的 jar/aar 文件,参加后续构建过程。其中,SNAPSHOT 版本号因为能够随时更新 jar/aar 到 maven 仓库,而在 app 公布版本构建时,并不心愿这种状况产生,这会带来各种难以预期的线上危险。因而 apk 构建过程,是否存在 SNAPSHOT 版本号的内部依赖模块,须要被严格管控住。

为了,研发了 snapshot 版本号检测性能,筛选出参加到 apk 构建过程所有版本号为 snapshot 的内部模块。示例内容如下:

com.youku.arch:testlib:0.1-SNAPSHOT
com.youku.arch:testlib2:0.2-SNAPSHOT

进一步,在 app 版本迭代要害节点,例如:集成、灰度 / 正式版本公布,利用此项检测能力造成卡口。优酷在几年前,就曾经以本地卡口模式(apk 构建失败)上线此性能,并在 2021 年将此卡口融入到整个卡口体系,成为其中一个卡口项,累计拦挡 7 次,无效避免 snapshot 版本模块引入到 apk 构建过程中。

snapshot 依赖

开发阶段,为了不便模块间联结调试,通常会将依赖的模块版本批改为 SNAPSHOT,在实现联结调试后的正式版本打包过程中,如果没有将依赖模块的 SNAPSHOT 版本号批改回正式版本,而这个工夫窗口内,依赖模块的 SNAPSHOT 版本一旦有更新,会导致模块正式版本编译时依赖非预期代码,最终导致 apk 运行时呈现各种不兼容问题,例如:API 不兼容(类、变量、办法签名不匹配)、常量不统一(常量在模块编译时,会进行常量开展)。

snapshot 依赖检测性能,正是为此而生,在检测后果中列出每个模块依赖的 snapshot 版本号模块,以及 apk 构建时此模块对应的版本号。示例内容如下:

com.youku.android:YHPage:1.9.35.5
|-- com.ali.android:VCommon:20210309-SNAPSHOT ==> 11.1.6.4
|-- com.youku.android:YRes:20210309-SNAPSHOT ==> 1.0.44.2

com.youku.android:OUtil:1.0.4.11
|-- com.youku.android:OService:20210105-SNAPSHOT ==> 1.3.8.2

作为腐化治理项,优酷在 2021 年初上线此性能,过后有 200 多个模块在 pom 文件中存在 snapshot 模块依赖,过后对立增加到了白名单,在接下来版本迭代过程中逐渐清理,截止目前已清理近 40%,效果显著。在同一时间于 app 版本迭代要害节点,造成了对应流程卡口,近一年工夫累计拦挡 25 次,无效避免由此导致的线上危险问题产生。

其它治理实际

上述模块相干腐化治理,只是与工程腐化这场持久战的前哨。针对后面工程腐化的元素级分类拆解,开拓了以下“五大战场”,能够返回查看详情(点击跳转):

  • proguard 配置
  • manifest
  • java 代码
  • 资源
  • 动态链接库 so

还能做些什么

在优酷近两年的工程腐化实际中,失去了很多研发同学的反对,他们怀抱匠心、激情与勇气,及时解决呈现的新增问题,一点一点的去消化存量技术债,长期的保持和致力独特换来目前工程腐化问题的全面显著升高。“用正确的形式,做正确的事,无论简略还是艰难”,这既是优酷进行工程腐化解决方案设计和治理实际时,所动摇遵循的准则,也是本系列文章想要传达出来的技术理念。

目前可能通过工具检测到的具体腐化问题,加起来不过 20 余项,绝对于工程腐化的冰山,毫不夸大的说这真的只是一角儿。况且,这里所给出的应答计划,也仅仅可能解决其中一类问题,面对那些极度简单,甚至牵一发而动全身的腐化问题,尚短少无效解决方案。面对工程腐化,还有很长的路要走,还有很多事件能够并且须要去做,向工程腐化开炮,是一种间接而切中要害去解决问题的态度,积跬步行千里,与诸君共勉。

关注【阿里巴巴挪动技术】微信公众号,每周 3 篇挪动技术实际 & 干货给你思考!

正文完
 0