一、问题背景

在将浏览器内核从 Chromium 132 升级到 148 的过程中,遇到了一个棘手的问题:Render 进程无法收到 Browser 进程发送的 Legacy IPC 消息。奇怪的是,反向的消息(Render → Browser)却能正常发送。

这个问题的本质是 Chromium 148 对进程内 IPC 路由架构进行了根本性的重构。

二、问题表现

升级到 148 后,所有依赖 Legacy IPC 通信的功能全部失效:

  • Browser 进程发出的 IPC 消息在 Render 端无法被 RenderFrame 接收

  • 日志显示消息已到达 Render 进程,但没有被分发到目标 RenderFrame

  • Mojo 通信的功能正常,只有走 IPC::Channel 的老消息受影响

  • Render 进程向 Browser 发送消息正常

三、根因分析:Chromium 148 架构变更

3.1 为什么 Chromium 要改这个架构?

Chromium 148 对渲染进程的 IPC 架构进行了根本性重构,主要驱动力来自三个方面:

1. Site Isolation 的深化

随着 Site Isolation(站点隔离)策略的全面推行,不同站点的 iframe 需要在逻辑上完全隔离。旧架构中,所有 RenderFrame 共享同一个全局路由表,这存在安全隐患——一个站点理论上可能通过某种方式获取其他站点 frame 的 routing_id

新架构引入了 AgentSchedulingGroup 作为按站点分组的调度单元:

text

旧模型: RenderProcess → RenderThread (全局路由表) → 所有 RenderFrame
新模型: RenderProcess → AgentSchedulingGroup (按站点分组) → 同站点 RenderFrame
2. 全面向 Mojo 迁移

Chromium 的长期目标是废弃 Legacy IPC,全面转向 Mojo。Mojo 使用 message pipe 进行端到端通信,天然不需要全局路由表:

text

Legacy IPC: Browser → Channel → 路由表查 routing_id → 目标 RenderFrame
Mojo:       Browser → MessagePipe → 目标 RenderFrame (直接绑定)

routing_id 是 Legacy IPC 时代的产物,是一个全局命名空间中的整数标识符。在 Mojo 体系中,这个设计已经过时。

3. 进程模型简化

旧架构中,RenderThread 承担了太多职责:持有 Channel、维护路由表、分发消息、处理控制消息等。新架构将这些职责分离到不同的组件中,使每个组件的职责更单一。

3.2 132 vs 148 架构对比

Chromium 132 架构

text

┌─────────────────────────────────────────────────────────┐
│                    RenderProcess                         │
│  ┌───────────────────────────────────────────────────┐  │
│  │                  RenderThread (全局单例)            │  │
│  │                                                    │  │
│  │  职责:                                             │  │
│  │  1. 持有 IPC::Channel (与Browser通信的管道)       │  │
│  │  2. 维护全局路由表 routing_id → IPC::Listener      │  │
│  │  3. 分发所有收到的IPC消息                          │  │
│  │                                                    │  │
│  │  ┌─────────────────────────────────────────────┐  │  │
│  │  │           全局路由表 (routing_id_map_)       │  │  │
│  │  │                                             │  │  │
│  │  │  routing_id=1 → RenderFrameImpl (主frame)   │  │  │
│  │  │  routing_id=2 → RenderFrameImpl (iframe A)  │  │  │
│  │  │  routing_id=3 → RenderFrameImpl (iframe B)  │  │  │
│  │  │  MSG_ROUTING_CONTROL → RenderThread 自身     │  │  │
│  │  └─────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────┘  │
│                          │                               │
│         ┌────────────────┼────────────────┐              │
│         ▼                ▼                ▼              │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐         │
│  │RenderFrame │  │RenderFrame │  │RenderFrame │         │
│  │(routing=1) │  │(routing=2) │  │(routing=3) │         │
│  │            │  │            │  │            │         │
│  │ 实现:      │  │ 实现:      │  │ 实现:      │         │
│  │ IPC::Listen│  │ IPC::Listen│  │ IPC::Listen│         │
│  │ IPC::Sender│  │ IPC::Sender│  │ IPC::Sender│         │
│  └────────────┘  └────────────┘  └────────────┘         │
└─────────────────────────────────────────────────────────┘

