工程健康治理

工程化

  • 定义: 为项目搭建一系列的周边工具,打通 CI / CD 流程
  • 解决问题: 解决标准操作流程(SOP 标准作业程序) 的问题

组件化

  • 定义:将项目打碎并拆分成若干个组件组成的项目,以面向组件的方式进行开发。
  • 解决的问题:解决工程业务复杂的问题。

组件化达到的效果是:

1. 化整为零,各自独立;确保每一部分是正确的,整体就是正确的。解决代码复用并是组件化要解决的主要问题,组件化要解决的问题是将复杂的大工程拆分成很多简单的小工程,且小工程之间能够互相协作;工程使用了 Cocoapods 也不意味着已完成组件化。组件化的是指代码可以独立编译,可以独立测试。
 
2. ## 好的架构没有 Common 没有 Core,也不应该有大组件的存在.

有 Common / Core 时,意味着有那么一部分代码,其职责是不明确的。不明确职责的代码会造成代码未来难以维护,因此不是一个好的架构。
另外每人对 Common / Core 的认知不同,很可能会使得 Common / Core 变成一个公共垃圾堆,出现 “既然放哪里都不合适,那我就放 Common / Core 吧” 的情况。该垃圾堆持续增长,成为一座垃圾山,导致 Common / Core 未来无法维护。

为什么不允许存在大组件

原因是大部分功能的开发,其实仅需要大组件的其中一个功能,但为了实现该功能,却需要引入一个大组件。这种情况不仅导致代码维护复杂,还会导致这个小组件编译时长不合理地增多。此时更合理的做法应该是将大组件打散成若干个小组件,其他组件需要啥,就只依赖它需要的那部分。我们认为一个大组件应该是由若干个小组件组成,而不是一个大组件由很多功能的代码拼成。每个组件实现了什么功能,就是什么组件。

组件化的实现方案解析:

1.基于预注册:分为 URL 注册 / Protocol 注册

目前注册类方案存在管理注册时序、大量注册实例造成无用内存消耗、大量注册代码造成的时间损耗等问题,对于这些问题,我们可以使用各种“补丁逻辑”(例如直接将注册 URL 注入 mach_O 文件等)来解决
  
2.基于中间件 Target - Action  Category(OC)和Extension(Swift) Runtime实现

组件化的实现可能会遇到的问题:

1.Argument List Too Long

Argument List Too Long 指的是 Xcode 代码编译基本完成后,在执行工程 Build Phases 中的 Run Script 时 / 编译时突然停止。

原因:Cocoapods 为 Pod 生成编译参数后,会写入到环境变量。此时若使用 Pod 构造的组件达到一定量级,会导致写入环境变量过多,继而导致环境变量总长度超过操作系统限制(即 26w 个字符),最终导致命令停止。

解决方案:环境变量中主要包含三个长度较长的内容,即 Header Search Path、Library Search Path 及 ModuleMap Path,所以关键是对这些信息进行合并。

1.合并 Header Search Path、Library Search Path 到一个文件夹:建立专用文件目录,将相关数据迁移至该专用目录,然后在 Xcode 中设定该目录。

2.合并 ModuleMap 到一个文件:需要注意的是,Xcode 要求我们提供 ModuleMap 文件路径(而非文件夹路径),所以这里关键在于如何合并 ModuleMap 文件。由于 ModuleMap 编译时才产生,所以得物在编译 Pre Action 时,将其他 ModuleMap 内容合并至当前 DerivedData 路径下的 ModuleMap,同时设定 Xcode 读取的 ModuleMap 文件路径。

容器化

  • 定义:利用拆分的组件,在快速满足业务需求的同时,尽量少地提升项目复杂度,同时以面向构件的方式进行开发。
  • 解决的问题:解决组件复用性的问题。

包体积大小治理

这两块内容主要有两款内容 ==资源== 和 ==代码==, 治理的方法主要就是这两块。

