一、问题背景
在将浏览器内核从 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 关键教训
-
理解架构变更的本质 :不要只看 API 变化,要理解 Chromium 为什么改。148 的目标是废弃 Legacy IPC 转向 Mojo,
routing_id路由被主动移除。 -
消息收发的不对称性:IPC 通信中,发送路径和接收路径的依赖不同。发送端直接持有 Channel 引用即可,接收端依赖路由表。这就是为什么只有接收受影响。
-
Legacy IPC 的保护 :如果你的产品还大量使用 Legacy IPC(通过自定义宏控制),升核时必须重点关注
IPC::Listener、IPC::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 才是根本解决方案。
本文基于真实的浏览器内核升级工程经验编写,代码示例已脱敏处理,仅保留架构相关的核心逻辑。