关于ios:Alibaba-iOS-工程架构腐化治理实践

2次阅读

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

“业务开发遇到环境问题越来越多,重大影响开发效率,有些外表看似打包问题,背地却是工程架构的腐化。”

背景

近年来,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 个:

  1. 越是底层的模块依赖关系越简略;
  2. 没有循环依赖的模块更容易治理;
  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.transform

require '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)
    end
end

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 篇挪动干货 & 实际给你思考!

正文完
 0