资源

  • 资源压缩

    • 给定资源无损压缩 (主要针对于美术给定资源未无损压缩的情况)
    • 80% 有损压缩 (此情况需要排除例外,例如部分特殊图片需要展示较高的效果,保留方式一的压缩方式)
    • 重复资源手动合并(初级)
    • 重复资源合并自动合并。利用脚本,在编译结束时,针对资源计算 MD5,然后确认相同MD5资源的数量,若存在多个,则移除相同的冗余资源,同时为了让大家都使用相同的资源,对 imageNamed: 等方法进行封装 / Hook,将传入的资源名字处理成 MD5,然后再进行资源的搜索。这样可以解决多组件使用相同资源的问题 ==(高级)==

  • 资源下发

    • 自建资源下发平台,每个版本对应一个资源包。eg:1.0.0 的版本,对应 1.0.0 的资源包;2.0.0 的版本,对应 2.0.0 的资源包。客户端通过“资源管理器”访问资源,如果资源没下载好,则访问 CDN 进行下载(非重要图片资源处理)。对于比较大的动画 / AR 资源,若未下载好,则使用图片资源进行业务逻辑的降级兜底。
    • 资源增量下发,只下载新版本新增资源。(对于资源下发平台,得物此块获得了大概 3 - 4 倍的提升)==(进阶)==

