当前位置: 首页 > news >正文

从循环依赖谈 Chromium 模块化设计:编译结构与最佳实践

在大型 C++ 工程中,循环依赖(Circular Dependency) 往往是性能退化、编译效率降低与代码可维护性下降的根源之一。Chromium 作为一个拥有数千万行 C++ 代码的大型项目,在架构设计上天然地面临循环依赖的挑战。

本文将结合 C++ 的语言特性和 Chromium 的模块化工程实践,深入剖析:

  • 什么是循环依赖以及它的危害;

  • Chromium 中真实出现循环依赖风险的模块;

  • Chromium 如何通过接口设计、前向声明、Observer 模式等方式进行解耦;

  • 工程层面如何避免、检测和修复循环依赖;

  • 我们能从中借鉴哪些实战经验。


一、什么是 C++ 循环依赖?

循环依赖是指两个或多个类或模块互相依赖,形成一个依赖闭环。在 C++ 中通常表现为头文件互相包含,或类之间存在互为成员变量的情况:

示例:

// a.h #include "b.h" class A { B b_; // ❌ 错误:B 类型不完整 }; // b.h #include "a.h" class B { A a_; // ❌ 错误:A 类型不完整 }; 

这会导致编译失败,错误如:

error: incomplete type 'B' is not allowed

除了编译错误,循环依赖还会引发一系列工程问题,比如:

  • 模块强耦合,无法单独测试或替换;

  • 编译时间显著增加;

  • 增量构建失效,导致一行小改动引发大面积重编译;

  • 更难做重构或解耦。


二、Chromium 项目中典型的循环依赖风险点

在 Chromium 中,有众多模块存在天然的双向关系。例如:

模块 A模块 B关系说明
WebContentsImplRenderFrameHostImplWebContents 持有 Frame 列表,Frame 回调 WebContents
RenderFrameHostImplNavigationRequestFrame 启动导航,导航需要 Frame 状态
SiteInstanceImplRenderProcessHostSiteInstance 映射进程,进程反查站点信息

这些模块如果直接通过头文件相互引用,会很容易产生编译环。


三、Chromium 是如何规避循环依赖的?

Chromium 并未完全依赖语言特性,而是结合 C++ 编程惯例与模块设计原则,通过以下方式实现解耦:


1)前向声明 + 指针成员

最常见的方式就是使用前向声明(forward declaration)替代 #include,并将成员变量改为指针或引用:

// WebContentsImpl.h class RenderFrameHostImpl; // 前向声明 class WebContentsImpl { RenderFrameHostImpl* main_frame_; // 避免完整类型依赖 };

这样可以有效切断头文件依赖闭环。


2)Observer 模式解耦双向通知

例如 Frame 需要回调 WebContents 状态更新,不直接访问其接口,而是通过接口抽象:

// render_frame_host_delegate.h class RenderFrameHostDelegate { public: virtual void OnFrameStartedLoading() = 0; }; 

再在 WebContentsImpl 中实现这个接口:

class WebContentsImpl : public RenderFrameHostDelegate { void OnFrameStartedLoading() override { ... } }; 

这样 RenderFrameHostImpl 只依赖于 RenderFrameHostDelegate 抽象,而不是具体的 WebContents 实现。


3)Interface/Impl 分离:模块设计分层

Chromium 遵循“接口对外、实现隐藏”的架构习惯。例如:

// web_contents.h(纯虚类接口) class WebContents { public: virtual void LoadURL(...) = 0; }; // web_contents_impl.h class WebContentsImpl : public WebContents { void LoadURL(...) override; }; 

这样,其他模块只与 WebContents 接口交互,不依赖其实现类,进一步削弱了模块间的耦合度。


4)多进程通信断开跨模块直接调用

Chromium 是多进程架构,不同进程之间靠 Mojo 通信(IPC)传输数据。比如:

  • UI 进程的 WebContents 通过 Mojo 连接 GPU 进程;

  • RenderFrame 通过 IPC 调用浏览器进程接口;

这样天然避免了跨模块的直接类引用,Mojo 接口本质就是一个 protocol 层,结构上起到了“断层”的效果。


5)工具和规范约束 include 层级

Chromium 还通过构建工具和代码规范来避免 include 炸弹:

  • GN 构建系统限制跨目录包含;

  • 编译器强制要求头文件包含最小化;

  • 工具如 include-what-you-use (IWYU)clang-tidygn check 检测冗余 include。


四、一个真实模块解耦案例:RenderFrameHostImplWebContentsImpl

背景

