作者:刘天宇(谦风)
系列文章回顾《向工程腐化开炮 | 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 篇挪动技术实际&干货给你思考!