代码

  • 三方库裁剪

    • 针对只需要三方库一小部分功能的场景,对三方库进行定制化缩减,如移除不需要的功能
    • 移出类似三方库,指定使用某一个
    • 抽离稳定三方库细分功能做为组件
    • LinkMap解析可以让你看到各个库占的体积大小,定位到各个库的占用体积问题,从而提出缩减安装包大小的解决方案。

    分析工具 LinkMapParser


  • 代码治理

    • 无用代码扫描
    • 重复代码合并抽离成中间组件,各组件对中间组件进行依赖
    • 长依赖组件:部分项目中会存在组件依赖链过长的情况,如 A -> B -> C -> D(此处 "->" 代表依赖状态)。这样会导致使用 A 组件的时候,必须同时引入 B、C、D 组件。我们认为长依赖组件的出现是不合理的,是没有合理区分纵向依赖与横向依赖导致的。针对这部分内容的处理,需要调整依赖方式
      • 纵向依赖:直接依赖代码,通过 Pod Dependency 进行依赖。
      • 横向依赖:通过组件调度的方式进行间接依赖,无需直接依赖代码。
    • 长编译耗时组件:当新组件仅需使用大组件的小功能时,直接依赖了大组件,会导致新组件开发时,开发、编译成本变大。针对这部分内容,需要将大组件再次进行拆分成若干小组件进行处理。

  • Crash治理

    • Crash 管理系统(例如Bugly 等)手工确认 Crash 后分给具体的业务线,同时人工跟进问题修复情况。==(初级)==
    • 半自动化使用脚本爬 Bugly 信息(包括 Crash Stack 信息),然后自动识别业务线并自动落表。==(中级)==
    • 搭建自建 Crash平台,做自动化收集、解析、告警,同时实现自动跟进修复情况。==(高级)==

  • 启动流程治理

    • step1 启动流程分组:将代码按业务线分块并标注,用以确定每条业务线所占的时长,随后可根据该指标治理部分业务线大幅增长的不合理情况
    • step2 代码插桩diff:编译时,进行代码插桩。然后将新旧包代码插桩数据对比,当新增代码调用时,能够及时发现调用链的变化,用于确保调用链必要且合理。

  • 工程化

    • 围绕组件化的基础设施,需要注意的是,工程化的基础设施,需要随着组件化、容器化阶段不停调整。并不是意味进入组件化后,工程化就不用继续,而是进入组件化后,工作重点主要放在组件化,但仍需投入精力调整优化相关的基础设施。
      • 组件创建脚本、组件工具:解决创建、管理组件的问题。
      • 组件发版工具:解决组件发版的问题。
      • 二进制调试工具:解决二进制组件调试问题。
      • 组件上游依赖查询工具:解决组件依赖查询问题。
      • 编译成功节点切换工具:解决“出包难”的问题。
      • 裙带源码组件切换工具:解决 ARC 不对齐的问题

  • 组件管理

    在 Podfile 中== 不使用 == 定义版本号的方式,采用索引库的方式来解决组件频繁更新的问题。根据不同的开发环境建立不同的索引库,基于上述考虑,前主要使用索引做组件管理,且固定以下命名规范:
    [图片上传失败...(image-2a3667-1648176146458)]

    • 开发环境:使用 dev 索引库,规定组件发布新版本时,仅变更版本号的第一位,就像 A,从 1.0.0 变为 2.0.0。
    • 沙盒环境:使用 test 索引库,规定组件发布新版本时,仅变更版本号的第二位,就像 B,从 1.0.0 变为 1.1.0。
    • 灰度环境:使用 gray 索引库,规定组件发布新版本时,仅变更版本号的第三位,就像 C,从 1.0.0变为 1.0.1。
    • 现网环境:使用 release 索引库,规定组件不能发布新版本。

    不同环境使用不同的 Git 仓库,且 Podfile 中固定了每个组件当前环境的仓库地址(所以切换环境时,需要调整 Podfile 中的 Source,并处理不同环境 git upstream 的合并)。仓库环境的拆分,避免了新版本开发影响到已有版本逻辑。


  • 二进制

    • 单工程独立编译制作

      • 优点:组件发版时完成二进制制作;可靠性高。
      • 缺点:组件制作二进制时长取决于组件规模,大组件制作仍旧耗时;存在大量组件需要做二进制,单光算编译时长,1400个组件 需要 5 天制作;出现测试包体积变大。
    • 全工程编译产物制作:利用编译缓存,从 Xcode 编译缓存 DerivedData 中取出组件

      • 优点:每 50min 完成 1400+ 二进制制作。
      • 缺点:只能分批制作(1h / 次);存在 ARC 不对齐的情况。
      • ARC 不对齐:A 组件已经是二进制(在编译时加入 ARC),B 组件使用源码编译,此时再次编译(编译器会为 B 会添加 ARC,但不会对 A 组件进行处理,有可能导致 ARC 不对齐引发内存被提前释放,导致 EXC_BAD_ACCESS Crash)。为了解决该问题,在做二进制时,需要对新发版的组件做裙带组件源码切换:将其上游一级、下游一级的组件变成源码依赖而非二进制依赖
      • 增量制作: 将一个组件的上一级和下一级依赖的组件变为源码参与编译,解决ARC 不对齐问题

  • 持续交付

    • Xcode Server 定时打包 :CI 机定时打包

      • 能够避免编译错误长时间不被解决的问题。
      • 不保证编译成功,且每轮编译时长较长,相隔 1h 才能获得编译结果;但 CI 机编译成功不代表开发端编译一定能成功
    • 记录编译成功节点并提供脚本用以切换到该分支

      • 优点:能够避免编译错误长时间不被解决的问题;能够最大限度保证开发端代码编译成功。
      • 缺点:编译成功节点落后近 1h;由于开发的组件一定是源码,若使用二进制编译,会造成 ARC 不对齐引起的各种偶发问题。
    • 多机车轮打包: 使用大量机器持续构建不同环境的包

      • 优点:能够及时发现编译错误,每轮 10 min;能够最大限度保证开发端代码编译成功。
      • 缺点:需要较多机器资源。
    • 构建失败处理

      • 定期输出构建信息, 如遇到失败,则自动提醒引发失败的工程师(提交者)。
      • 标记处理状态。

--- 整理至得物 得物iOS工程演进之路

你可能感兴趣的:(工程健康治理)