消息接收流程(132):

text

IPC::Channel 收到消息
  → RenderThread::OnMessageReceived(msg)
    → 提取 routing_id
      → routing_id_map_.find(routing_id) 找到目标 RenderFrame
        → render_frame->OnMessageReceived(msg) 直接分发

消息发送流程(132):

text

RenderFrame::Send(msg)
  → msg->set_routing_id(routing_id_)
    → render_thread_->Send(msg)
      → channel_->Send(msg) 直接写入通道
Chromium 148 原生架构

text

┌─────────────────────────────────────────────────────────────────┐
│                        RenderProcess                             │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │                  RenderThread (大幅简化)                     │ │
│  │                                                             │ │
│  │  职责:                                                      │ │
│  │  1. 持有 IPC::Channel (仅用于legacy IPC)                    │ │
│  │  2. 只处理 MSG_ROUTING_CONTROL 消息                        │ │
│  │  3. ❌ 不再维护 RenderFrame 的 routing_id 路由表            │ │
│  └────────────────────────────────────────────────────────────┘ │
│                          │                                       │
│          ┌───────────────┼───────────────┐                      │
│          ▼               ▼               ▼                      │
│  ┌─────────────────────────────────────────────────┐           │
│  │     AgentSchedulingGroup (site-a.example.com)    │           │
│  │                                                  │           │
│  │  职责:                                           │           │
│  │  1. 同站点帧的调度中心                           │           │
│  │  2. 维护 LocalFrameToken → RenderFrame 映射      │           │
│  │  3. ❌ 不维护 routing_id 映射 (原生148)          │           │
│  │  4. 管理 Mojo 接口 (Associated Interface)        │           │
│  │                                                  │           │
│  │  ┌────────────────────────────────────────────┐  │           │
│  │  │     listener_map_ (按LocalFrameToken)       │  │           │
│  │  │                                            │  │           │
│  │  │  frame_token_A → RenderFrameImpl (主frame) │  │           │
│  │  │  frame_token_B → RenderFrameImpl (iframe)  │  │           │
│  │  └────────────────────────────────────────────┘  │           │
│  └─────────────────────────────────────────────────┘           │
│                          │                                       │
│         ┌────────────────┼────────────────┐                     │
│         ▼                ▼                ▼                     │
│  ┌────────────┐  ┌─────────────────────────────────────┐        │
│  │RenderFrame │  │  AgentSchedulingGroup (site-b.com)   │        │
│  │            │  │  ┌────────────┐                      │        │
│  │ 实现:      │  │  │RenderFrame │                     │        │
│  │ ❌ 不继承  │  │  │            │                     │        │
│  │ IPC::Listen│  │  │ 实现:      │                     │        │
│  │ IPC::Sender│  │  │ ❌ 不继承  │                     │        │
│  └────────────┘  │  │ IPC::Listen│                     │        │
│                   │  └────────────┘                      │        │
│                   └─────────────────────────────────────┘        │
└─────────────────────────────────────────────────────────────────┘

消息接收流程(148 原生)—— 问题所在:

text

IPC::Channel 收到消息
  → RenderThread::OnMessageReceived(msg)
    → 提取 routing_id
      → ❌ 没有 routing_id_map_,无法找到目标 RenderFrame
        → 消息被丢弃!

消息发送流程(148)—— 不受影响:

text

RenderFrame::Send(msg)
  → agent_scheduling_group_->Send(msg)
    → channel_->Send(msg) 直接写入通道,不需要路由表

3.3 为什么只有接收受影响?

这个问题的关键点在于,接收消息依赖路由表,而发送消息不依赖

方向 路径 是否依赖 Render 端路由表 受影响
Browser → Render Channel → 路由表查 routing_id → RenderFrame
Render → Browser RenderFrame → Channel → Browser 路由表 (直接写 Channel)

