“ 业务开发遇到环境问题越来越多,重大影响开发效率,有些外表看似打包问题,背地却是工程架构的腐化。”
背景
近年来,iOS工程复杂度高的负面影响逐步裸露,很多同学都受到了iOS打包慢和打包简单的“残害”,业务开发效率受到很大影响。我记得已经有个同学跟我诉苦,他把几个模块打包后集成到主工程,这个过程中每个步骤都有打包失败,总共花了大半天工夫。
Alibaba.com是跨境B类电商业务,2012年开始开发iOS客户端。为了撑持业务倒退,2016年进行组件化革新,从繁多工程架构演变模块化架构。随着业务和无线技术的倒退,客户端曾经从小型模块化工程演变为一个巨无霸工程。团队一共建设了100多个自保护模块,包含业务模块、架构设施、Hybrid容器、Flutter容器、动态化技术、根底中间件等能力。外表上工程架构正在有序地演进,但外部曾经乱象丛生。模块关系凌乱,循环依赖和反向依赖行为越来越多。大量模块不合乎LLVM Module规范,spec文件不全、头文件援用不标准。因为工程不标准,Cocoapods无奈降级,只能应用1.2和1.5旧版本,技术上落后了3年以上。
为了彻底解决问题,进步业务开发体验,阿里巴巴ICBU端架构组对iOS工程架构进行全面地治理。我也写下一篇文章记录本人的思考,欢送有趣味的同学领导交换。
Steve Mcconnell 《Code Complete》:“软件的首要技术使命:治理复杂度。”
架构腐化会产生哪些问题?
问题一:模块打包复杂度高
工程环境混淆
2016年Alibaba客户端组件化做的并不彻底,很多模块只是模式上的拆散,实际上还存在反向依赖和循环依赖问题。到了2017年,团队想做Framework化,发现模块独自打包编译不过。于是,为了模块编译通过,咱们开发兼容脚本,将所有framwwork和头文件都增加到工程searchPath里,并且让模块间接读取同步主工程Profile里所有依赖。自从有了兼容逻辑,spec文件不写依赖形容也能编译得过,于是再也没人保护spec文件,跨模块的头文件援用也越写越乱。
环境不兼容&模块构建失败
因为存在循环依赖、头文件不标准等问题,模块编译脚本加了许多workaround逻辑,兼容头文件索引。这导致模块Cocoapods环境无奈降级,始终停留在1.2版本。而随着中间件和社区swift技术越来越多,主工程Podfile用了cocoapod 1.5的新语法。环境开始不兼容。同时,模块解析主工程Podfile时,无奈辨认cocoapod 1.5的新语法,模块构建失败。
每年节约了90人日的开发资源
模块打包失败后,开发须要剖析日志,排查打包失败起因,若剖析不进去则须要找架构组反对。一个模块打包失败,会始终卡住需要不能集成,会阻塞测试或其余开发工作。
依据开发反馈的状况,预计均匀一次模块打包失败要耗费2个小时的研发资源。据统计,Q1期间,模块打包失败总数高达200屡次,其中70%的打包失败是因为简单度过高导致的。每一次打包失败节约2个小时,相当于每年节约了90人日的研发资源。
Robert Martin 《Clean Architecture》:“不论你们多敬业,加多少班,在面对烂零碎时,你依然会举步维艰,因为你的大部分精力不是在应答开发需要,而是在应答凌乱。”
问题二:主工程打包慢
如果模块不标准,又须要援用swift中间件,无奈独立动态库,只能以源码模式集成到主工程。这导致主工程打包时须要编译大量源码,均匀打包工夫比手淘、优酷等工程慢12分钟。需要提测、集成、修复bug、排查问题时都须要进行主工程打包,打包慢会阻塞开发和测试的工作。某一次双周迭代打包了70次,节约了14个小时。
问题三:工程环境不稳固
Cocoapods环境不能降级,只能应用1.2和1.5的旧版本。但旧版本环境没人保护,环境极其软弱,比方有人公布了一个不非法的spec,Pod Update就会挂掉。因为模块不标准,源码开发时会呈现各种莫名其妙的编译问题。业务开发和调试效率会很低,节约大量的工夫。
问题四:Swift开发举步维艰
近几年swift遍及,iOS社区和团体swift中间件越来越多。然而,Swift模块严格遵守“LLVM Modules”标准,不容许循环依赖、内部依赖要显示申明、头文件援用要采纳尖括号,否则就会呈现“could not build module xxx”、“No such module”等谬误。高标准的要求下,咱们的工程开发引入Swift举步维艰。尽管咱们本人能够不应用Swift,但团体和三方的中间件Swift化的趋势是不可逆的。
近两年,Alibaba.com的工程引入许多Swift中间件,同时也自主研发了许多swift组件,这也彻底引爆了研发效率的问题。相干模块频繁打包异样。不标准问题盘根错节,常常解决完一个编译器谬误,又呈现另一个谬误,子子孙孙无奈穷尽。最初零碎开始呈现各种不可控危险。
此外咱们大部分模块都不合乎LLVM Modules标准。如果业务需要应用Swift或援用到Swift中间件,就要花大量工夫去解决适配问题。依据麻利迭代的数据,需要A打算10人日,理论耗费20人日,需要B打算6人日,理论耗费10人日。
复杂度的好转到肯定水平,肯定进入有诸多unknown unknown的水平
问题五:历史代码清理艰难
最近几年很多旧业务曾经下线或革新。但因为模块之间耦合重大,许多旧代码始终不敢删,这也导致包大小继续收缩。
架构腐化治理的艰难与策略
影响范围广,治理难推动
2020年,我在iOS技术栈发动了架构治理我的项目,动员各个业务线的iOS开发一起治理,却陷入了困局。一方面,业务开发没有投入资源。另一方面,许多业务模块之间调用关系凌乱,治理危险高,大家都不敢轻易动。
数据化剖析,自顶向下推动
iOS工程的凌乱曾经重大影响了业务倒退,大家工夫都节约在解决编译打包问题上。各业务的iOS开发同学都被困扰,许多开始反馈因为打包困难严重影响开发效率。
为此,我开始全面梳理研发流程的数据。一方面,我统计了模块构建失败数据,主工程打包的耗时,而后再联合其余客户端的数据进行比照;另一方面,我对业务开发做访谈,从用户的角度理解资源节约的数据,补充研发平台中无奈统计到的环节。最初,胜利将工程凌乱对研发效率的负面影响量化为具体的数据。
有了数据分析后果,就有了推动的抓手,能够自顶向下推动架构治理。
解决方案
纵观全局,理清模块依赖关系
第一个难点是模块的关系不清晰。模块形容文件里依赖列表都是空的,模块之间的关系就像一团毛线。
模块的关系不清晰,治理我的项目就无奈拆解,老本也估算不进去。因而要先纵观全局,剖析整体的模块依赖关系。
我开发了一个工具进行剖析。首现查找模块的所有文件,应用正则匹配找到它import的内部头文件,失去内部援用的头文件汇合。而后搜寻主工程的Pods目录,匹配头文件所属的内部模块,最初聚合失去残缺的模块依赖树。
下一步是视觉化,视觉化当前能够更直观地查看模块关系的复杂度,不便制订治理打算。我应用了Dot language来形容模块关系,能够主动生成整个工程的依赖关系图,也能够生成某个特定模块的依赖关系图。
依赖倒置、分层治理
第二个难点是治理的依赖条件简单。
模块治理胜利的规范是整个依赖树的所有模块都没有循环依赖,并且都合乎LLVM Module标准。比方治理业务模块A,模块A的依赖树里有一个模块C,模块C存在循环依赖或不合乎Module标准,A模块打包时就会报异样.而Cocoapod和XCode每次只报一个异样,不能剖析整个依赖树所有的问题。
咱们工程本人保护的模块有130多个,三方库和中间件模块200多个。业务模块除了本身依赖,还有许多间接依赖,依赖树非常复杂。这种状况下,间接治理业务模块复杂度极高,治理过程也会很凌乱。
上图的示例中,模块C、模块I、模块G是关系简单的核心模块。比方“模块I”间接依赖了30个内部模块,间接依赖100多个模块,它间接耦合关系有5个循环,间接耦合关系15+个循环。如果间接治理“模块I”,须要解耦15个循环关系,将100多个模块进行Module化革新。依照这样的思路治理,批改逻辑极其简单,很可能治理到一半就进行不上来。
为了解决这个困局,我对模块进行分层和分类。划分的根底逻辑有3个:
- 越是底层的模块依赖关系越简略;
- 没有循环依赖的模块更容易治理;
- 治理实现的模块能够被疏忽。
依照这个思路,我先梳理分明模块所属的档次,而后自底层逐层向上治理。当底层模块都治理完,依赖多的模块累赘也会大大降低。当底层的循环依赖解耦实现,下层的模块就不必解决的间接循环依赖。
最初应用四象限分析法,将模块分为4个组,1根底模块无循环依赖、2根底模块有循环依赖、3业务模块无循环依赖、4业务模块有循环依赖,按程序治理每一组。
自动化修复
第三个难点是代码改变量大。模块治理面临许多子问题,“模块spec文件的依赖形容不全”、“umbralla头文件不缺失”、“public头文件援用不标准”、“循环依赖解耦”。仅仅修复“模块spec文件的依赖形容不全”就很艰难。
补全依赖的办法是查找所有源文件的import形容“(import <xxxFramework/xxx.h)”,统计以来的所有framework。再基于framework名称反向查找所属的模块。另外有很多import格局不标准,有些是间接援用文件名(import “xxx.h”),有些是门路形式援用(import <xxx/xxx/xxx.h>),遇到这种不标准的援用,还须要全局搜寻能力找到属于哪个模块。举个例子,模块A的dependence形容是空的,但实际上它依赖了20几个模块。模块A有60多个源文件,每个源文件import援用均匀是10行,总共600行援用代码。如果人工剖析这600行代码,预计得花一天工夫。这还只是批改其中一个问题,还不包含“umbralla头文件不缺失”、“public头文件援用不标准”、“循环依赖解耦”。
因而,纯人工治理基本行不通,必须通过自动化的形式提高效率。于是我开发了一个架构治理引擎,能够用来剖析模块依赖关系,也能够修复spec依赖形容不全、主动生成umbralla头文件、批改不标准头文件援用等等。自动化的修复工具能够笼罩95%的代码改变量,开发只负责批改路由、服务API、代码迁徙、模块拆分合并等变动较大的逻辑改变。
架构治理引擎不仅能够做架构治理,它还能做为团队管理工具,比方剖析git仓库活跃度,批量设置CodeReview规定,记录研发过程的日志。
上面这段代码应用了ruby语言和cocoapods-core框架,次要性能是剖析模块import代码,修复模块的podspec的依赖。
require 'cocoapods'require 'cocoapods-core'require 'xcodeproj'def DependencesAnalyser.main(contextHelper, projectToolPath, moduleName, allModuleNames) # 1修复import格局 iOSProjectDir = contextHelper.projectDir podDir = contextHelper.podDir iOSProjectName = contextHelper.projectName # 读取source_files门路 sourceDir = contextHelper.sourceDir if sourceDir.nil? puts '[error]依赖修复失败,找不到正确的sourceDir' return nil end # 1 读取源文件目录下的所有.h和.m文件的门路 allheadPaths = getSourceHeaderPath(sourceDir) # 2 遍历所有源文件,读取文件的每一行,正则匹配出所有import的代码行 # 2.2 如果是import "" 或者 import <xx.h> 规定援用的,解析出依赖的头文件 importHeaders = parseHeaderNameFromQuotationImport(allheadPaths) # 2.1 如果是import <xx/xx.h> 规定援用的间接截断出framework名 dependences = parseFrameworkNameFromAngleBracketsImport(allheadPaths) # 3 如果是import "" 规定援用的,判断援用的头文件是否存在Pod目录下,如果存在记录所在Pod的Framework名 # 3.1 读取主工程Pod文件目录下所有依赖库的.h文件的门路 dependencesFromQuatationImport = findFrameNameFromQuatationImportHeader(podDir, importHeaders) dependences = dependences + dependencesFromQuatationImport filtedDependences = filterDepencences(dependences, projectToolPath, moduleName, allModuleNames) # 4 读取podspec,批改dependence后,输入新的podspec文件 modify_spec_file(filtedDependences, contextHelper) # 5 输入依赖关系文件 return filtedDependences end
架构和业务单干治理
第四个难点是解耦波及大量业务逻辑。很多代码是业务的分支逻辑,重构后很难测试,如果不全面验证很容易出线上故障。
解耦波及大量业务逻辑,升高危险最好的办法是交给业务开发来批改。因而架构组牵头了横向的iOS工程治理我的项目,架构组提供治理计划和工具,业务开发负责业务逻辑解耦。业务解耦采纳了4种形式,路由Scheme、服务化API、公共组件下沉、模块合并。
举几个典型的解耦场景:
场景一, 产品模块里有一个子业务是产品举荐,订单模块也须要用到,于是订单模块会反向依赖产品模块,造成循环关系。这种场景解耦的形式是从产品模块中拆分出根底组件,订单模块依赖根底组件。
场景二, 产品模块跳转订单模块时应用产品的model作为API的入参,订单模块为了援用产品的model,反向依赖了产品模块。这种场景解耦的形式是应用路由URL Scheme协定,将model转化为URL中query的入参。
长效保障机制
进行架构治理后,模块的循环依赖和modula标准等问题失去解决,但今后可能呈现二次腐化。咱们当然不心愿隔一段时间又要从新治理,于是从架构设计和研发流程的卡口动手,优化架构和流程,杜绝后续的二次腐化。
架构优化
- 系统性进行模块定义和划分,减少模块逻辑的内聚性,防止一个需要须要同时开发多个模块。收敛模块数量,缩小模块的保护老本;
- ICBU业务模块最终都会集成到主客,版本仲裁对立在主工程能够缩小复杂度,防止模块的版本申明呈现抵触。模块依赖形容只申明模块名,不申明版本号,打包时同步主工程的模块版本作为版本仲裁。
收敛模块工程
如果模块各自保护构建工程,长期保护必然导致构建配置有很大差别。一方面,这样不能对立降级构建配置,架构治理和技术升级的老本会很高;另一方面,模块如果呈现构建问题,排查老本也会变高。
因而,咱们建设了打包脚本,每次打包动静生成模块工程。模块不再保护独立工程,构建配置对立收敛到podspec文件。
模块打包时,动态创建模块的构建工程
require 'cocoapods'require 'cocoapods-core'require 'xcodeproj'require 'rubygems'project_creater = ProjectCreater.new(ContextHelper.tempProjectPath, ContextHelper.projectName)project_creater.transformrequire 'pathname'class ProjectCreater def initialize(root, name) @project_path = Pathname.new(root).realpath @project_name = name end def transform puts "ProjectCreater-开始" prepare puts "ProjectCreater-开始重命名" rename puts "ProjectCreater-实现" end private def prepare xcodeproj_path = @project_path.join("#{@project_name}.xcodeproj").to_s if File.exist?(xcodeproj_path) `rm -rf #{xcodeproj_path}` end end def rename Dir.glob(File.join(@project_path.join("Podfile").to_s)).each do |file| content = File.read file content = content.gsub(/POD_NAME/, @project_name) File.open(file, 'w') { |f| f << content } end Dir.glob(@project_path.join('PROJECT.xcodeproj').to_s + '/**/*').each do |name| next if Dir.exist? name if File.extname(name) == '.xcuserstate' next end text = File.read name text = text.gsub("PROJECT",@project_name) File.open(name, "w") { |file| file.puts text } end scheme_path = @project_path.join("PROJECT.xcodeproj/xcshareddata/xcschemes/").to_s File.rename(scheme_path + "PROJECT.xcscheme", scheme_path + @project_name + ".xcscheme") File.rename(@project_path.join("PROJECT.xcodeproj").to_s, @project_path.join(@project_name + ".xcodeproj").to_s) endend
CocoaPod和Xcode编译卡口
- 主工程CocoaPods环境降级到1.9.1版本,update时会检测循环依赖;
- 去掉兼容的Header search Path逻辑,模块必须应用标准的头文件援用形式能力编译通过;
- 开启XCode modular编译查看,如果模块的头文件援用不标准会编译不过。
Devops构建卡口
- 严格走集成单流程,集成单须要编译通过能力集成;
- 在构建流程中退出动态扫描插件,检测模块标准。
总结
架构腐化就像“流感病毒”,它的负面影响很难被感知和量化。
对于技术团队而言,要防止架构腐化,技术团队要对技术有更高的敬畏,相比于等大火蔓延再就抢救,咱们应该对及时灭火的人给与更多实质性的反对和激励。
对于架构师而言,须要架构师能纯熟开发工具。面对简单的度架构问题,首现要进行全面剖析,对系统问题进行拆解,找到复杂度最低的治理门路,并无意识地寻找数据撑持,取得团队的反对。
最初,从架构治理的角度。客户端工程是人造中心化架构,它很容易因为环境抵触导致编译问题。因而,咱们设计组件化架构时,要确保模块的环境齐全独立,避免出现中心化架构。架构治理不是起点,治理实现后要有避免腐化的机制,避免出现二次腐化。
参考
- 《Clean Architecture》https://book.douban.com/subje...
- 《Code Complete》https://book.douban.com/subje...
- DOT Language https://graphviz.org/doc/info...
- LLVM Module https://clang.llvm.org/docs/M...
咱们招聘啦!
Alibaba.com 是寰球最大的B类国际化电商平台,长期招牌端架构、直播、短视频、IM、电商等畛域的技术人才。如果你对iOS、Android、Flutter等挪动技术充满热情,欢送退出Alibaba.com客户端研发团队,能够Base杭州和深圳。
简历投至形式
分割邮箱:blacktea.hw@alibaba-inc.com
微信号:blackteachinese
《淘宝客户端诊断体系降级实战》
《Cube 技术解读 | 支付宝新一代动态化技术架构与选型综述》
关注咱们,每周 3 篇挪动干货&实际给你思考!