RenderFrameHostImpl 是渲染帧在浏览器进程的抽象,WebContentsImpl 管理整个页面状态。

它们间存在双向需求:

  • WebContentsImpl 需要枚举所有 Frame;

  • RenderFrameHostImpl 在加载、崩溃等状态变更时需要通知 WebContents;

解法

Chromium 通过引入 RenderFrameHostDelegate 接口:

class RenderFrameHostDelegate { public: virtual void OnDidFinishNavigation(...) = 0; }; 

RenderFrameHostImpl 只依赖这个接口,并通过构造函数注入:

RenderFrameHostImpl(RenderFrameHostDelegate* delegate); 

WebContentsImpl 作为实现类提供具体行为。

这样,从结构上就避免了 .h 文件之间的互相包含,且功能解耦清晰。


五、工程实践建议(如何规避循环依赖)

结合 Chromium 的做法,C++ 项目中可以遵循以下建议:

建议描述
✅ 避免在头文件中包含完整类定义,尽量使用前向声明
✅ 用 raw_ptr<T>WeakPtr<T> 持有成员,解耦生命周期
✅ 使用接口抽象定义回调、监听器等交互关系
✅ 头文件只声明接口和最小依赖,实际逻辑放在 .cpp
✅ 拆分复杂类为 interface + impl,避免混杂职责
✅ 引入中介者模式(如消息总线、调度中心)打破闭环
✅ 用构建工具限制跨目录 include,减少耦合路径

六、如何自动检测循环依赖?

在大型工程中,人工很难发现所有隐蔽依赖,可以使用以下工具:

  • include-what-you-use:分析头文件包含是否必要;

  • clangd/clang-tidy:实时提示循环引用风险;

  • gn desc --tree:查看目标的构建依赖树;

  • graphviz + python:生成头文件依赖图;


七、总结

循环依赖是 C++ 项目结构设计中的大敌,尤其在 Chromium 这种超大规模工程中尤为棘手。Chromium 的工程团队通过:

  • 精细的模块划分;

  • 接口/实现解耦;

  • 前向声明 + 依赖注入;

  • Mojo 通信打断进程级耦合;

  • 构建系统强约束依赖边界;

有效地避免了大量头文件循环引用的问题。

我们在日常开发中也可以借鉴其思路,将模块边界划清,构建更松耦合、可维护的系统结构。


📎 附录:相关参考

  • Chromium Source - base/observer_list.h

  • Chromium Design Docs - Multi-process architecture

  • include-what-you-use 官方项目

http://www.lryc.cn/news/605939.html

相关文章:

  • 基于 Amazon Nova Sonic 和 MCP 构建语音交互 Agent
  • 开发避坑短篇(11):Oracle DATE(7)到MySQL时间类型精度冲突解决方案
  • USRP捕获手机/路由器数据传输信号波形(下)
  • 6.苹果ios逆向-过ssl证书检测-安装SSL Kill Switch 3
  • JVM字节码文件结构剖析
  • uniapp Vue3版本使用pinia存储持久化插件pinia-plugin-persistedstate对微信小程序的配置
  • 【生活篇】Ubuntu22.04安装网易云客户端
  • 计数组合学7.9( 标量积)
  • 如何使用 JavaScript 接入实时行情 API
  • esim系统科普
  • ES 工业网关:比德国更适配,比美国更易用
  • 是德科技的BenchVue和纳米软件的ATECLOUD有哪些区别?
  • node.js之Koa框架
  • 25-vue-photo-preview的使用及使用过程中的问题解决方案
  • Hive课后练习题
  • 【Leetcode】2683. 相邻值的按位异或
  • 《Java 程序设计》第 16 章 - JDBC 数据库编程
  • rabbitmq的安装和使用-windows版本
  • MFC CChartCtrl编程
  • Python爬虫07_Requests爬取图片
  • 【Java23种设计模式】:模板方法模式
  • 【C语言】深度剖析指针(三):回调机制、通用排序与数组指针逻辑
  • PostgreSQL面试题及详细答案120道(01-20)
  • 前端方案设计:实现接口缓存
  • 什么是网络安全?网络安全包括哪几个方面?学完能做一名黑客吗?
  • 网络与信息安全有哪些岗位:(4)应急响应工程师
  • Amazon RDS for MySQL成本优化:RDS缓存降本实战
  • 前缀和-1314.矩阵区域和-力扣(LeetCode)
  • 隐私灯是否“可信”?基于驱动层的摄像头指示机制探析
  • 【1】数据可视化分析方法