查看原文
其他

网易严选APP工程架构演进

严选技术 严选技术产品团队 2023-04-08





互联网技术发展到今天,移动互联网仍然是一个重要的战略核心,APP也仍然是绝大多数互联网企业用户获客留存的核心渠道。


1. 前言
互联网技术发展到今天,移动互联网仍然是一个重要的战略核心,APP也仍然是绝大多数互联网企业用户获客留存的核心渠道。
根据2021年移动应用趋势报告[1],移动应用程序的受欢迎程度仍在持续增长,应用安装量同比增长31%,用户参与度(按会话数衡量)增长4.5%。Apple应用商店中有超过222万个应用程序[2],Google Play商店中有348万个应用程序。
网易严选历经多年的沉淀发展,以优质的商品类目和层出不穷的优惠,致力于为消费者不断带来高品质灵感好物,与此同时,能够不断的为用户带来极致的购物体验,也是研发同学的追求。目前,APP端代码量到了百万级别,移动端的开发小伙伴在技术和业务的多重压力下,也在不断推进着移动端的架构演进。
2. 什么是APP架构-想做什么?
架构指导设计思想,处于不同的阶段,不同的业务形态下会有不同的实现。在阅读本文之前,我们可以先思考一下什么是APP架构?它有什么标准?指导设计的基本原则是什么?
2.1 APP架构的标准
一个好的架构,不仅仅能保障完成产品所需要的功能、提供流畅的用户体验,同时也要考虑到可扩展性和可维护性。架构设计的本质是对系统进行有序化重构,从而降低“软件熵”;架构设计的重要目的是降低“因需求增加而导致的软件熵增加,从而导致的成本增加”。在架构演进的过程,应该满足以下基本原则:
  • 合适优于领先
  • 演进优于快成
  • 简单优于复杂
2.2 APP架构的指导思想
架构提供思想,实现灵活变通。APP作为最接近用户的终端,会更加关注业务不断复杂下的稳定性、业务场景多变下的复用性、业务不断丰富下的扩展性。好的架构源于不停地衍变,通常我们可以把APP端架构核心思想简化为:移动端架构 = 模块化(业务架构) + 分层(技术架构)。

2.2.1 分层

从技术思维去思考,通过分层治理,大致如下图所示:

纵向层级之间使用接口通信,单向依赖,遵循依赖倒置原则,拒绝跨层访问。横向层通信统一使用路由或者事件总线访问,每个业务模块定义好自己的消息代理和外部消息处理事件。

2.2.2 模块化

业务思维驱动,处理边界模糊问题,由不同的组件组合成模块,实现环境隔离,做到职责清晰。

用分属同一功能/业务的代码进行隔离(分装)成独立的模块,保障可以独立运行。以页面、功能或其他不同粒度划分程度不同的模块,位于业务层,模块间通过接口调用,降低模块间的耦合,由之前的主应用与模块耦合,变为主应用与接口耦合,接口与模块耦合。模块内自洽完整,实现高内聚低耦合
3. 现状分析-为什么做?
严选APP端,由于早期业务变化快,App迭代频繁,采取“短平快”的方式开发,写代码的方式比较粗放,导致绝大数的代码都往主(单)工程直接堆,导致了代码内各子系统之间耦合严重,边界模糊,“你中有我,我中有你”的情况随处可见。
近来,工程复杂度高的负面影响逐渐暴露,很多同学都受到了各种方式的“摧残”,历史遗留的代码不敢改,动一处而牵全身,需求合并时代码覆盖与冲突,发版时代码的安全与工程结构的稳定,打包速度等等,导致业务开发效率受到很大影响。主要存在的问题如下:
3.1 工程文件耦合

3.1.1 主工程代码堆积

严选iOS端工程主要使用cocoapods管理,虽然拆分出了一些组件库,主工程的代码量占比仍然高达70%。在开发中,需求与功能代码不断堆积,部分通过group、folder组织代码,依然隔绝不了相互之间过度耦合和依赖。同时也带来了代码覆盖与冲突等问题。

  • 代码文件主要位于主工程(~70%),复用的能力弱、体验差
  • 工程的结构比较简单,层级没有细分,从图上可以看出来大致的结构只分了2层(主工程/Pod库);
  • 缺少引用隔离,主工程的代码没有域隔离,导致了不同的业务模块之间,存在大量的相互交叉的引用;
  • pod组件拆分的颗粒度比较大,依赖不清晰,大多数的二方库,都只能在主工程运行,podspec编写不规范

3.1.2 历史代码清理困难

存在旧业务已经下线或改造,但因为模块之间耦合严重,许多旧代码一直不敢删,人员的变动,历史逻辑缺少维护,这也导致包大小持续膨胀。

3.1.3 集中式管理