发送方向:RenderFrame 直接持有 AgentSchedulingGroup(进而持有 Channel)的引用,可以直接写入。Browser 端的路由表仍然存在,所以 Browser 能正确接收。

接收方向:消息从 Channel 到达 Render 端后,需要根据 routing_id 找到对应的 RenderFrame。在 148 中,RenderThread 不再维护这个路由表,AgentSchedulingGroup 也只用 LocalFrameToken 做索引,routing_id 无法直接映射,导致消息无法分发。

四、解决方案

核心思路:在 148 的新架构上,手动重建 routing_id 路由表,让 Legacy IPC 消息可以正确分发。

4.1 修改 RenderFrame 接口

让 RenderFrame 重新成为 IPC 通信端点:

cpp

// content/public/renderer/render_frame.h
// 修改前 (148原生):
class CONTENT_EXPORT RenderFrame : public base::SupportsUserData {
  // 不继承 IPC 接口
};

// 修改后:
#if BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
#include "ipc/ipc_listener.h"
#include "ipc/ipc_sender.h"
#endif

class CONTENT_EXPORT RenderFrame :
#if BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
    public IPC::Listener,      // 重新继承
    public IPC::Sender,        // 重新继承
#endif
    public base::SupportsUserData {
  // ...
};

4.2 在 AgentSchedulingGroup 中重建路由表

这是最关键的一步——在 AgentSchedulingGroup 中维护 routing_id → RenderFrame 的映射:

cpp

// content/renderer/agent_scheduling_group.h
class AgentSchedulingGroup {
 private:
  // 原有的 LocalFrameToken 映射(Mojo 用)
  absl::flat_hash_map<blink::LocalFrameToken, 
                      raw_ptr<RenderFrameImpl>> listener_map_;

  // 新增: routing_id 映射(Legacy IPC 用)
#if BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
  std::map<int32_t, raw_ptr<RenderFrameImpl>> routing_id_map_;
#endif
};

4.3 修改 Frame 注册/注销流程

cpp

// content/renderer/agent_scheduling_group.cc

void AgentSchedulingGroup::AddFrameRoute(
    const blink::LocalFrameToken& frame_token,
#if BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
    int routing_id,       // 新增参数
#endif
    RenderFrameImpl* render_frame,
    scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
  
  // 保留原有的 LocalFrameToken 映射
  listener_map_.insert({frame_token, render_frame});
  
  // 新增 routing_id 映射和路由注册
#if BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
  DCHECK(!base::Contains(routing_id_map_, routing_id));
  routing_id_map_.insert({routing_id, render_frame});
  render_thread_->AddRoute(routing_id, render_frame);
  render_thread_->AttachTaskRunnerToRoute(routing_id, std::move(task_runner));
#endif
}

void AgentSchedulingGroup::RemoveFrameRoute(
    const blink::LocalFrameToken& frame_token
#if BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
    , int routing_id
#endif
) {
  listener_map_.erase(frame_token);
#if BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
  DCHECK(base::Contains(routing_id_map_, routing_id));
  routing_id_map_.erase(routing_id);
  render_thread_->RemoveRoute(routing_id);
#endif
}

4.4 实现消息接收分发

cpp

// content/renderer/agent_scheduling_group.cc

#if BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
bool AgentSchedulingGroup::OnMessageReceived(const IPC::Message& message) {
  DCHECK_NE(message.routing_id(), MSG_ROUTING_CONTROL);
  
  // 根据 routing_id 找到目标 RenderFrame
  auto* listener = GetListener(message.routing_id());
  if (!listener)
    return false;
  
  // 分发给目标 RenderFrame
  return listener->OnMessageReceived(message);
}

bool AgentSchedulingGroup::Send(IPC::Message* message) {
  std::unique_ptr<IPC::Message> msg(message);
  
  if (GetMBIMode() == features::MBIMode::kLegacy)
    return render_thread_->Send(msg.release());
  
  DCHECK_NE(message->routing_id(), MSG_ROUTING_CONTROL);
  DCHECK(channel_);
  return channel_->Send(msg.release());
}
#endif

