一、前言
在大型 C++ 工程(例如 Chrome 浏览器内核)中,开发者经常会遇到这样的选择:
到底应该在关键点使用 CHECK
直接崩溃,还是使用 return
、LOG
记录错误然后继续执行?
这看似只是一个代码风格问题,实则背后牵扯到 线上崩溃率、稳定性、用户体验、以及问题定位效率。
本文结合浏览器内核的真实代码案例,从源码设计出发,分析 CHECK
与 return
的差异,探讨它们在 稳定性优化 和 线上质量保障 中的取舍,并给出一些落地的优化实践经验。
二、背景知识:什么是 CHECK
在 Chromium 基础库(base/check.h
)中,CHECK
是一个类似于断言的宏,用法非常常见:
CHECK(pointer); CHECK_EQ(size, expected_size);
和 DCHECK
不同的是:
CHECK
永远生效(无论 Debug 还是 Release),失败时会直接触发 致命崩溃(调用ImmediateCrash()
)。DCHECK
只在 Debug 或启用了--enable-dcheck
的模式下才有效,Release 默认不生效。
也就是说:
CHECK = 防御型编程 + 线上崩溃
DCHECK = 调试模式下的保护网
因此,使用 CHECK
的地方,意味着开发者明确认为 “如果条件不满足,继续运行程序只会带来更大风险,还不如直接崩溃并上报”。
三、典型代码案例分析
以浏览器 UI 初始化的一个场景为例:
ViewBuilder buildui; const ui::ThemeProvider* tp = GetThemeProvider(); if (tp) { base::RefCountedMemory* memory = tp->GetRawData("browser.xml", ui::k100Percent); CHECK(memory); if (memory) { buildui.BuilderUiUtf8((const char*)(memory->front()), this); } }
3.1 为什么这里要用 CHECK?
browser.xml
是浏览器 UI 布局的核心资源,缺失后界面几乎无法正常渲染。如果
memory == nullptr
,继续运行只会导致后续逻辑 不可控,甚至触发 更隐蔽的崩溃。与其后面某处 随机崩溃,不如在这里立刻崩溃并生成明确的堆栈。
这就是 “快速失败(fail fast)” 的思想。
3.2 如果换成 return,会发生什么?
假设我们改成:
if (!memory) { LOG(ERROR) << "browser.xml not found!"; return; }
那么结果可能是:
浏览器启动后黑屏、空白 UI,但进程没崩溃。
用户上报 “浏览器打不开界面”,但 crash dump 无法收集到。
问题变成 用户可见 bug,而不是 研发可复现的崩溃。
这对于稳定性指标(crash 率下降),看似有好处,但实际上可能导致 用户体验更差,研发也更难排查问题。
四、崩溃率 vs 稳定性:Return 和 CHECK 的权衡
在工程实践中,我们必须明确:
崩溃率降低 ≠ 稳定性提升
关键在于 是否能继续安全执行
我们来对比:
场景 | 使用 CHECK | 使用 return |
---|---|---|
必要资源缺失(如 ICU 数据、核心 UI XML) | 立即崩溃,易于定位问题 | 可能黑屏、功能残缺,用户可见异常 |
可选功能缺失(如某个实验性模块文件) | 不建议 CHECK,影响整体稳定性 | return 更合理,降级执行 |
开发/调试阶段 | CHECK 更利于暴露问题 | return 可能掩盖 bug |
线上用户体验 | 崩溃率升高,定位快 | 崩溃率降低,但潜在 bug 难发现 |
所以,CHECK
和 return
的取舍,本质是 在开发效率和用户体验之间做平衡。
五、实战案例:ICU 数据初始化
再看一个浏览器启动过程的例子:
bool InitializeICUFromDataFile() { LazyInitIcuDataFile(); bool result = InitializeICUWithFileDescriptorInternal(g_icudtl_pf, g_icudtl_region); // 省略修复逻辑... #if !BUILDFLAG(IS_CHROMEOS) CHECK(result); #endif return result; }
5.1 为什么要 CHECK(result)
ICU(International Components for Unicode)库是浏览器处理多语言文本的底层依赖:
URL 编解码
字符串归一化
字体渲染
如果 result == false
,意味着 ICU 数据加载失败,浏览器将 无法正常显示文字,后续所有逻辑都是“废的”。
这种情况不可能通过 return
优雅降级,因此必须 硬崩溃。
5.2 线上优化的方式
有些厂商会加 修复逻辑:
如果文件损坏,尝试复制备份文件或重新下载。
只有在修复仍失败时,才触发
CHECK
。
这样既保留了问题定位能力,也避免了因文件损坏导致的大规模崩溃。
六、Return vs CHECK 的取舍标准
结合浏览器内核开发经验,我总结了一套实践标准:
核心依赖缺失(必须)
比如 ICU、核心 DLL、主进程资源
必须
CHECK
,否则继续执行毫无意义
关键路径但可恢复(尝试修复后 CHECK)
比如 UI skin、配置文件
可以先尝试 fallback/修复,最后再
CHECK
非关键路径(直接 return)
比如实验性模块、日志收集、辅助功能
用
return
避免线上崩溃率增加
Debug 阶段优先 CHECK,Release 阶段视情况降级
开发时暴露问题
发布时保障用户体验
七、稳定性优化的工程思考
7.1 崩溃率指标要结合 “可用性”
单纯追求 crash 率下降 并不科学
更重要的是用户是否能 完成主要功能
如果 return 导致浏览器无法启动,虽然 crash 率下降,但用户留存反而下降
7.2 崩溃收敛要和监控系统结合
通过 Crashpad 或 Breakpad 收集崩溃堆栈
配合日志系统判断是 偶发 bug 还是 环境问题
对于外部文件缺失类问题,可以考虑 自愈机制(repair)
7.3 CHECK 的位置很关键
CHECK 应该放在 错误首次被发现的位置,而不是后面调用处
否则会让堆栈看起来和实际问题无关,增加排查难度
八、总结
本文通过分析 CHECK
和 return
的区别,结合浏览器内核中的 UI 资源加载 和 ICU 初始化 两个案例,说明了它们在 线上崩溃率优化 中的不同作用:
CHECK:快速失败,便于定位核心问题,但可能提高 crash 率
return:避免崩溃,提升稳定性,但可能掩盖严重问题
最佳实践是:
核心依赖:必须 CHECK
可修复:修复失败后 CHECK
非关键路径:return
崩溃率优化的目标不只是数字下降,而是 既能快速定位问题,又能保障用户体验。
这正是工程实践中的智慧所在。