严选工程代码从一开始就使用了cocoapods进行管理,托管在gitlab。使用git管理了全部的代码,这种方案在项目前期有着很好的优势,不需要具体的开发同学关注环境配置,也不需要侧重关注分层设计,聚焦于业务开发,同时又可以在一个仓库中关注到所有的变更信息。当业务逐渐丰富,工程复杂度逐渐增加的时候,风险大于收益,问题就会不断地暴露。
  • 没有约束的修改工程任意一处代码,导致工程的不稳定和不可控
  • 在Pods下直接修改依赖库,导致后续功能代码缺失的风险,涉及业务功能的完整性;
  • 代码集中式管理,使得仓库体积非线性增长,单仓远程仓库3.xG起,本地冗余就更多。
3.2 工程环境复杂

3.2.1 业务耦合

严选iOS端单工程包含多Target多配置,主工程中容纳了多业务形态,主APP、App Clip、Widget、Today View等。
  • 主工程管理方式粗糙,Group与Folder交叉使用,缺少边缘界线,看似分组的代码,实际耦合;
  • 主APP与App Clip业务形态互斥,代码层面又存在相互的引用,使得主工程代码、组件库等冗余各种的判断和校验;
  • 业务模块与功能组件边界不清晰,位于底层的功能代码里面耦合了业务逻辑。

3.2.2 复杂度高

团队内使用pod管理,由于存在循环依赖、反向依赖、重复依赖、头文件不规范等问题,为了主工程正常编译通过,配置searchPath,使得某些拆分的组件库spec文件里面依赖不全也能正常编译通过。以此spec文件缺少维护,跨模块的头文件引用也越写越乱,缺少规范。

从上图可以看出来,在组件化方面,之前做的不彻底,很多拆分出去的模块只是形式上的分离,子业务错综复杂,纵不分层,横不解耦。模块的关系不清晰,部分模块描述文件里依赖列表空的或者缺失的,部分模块关系复杂,既缺少上下分层隔离,又相互循环交叉引用。
4. 演进方案-如何做?
4.1 工程优化

4.1.1 纵观全局,理清模块依赖关系

模块的关系不清晰,治理项目就无法拆解,成本时间也估算不出来。因此要先纵观全局,分析整体的模块依赖关系。从上图可以看出,模块除了自身依赖,还有许多间接依赖,依赖树非常复杂。直接治理复杂度极高,治理过程也会很混乱。为了解决这个困局,需要对模块进行分层和分类。划分的基础逻辑如下:
  1. 越是底层的模块依赖关系越简单;
  2. 没有循环依赖、依赖倒置的模块更容易治理;
  3. 治理完成的模块可以先忽略。
按照这个思路,先梳理清楚模块所属的层次,然后自底层逐层向上治理。当底层模块都治理完,依赖多的模块负担也会大大降低。当底层的循环依赖解耦完成,上层的模块就不用处理的间接循环依赖。

最后通过拓扑排序对所有模块进行排序并结合分层分类,按照上图的顺序优先治理依赖清晰且属于基础能力的模块,最后逐级向上分层治理,处理模块之间的依赖不清晰问题,同时解决依赖倒置、循环依赖等问题。

4.1.2 模块化、颗粒度解耦

屋子打扫干净了才能请客。对于工程也是一样,处理了已知的模块的问题,我们可以着手解决在主工程耦合着,没有分离出来的组件/模块。
大部分的代码在主工程中耦合,此时切记不能侧重速成!业务模块先大颗粒度的分离解耦,再细化提取公共组件。功能、UI模块可以分层解耦,逐渐下沉。

如上图所以,在模块/组件第一阶段,我们通过制定合理的工程架构目标,沉淀了更多的基础组件和模块,也为我们后续分批演进优化奠定基础。

4.1.3 提高安全性,保障项目完整

严选的主工程包含了多种业务形态和配置,主APP 与App Clip虽然在业务上互斥,在工程代码层面,Clip里面大部分的功能和能力复用了主APP,但是由于没有明确的代码隔离环境,开发同学在编码时很容易改动到影响的代码而不能自知。同时,由于企业包不支持Clip,所以构建内部测试包的时候需要移除Clip,在工程配置变动的时候,极有可能影响到主工程的配置,导致上线后该业务形态缺失。所以,无论是模块还是项目配置,一些核心稳定的功能,不希望存在不可控修改的风险。需要有效的检测和及时反馈方案来保证其安全和完整性:
  1. 核心稳定的基础能力库,可考虑二进制化或部分二进制化;
  2. CI流程编排触发校验与反馈提醒,保证项目工程的完整性;
  3. 子仓库发布触发lint,单独编译校验,确保所有的subspec的依赖是正确。