// routing_id 查找辅助方法
#if BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
RenderFrameImpl* AgentSchedulingGroup::GetListener(int32_t routing_id) {
  return base::FindPtrOrNull(routing_id_map_, routing_id);
}
#endif

4.5 适配 OnBadMessageReceived 签名变更

148 中 IPC::Listener::OnBadMessageReceived 的签名从无参变为带 const IPC::Message& 参数:

cpp

// 修改前 (132):
void OnBadMessageReceived() override;

// 修改后 (148):
void AgentSchedulingGroup::OnBadMessageReceived(const IPC::Message& message) {
  return ToImpl(*render_thread_).OnBadMessageReceived(message);
}

4.6 适配 RenderFrameImpl 消息处理

cpp

// content/renderer/render_frame_impl.cc

// 构造时传入 routing_id
agent_scheduling_group_->AddFrameRoute(
    frame_token_,
#if BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
    routing_id_,
#endif
    this, GetTaskRunner(blink::TaskType::kInternalNavigationAssociated));

// 析构时传入 routing_id
agent_scheduling_group_->RemoveFrameRoute(
    frame_token_
#if BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
    , routing_id_
#endif
);

// 消息处理中适配 observer 调用
bool RenderFrameImpl::OnMessageReceived(const IPC::Message& msg) {
  for (auto& observer : observers_) {
#if defined(USE_CUSTOM_HACK) && BUILDFLAG(CONTENT_ENABLE_LEGACY_IPC)
    // 优先调用 OnMessageReceived
    if (observer.OnMessageReceived(msg))
      return true;
#endif
#ifdef USE_CUSTOM_HACK
    // 兼容旧的回调路径
    if (observer.OnMessageReceivedFromWidget(msg, nullptr))
      return true;
#endif
  }
  // ... 其他消息处理
}

4.7 修复后的完整消息流

text

修复后:
IPC::Channel 收到消息
  → RenderThread::OnMessageReceived(msg)
    → AgentSchedulingGroup::OnMessageReceived(msg)
      → 提取 routing_id
        → routing_id_map_.find(routing_id) ✅ 找到了!
          → render_frame->OnMessageReceived(msg) 成功分发

五、踩坑总结

5.1 关键教训

  1. 理解架构变更的本质:不要只看 API 变化,要理解 Chromium 为什么改。148 的目标是废弃 Legacy IPC 转向 Mojo,routing_id 路由被主动移除。

  2. 消息收发的不对称性:IPC 通信中,发送路径和接收路径的依赖不同。发送端直接持有 Channel 引用即可,接收端依赖路由表。这就是为什么只有接收受影响。

  3. Legacy IPC 的保护:如果你的产品还大量使用 Legacy IPC(通过自定义宏控制),升核时必须重点关注 IPC::ListenerIPC::Sender 接口和路由机制的变更。

5.2 检查清单

升级 Chromium 大版本时,检查以下内容:

  • RenderFrame 是否还继承 IPC::Listener 和 IPC::Sender

  • RenderThread 是否还维护 routing_id_map_

  • AgentSchedulingGroup 的消息路由机制

  • OnBadMessageReceived 的签名是否变更

  • 自定义 RenderFrameObserver 的消息回调路径

  • AddRoute / RemoveRoute 的调用是否还存在

5.3 架构演进趋势

text

Chromium 版本        IPC 机制                     路由方式
───────────         ────────                     ────────
132 及之前          Legacy IPC 为主              RenderThread 全局路由表
148                 Mojo 为主, Legacy IPC 废弃    AgentSchedulingGroup Token 路由
未来                Mojo 完全替代                 无全局路由

对于仍有 Legacy IPC 需求的产品,需要在每次升级时关注 Chromium 对旧 IPC 基础设施的裁剪,及时做兼容性修复。长期来看,逐步将 Legacy IPC 迁移到 Mojo 才是根本解决方案。


本文基于真实的浏览器内核升级工程经验编写,代码示例已脱敏处理,仅保留架构相关的核心逻辑。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