Chromium Settings 自启动开关:三种 pref 同步方案深度对比
在 Zero 浏览器设置页实现「开机自启动」时,一个看似简单的问题------开关状态重进页面后恢复默认关闭------背后其实是两套完全不同的数据通道在打架。本文从机制层面对比三种实现,并给出推荐做法。
背景:我们要解决什么
需求可以拆成两件事:
| 职责 | 说明 |
|---|---|
| 写/读 pref | zero.browser.autostart ↔ PrefService ↔ config.ini |
| 装 zero 服务 | 用户开启时触发 zerosrv.exe /install |
HTML 侧已经用了标准绑定:
html
<settings-toggle-button
pref="{{prefs.zero.browser.autostart}}"
label="开机自启动"
on-settings-boolean-control-change="onAutostartChange_">
</settings-toggle-button>
问题出在:pref 从哪来、往哪写------选错了通道,就会出现「磁盘上有值,UI 却显示关」。
Chromium Settings 的标准 pref 闭环
在讨论三种方案前,先看清 Settings 页的设计约定。
渲染错误: Mermaid 渲染失败: Parse error on line 9: ...lPrefs() API->>C++: 遍历 allowlist ----------------------^ Expecting 'NEWLINE', ',', '()', 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', 'TXT', got '+'
读(页面初始化)
typescript
// chrome/browser/resources/settings_shared/prefs/prefs.ts
this.settingsApi_.getAllPrefs().then((prefs) => {
this.updatePrefs_(prefs);
CrSettingsPrefs.setInitialized();
});
写(用户操作)
typescript
// chrome/browser/resources/settings_shared/controls/settings_boolean_control_mixin.ts
notifyChangedByUserInteraction() {
this.dispatchEvent(new CustomEvent('settings-boolean-control-change', ...));
if (!this.pref || this.noSetPref) {
return;
}
this.sendPrefChange();
}
typescript
// prefs.ts
this.settingsApi_.setPref(key, prefObj.value, /* pageId */ '');
门禁:allowlist
cpp
// chrome/browser/extensions/api/settings_private/prefs_util.cc
std::optional<settings_api::PrefObject> PrefsUtil::GetPref(
const std::string& name) {
if (GetAllowlistedPrefType(name) == settings_api::PrefType::kNone) {
return std::nullopt;
}
// ...
}
不在 allowlist 里的 pref,前端根本读不到、也写不进去------这是许多 bug 的根因。
方案一:allowlist + pref 绑定(推荐)
思路
完全走 Settings 标准链路:加 allowlist,让 getAllPrefs / setPref 能操作 zero.browser.autostart;WebUI message 只管装服务。
代码形态
C++:加入 allowlist
cpp
#ifdef USE_360HACK
// zero browser autostart
(*s_allowlist)[::prefs::kAutoStart] =
settings_api::PrefType::kBoolean;
#endif
前端:开关变更只触发装服务(开启时)
typescript
// chrome/browser/resources/settings/basic_page/basic_page.ts
private onAutostartChange_(event: Event) {
const target = event.target as HTMLElement&{checked?: boolean};
if (!target.checked) {
return;
}
chrome.send('checkAndInstallZeroService', [true]);
}
C++:handler 不再写 pref
cpp
// chrome/browser/ui/webui/settings/settings_startup_pages_handler.cc
void StartupPagesHandler::HandleCheckAndInstallZeroService(
const base::Value::List& args) {
CHECK_EQ(1U, args.size());
if (!args[0].GetBool()) {
return;
}
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
base::BindOnce(&CheckAndInstallZeroServiceOnFileThread));
}
数据流
读:getAllPrefs → prefs.zero.browser.autostart → 开关显示
写:sendPrefChange → setPref → PrefService → SeProfile → config.ini
副作用:chrome.send → 装 zero 服务(仅开启)
优点
- 与 Settings 里其它开关(书签栏、拼写检查等)完全一致
- 读写同一通道,重进页面自然正确
onPrefsChanged自动同步外部变更(策略、同步等)- handler 职责单一,易维护
缺点
- 必须记得加 allowlist(漏了就会复现「显示关」)
- 需要理解 settingsPrivate 机制,不能只写 C++ handler
方案二:allowlist + 进页显式 getPref(冗余加固)
思路
在方案一基础上,进入 /onStartup 路由时再手动拉一次 pref:
typescript
const AUTO_START_PREF = 'zero.browser.autostart';
private refreshAutostartPrefIfNeeded_() {
if (!routes.ON_STARTUP || this.currentRoute_ !== routes.ON_STARTUP) {
return;
}
CrSettingsPrefs.initialized.then(() => {
return chrome.settingsPrivate.getPref(AUTO_START_PREF);
}).then((pref: chrome.settingsPrivate.PrefObject) => {
if (!pref || !this.prefs) {
return;
}
this.set('prefs.zero.browser.autostart', pref);
});
}
// connectedCallback / currentRouteChanged 中调用
与方案一的对比
| 方案一 | 方案二 | |
|---|---|---|
| 页面加载读 pref | getAllPrefs 一次 |
getAllPrefs + 进路由 getPref |
| 代码量 | 少 | 多 |
| 与同类开关一致性 | 高 | 低(特殊处理) |
| 修 bug 必要性 | 足够 | 通常不必要 |
何时可能有用
dom-if restamp与 pref 初始化存在极端竞态(实测少见)- 直链
zero://settings/onStartup且怀疑 binding 未更新
结论
方案二是方案一的子集 + 防御代码 。若方案一实测通过,建议删除 refreshAutostartPrefIfNeeded_,避免维护两套「读」逻辑。
方案三:WebUI handler 直写 SetBoolean(不推荐)
思路
用户点开关 → chrome.send → C++ 直接 prefs->SetBoolean,不依赖 settingsPrivate。
代码形态(问题版本)
前端
typescript
private onAutostartChange_(event: Event) {
const target = event.target as HTMLElement&{checked?: boolean};
chrome.send('checkAndInstallZeroService', [!!target.checked]);
}
C++
cpp
void StartupPagesHandler::HandleCheckAndInstallZeroService(
const base::Value::List& args) {
CHECK_EQ(1U, args.size());
bool enabled = args[0].GetBool();
PrefService* prefs = Profile::FromWebUI(web_ui())->GetPrefs();
prefs->SetBoolean(prefs::kAutoStart, enabled); // 只写,不读
if (enabled) {
base::ThreadPool::PostTask(..., CheckAndInstallZeroServiceOnFileThread);
}
}
为什么「写了 pref,UI 还是关」
┌──────────────────────────────────────────────┐
│ UI 读通道(pref 绑定) │
│ prefs.zero.browser.autostart │
│ ↑ getAllPrefs(需 allowlist) │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ WebUI 写通道(旁路) │
│ chrome.send → SetBoolean(kAutoStart) │
│ ↓ 写入 PrefService ✓ │
│ ✗ 不更新 prefs 对象 │
│ ✗ 无 getPref 回流 │
└──────────────────────────────────────────────┘
allowlist 未加时:
getAllPrefs不含zero.browser.autostartprefs.zero.browser.autostart为undefinedresetToPrefValue()走默认分支 →checked = false- 用户看到永远关 ,尽管
PrefService里可能是true
allowlist 加了之后,方案三还会出现:
- 双写 :
sendPrefChange和 handler 都在写 pref - 职责混乱:同一个 message 既写 pref 又装服务
- 无标准同步:外部改 pref 时,除非另写监听,UI 不更新
若坚持用 WebUI 写,要补多少活?
至少要再造「读」通道之一:
cpp
// 需要新增
void HandleGetAutostartPref(const base::Value::List& args) {
bool value = Profile::FromWebUI(web_ui())->GetPrefs()
->GetBoolean(prefs::kAutoStart);
// ResolveJavascriptCallback...
}
或前端:
typescript
chrome.settingsPrivate.getPref('zero.browser.autostart'); // 仍要 allowlist
等于重造 settingsPrivate,不如直接用方案一。
三方案总览
| 维度 | ① allowlist + 绑定 | ② + 进页 getPref | ③ WebUI SetBoolean |
|---|---|---|---|
| 读 pref | getAllPrefs | getAllPrefs + getPref | 无(需另造) |
| 写 pref | setPref | setPref | C++ SetBoolean |
| 重进页面状态 | ✅ | ✅ | ❌(无 allowlist 时) |
| 与 Settings 惯例 | ✅ | △ 多特殊逻辑 | ❌ |
| 装服务 | 独立 chrome.send | 独立 chrome.send | 混在同一 handler |
| 推荐度 | ⭐⭐⭐ | ⭐⭐ | ❌ |
推荐架构(最终形态)
#mermaid-svg-CFYNrjEZTlvVaXK7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CFYNrjEZTlvVaXK7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CFYNrjEZTlvVaXK7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CFYNrjEZTlvVaXK7 .error-icon{fill:#552222;}#mermaid-svg-CFYNrjEZTlvVaXK7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CFYNrjEZTlvVaXK7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CFYNrjEZTlvVaXK7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CFYNrjEZTlvVaXK7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CFYNrjEZTlvVaXK7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CFYNrjEZTlvVaXK7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CFYNrjEZTlvVaXK7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CFYNrjEZTlvVaXK7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CFYNrjEZTlvVaXK7 .marker.cross{stroke:#333333;}#mermaid-svg-CFYNrjEZTlvVaXK7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CFYNrjEZTlvVaXK7 p{margin:0;}#mermaid-svg-CFYNrjEZTlvVaXK7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CFYNrjEZTlvVaXK7 .cluster-label text{fill:#333;}#mermaid-svg-CFYNrjEZTlvVaXK7 .cluster-label span{color:#333;}#mermaid-svg-CFYNrjEZTlvVaXK7 .cluster-label span p{background-color:transparent;}#mermaid-svg-CFYNrjEZTlvVaXK7 .label text,#mermaid-svg-CFYNrjEZTlvVaXK7 span{fill:#333;color:#333;}#mermaid-svg-CFYNrjEZTlvVaXK7 .node rect,#mermaid-svg-CFYNrjEZTlvVaXK7 .node circle,#mermaid-svg-CFYNrjEZTlvVaXK7 .node ellipse,#mermaid-svg-CFYNrjEZTlvVaXK7 .node polygon,#mermaid-svg-CFYNrjEZTlvVaXK7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CFYNrjEZTlvVaXK7 .rough-node .label text,#mermaid-svg-CFYNrjEZTlvVaXK7 .node .label text,#mermaid-svg-CFYNrjEZTlvVaXK7 .image-shape .label,#mermaid-svg-CFYNrjEZTlvVaXK7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-CFYNrjEZTlvVaXK7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CFYNrjEZTlvVaXK7 .rough-node .label,#mermaid-svg-CFYNrjEZTlvVaXK7 .node .label,#mermaid-svg-CFYNrjEZTlvVaXK7 .image-shape .label,#mermaid-svg-CFYNrjEZTlvVaXK7 .icon-shape .label{text-align:center;}#mermaid-svg-CFYNrjEZTlvVaXK7 .node.clickable{cursor:pointer;}#mermaid-svg-CFYNrjEZTlvVaXK7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CFYNrjEZTlvVaXK7 .arrowheadPath{fill:#333333;}#mermaid-svg-CFYNrjEZTlvVaXK7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CFYNrjEZTlvVaXK7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CFYNrjEZTlvVaXK7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CFYNrjEZTlvVaXK7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CFYNrjEZTlvVaXK7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CFYNrjEZTlvVaXK7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CFYNrjEZTlvVaXK7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CFYNrjEZTlvVaXK7 .cluster text{fill:#333;}#mermaid-svg-CFYNrjEZTlvVaXK7 .cluster span{color:#333;}#mermaid-svg-CFYNrjEZTlvVaXK7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CFYNrjEZTlvVaXK7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CFYNrjEZTlvVaXK7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-CFYNrjEZTlvVaXK7 .icon-shape,#mermaid-svg-CFYNrjEZTlvVaXK7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CFYNrjEZTlvVaXK7 .icon-shape p,#mermaid-svg-CFYNrjEZTlvVaXK7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CFYNrjEZTlvVaXK7 .icon-shape .label rect,#mermaid-svg-CFYNrjEZTlvVaXK7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CFYNrjEZTlvVaXK7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CFYNrjEZTlvVaXK7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CFYNrjEZTlvVaXK7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 副作用通道
仅开启
onAutostartChange_
checkAndInstallZeroService
LaunchZeroServiceInstall
标准 pref 通道
sendPrefChange
settings-toggle-button
settingsPrivate.setPref
PrefService kAutoStart
SeProfile 监听
config.ini autostart_browser
getAllPrefs
最小必要改动:
prefs_util.cc加kAutoStartallowlist- handler 去掉
SetBoolean,只装服务 - HTML 保持
pref="{``{prefs.zero.browser.autostart}}"
不必做:
- 进页
getPref(除非实测有边界 case) - handler 里写 pref
给 Code Review 的一句话
Settings 页的 pref 读写必须走 settingsPrivate + allowlist 闭环;WebUI message 适合命令式副作用 (装服务),不适合替代 pref 的 CRUD。之前「设完重进仍显示关」,不是缺一段前端获取逻辑,而是 allowlist 未开导致读通道断路 ;C++
SetBoolean写的是另一条线,和 UI 的prefs模型无关。
验证清单
- 开开关 →
zero.browser.autostart= true,config.iniautostart_browser=1 - 关设置页再开 → 开关仍为开(方案一即可验证)
- 关开关 → pref false,config.ini 0
- 控制台
chrome.settingsPrivate.getPref('zero.browser.autostart')有返回值 - 开启时日志出现
LaunchZeroServiceInstall(服务通道独立工作)
相关文件
| 文件 | 作用 |
|---|---|
chrome/browser/extensions/api/settings_private/prefs_util.cc |
allowlist |
chrome/browser/resources/settings/basic_page/basic_page.html |
开关 UI |
chrome/browser/resources/settings/basic_page/basic_page.ts |
装服务触发 |
chrome/browser/ui/webui/settings/settings_startup_pages_handler.cc |
WebUI handler |
chrome/browser/360/se_profile.cc |
pref 监听 → config.ini |
chrome/common/360/pref_names_360.cc |
kAutoStart = zero.browser.autostart |
推荐:方案一。 它改动最小、机制最正、和 Chromium Settings 生态一致;方案二是可选加固;方案三应作为反例避免回归。