下面是使用ruby和xcodebuild实现二进制化的核心代码:
require 'xcodeproj'require 'cocoapods'
def build_xcframework_framework(frameworks=[],xcframework_path) argvs = '' frameworks.map do |framework| if framework.framework_path && framework.framework_path != '' argvs = "#{argvs} -framework #{framework.framework_path}" end
if framework.framework_symbols && framework.framework_symbols != '' argvs = "#{argvs} -debug-symbols #{framework.framework_symbols}" end    end
command = "xcodebuild -create-xcframework #{argvs} -output #{xcframework_path}" execute_command(command)end
def execute_command(command) output = `#{command}`.lines.to_a if $?.exitstatus != 0 puts Pod::UI::BuildFailedReport.report(command, output) Process.exit endend
4.2 腐效治理

4.2.1 主工程文件剥离

对当前的工程代码进行拆解,以壳工程作为演进目标,限定主工程的业务范围:
  1. 注册与分发全局性事件;
  2. 初始化,注册并绑定各个模块;
  3. 全局化配置与管理,全局性决议事件的收拢。
工程抽象模型如下:

4.2.2 无用/重复资源删除

项目的不断演进,业务场景的不断迭代或者改造下架,会沉积大量废弃的代码文件和图片等资源,这些在业务场景下不会再被用到,仍然会被编译打包进安装包中占用其体积,而且会持续的腐化项目。
对于工程内的代码文件资源,可以使用以下方案:
    • 基于Mach-O检测:WBBlades
    • 基于源码检测:Fui
    • 使用工具检测:AppCode
    对于工程内的图片等媒体资源,可以使用以下方案:
      • LSUnusedResources:一款 GUI工具,通过正则组合匹配,检索效率还不错,需要注意的是大部分团队都会有自己的样式资源库,来读取对应的资源,此时需要自己适配特殊逻辑。
      • FengNiao:一款终端工具,和LSUnusedResources一样提供了正则组合校验,优点在于可以与CI集成,在关键节点编排时,在Run Script中触发检查。
      项目中可能会遇到这种情况,在仓库内(git tracking),但是不在工程内(workspace)的文件,通常是以下原因:
        • ‍遇到一些冲突或者覆盖的文件,在解决冲突的时候,处理的不规范。
        • 在移除文件时,只移除了引用,没有真的移除。
        解决方案:因为Xcode工程在通过workspace组织管理的时候,所有的文件信息都存储在project.pbxproj的section中,我们可以diff出当前的文件信息与section的实际引用,移除冗余的文件信息,严选工程已经配置在CI流程中,作为卡口。

        4.2.3 渐进化修复、自动化检查

        在制定方案的过程中,需要充分考虑架构的改动对业务的影响以及能给业务带来的收益。既不能影响业务的稳定性也不能影响业务的正常迭代。所以涉及到核心的功能模块,我们要制定完善的方案,渐进化修复:
        1. 制定详细的方案,请团队相关同学Code Review;
        2. 内部BugBash,快速验证快速修复;
        3. 使用AB分流验证,逐步确认核心功能稳定;
        4. 建立容错降级机制,保障业务流程健康,可快速回退。
        开发流程引入了CR机制,所有的代码合并都需要走MR,并邀请组内其他同学进行 Discussion。虽然组内制定并宣贯了基础规范,但是通用的基础问题靠人工不但费事费力,而且执行下来不稳定,毕竟1000个人就有1000个哈姆雷特。人工Review可以拔高上限,但是不能为规范的下限兜底,所以引入了自动化检查并强制提醒的流程:

        1. 代码规范和质量检查分析

        2. 模块依赖检查与分析

        3. 无用、重复代码分析

        4. 工程隐患提醒与检查

        如下图所示:

        4.3 长效保障机制
        工程优化和腐效治理后,模块的依赖、modula规范等问题得到解决,但今后可能出现二次腐化。我们当然不希望隔一段时间又要重新治理,于是从架构设计和研发流程的卡口入手,优化架构和流程,杜绝后续的二次腐化。

        4.3.1 CI辅助分析并设置卡口

        工程师的时间比机器值钱,毫无疑问,那些繁琐重复、不容易被直观发现又可能被忽略的问题,都应该交给机器去执行。

        如上图所示,严选工程构建了一套标准的CI准入流程,从开发、测试、提测、到发布,在关键节点触发对应CI的流程。在各个阶段提供对应的分析报告,辅助分析工程质量与模块依赖等核心点,并设立卡口,只有符合规范的代码才能准入合并,杜绝腐效的持续蔓延。

        4.3.2 基建能力搭建

        在架构改进的过程中,要保证不能影响业务的正常迭代,而本身团队人员有限,那么最大化提效,沉淀一套我们自己的脚手架工具和基建体系就是最好的保障。

        如上图所示,我们在构建组件的时候,建立标准模板库并配置对应的CI辅助能力,在后续的开发、调试、发布提供有力的支撑。
        5. 方案落地-最佳实践
        APP端架构设计,本身属于老生常谈的话题。在严选APP工程架构的演进中,我们沉淀了一套适合我们自己的技术解决方案,也在过程中完善了脚手架工具等基础设施的搭建。目前,严选APP架构图如下:

        对比架构演进之前,我们再次进行分析:
        • 工程文件耦合
        1. 架构优化之前,主工程代码比例大约在70%。目前经过治理和优化当前大约占比45%,降低了大约25%。剩余的主要是复杂的业务模块,也就是我们上面规划的第四批和部分第三批的模块,因为前置,已经积累了对应的解决方案,后续在业务稳定的前提下,持续进行优化即可。
        2. 在优化模块依赖和分层治理中,梳理清了模块的关系,没有使用的模块陆续下掉,并且引入了CI的检测,设立卡口在保证不会出现新的无用冗余代码的前提下,每次需求的改动,CI会主动提醒当前模块存在的历史遗留问题,告知对应的开发者进行优化。移除无用文件:340+,接口去重合并优化:80+。
        • 工程环境复杂
        1. 罗马不是一天建成的,系统建设也不是一蹴而就的事情。在架构演进过程中,一方面:不断的向组内分享我们在做什么,为什么这样做,培养模块化/组件化解耦思维,一方面:对历史代码不断地改造,拆分出组件库并进行分层治理,逐渐降低工程复杂度。二进制化库:11个,新增组件库:35个,编译优化速度提升:6%。
        2. 把不同的业务形态代码进行隔离,剥离Clip与主APP代码与依赖关系,通过subspec管理,依赖关系更清晰,涉及的改动对比清晰且可控。
        6. 未来规划
        在沉淀了一系列的解决方案,以及搭建了相应的基建能力后,我们还是存在优化的上升空间,以下是基于严选工程实践的一些思考和未来的规划。
        6.1 APP构建平台
        随着我们基础的组件的不断完善,组件化、容器化不断的落地实践,我们希望能更好的提升业务团队的开发效率。目前组内除了严选主站外,也在维护这一些其他的业务,比如:VIP APP、内部OA等等。我们在去年的新项目中研发落地过程中,享受到了部分组件化/模块化的便利与优势,帮助我们快速的搭建一套基础能力的框架,提高了研发同学的效率。由于严选有些模块具有很强的定制性,我们希望接下来能够剥离开来,进一步的扩大战果,能够基于基建能力快速的搭建复刻完整的APP能力,这是我们的一个重点方向。
        6.2 Swift合理引入的探索
        近几年swift普及,越来越多的新功能和新特性都只能在swift中受益。然而,Swift模块严格遵守“LLVM Modules”规范,不允许循环依赖、外部依赖要显示声明、标准Module化等,否则就会出现“could not build module xxx”、“No such module”等错误。高标准的要求下,我们的工程开发引入Swift 依然有诸多问题等着我们去解决。随着团队新鲜血液的注入,越来越多的小伙伴基于之前的swift经验,都想基于此提高研发的效率的同时又有技术的增长,这也是我们后面会关注的一个方向。
        7. 结语
        工程项目的组件化、容器化是一个系统性、持续性的工作。涉及工程架构的改造、CI/CD 研发工具链的支撑、本地研发工具链的支撑,业务架构的设计优化,需要从各个方面综合考虑成本和收益。
        架构是一个循序渐进的过程,没有最好的架构,只有更好的架构,在架构演进的过程中,我们需要充分考虑架构的改动对业务的影响以及能给业务带来的收益。好的架构一定是能帮助业务节省时间,保证质量的。与此同时,我们在架构改进的过程中,要保证不能影响业务的正常迭代,所以向前兼容且避免大面积冲突也是很重要的事情。
        对于软件工程来说,这世上没有银弹。这对于架构而言其实也非常的适用。移动技术的不断发展和业务的不断变化,推动了移动APP架构的不断演进。架构没有真正的好坏之分,只要适用于自己的业务,就是好的架构。

        引用资料
        • [1] https://www.adjust.com/blog/mobile-app-trends-2021/
        • [2] https://www.statista.com/statistics/276623/number-of-apps-available-in-leading-app-stores/
        • https://zh.wikipedia.org/zh-cn/%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84
        • https://www.statista.com/statistics/276623/number-of-apps-available-in-leading-app-stores/
        • 拓扑排序:https://oi-wiki.org/graph/topo/
        • fui:https://github.com/dblock/fui
        • WBBlades:https://github.com/wuba/WBBlades
        • 抖音 iOS 工程架构演进:https://juejin.cn/post/6950454120826765325



        本文由作者授权严选技术团队发布


        您可能也对以下帖子感兴趣

        文章有问题?点此查看未经处理的缓存