libpref

libpref 是一个通用的键值存储,用于实现偏好设置,这是一个涵盖多种事物的术语。

  • 功能启用/禁用标志(例如 xpinstall.signatures.required)。

  • 用户偏好设置(例如,从 about:preferences 设置的内容)

  • 内部应用程序参数(例如 javascript.options.mem.nursery.max_kb)。

  • 测试和调试标志(例如 network.dns.native-is-localhost)。

  • 可能需要在企业安装中锁定的事项。

  • 应用程序数据(例如 browser.onboarding.tour.onboarding-tour-addons.completedservices.sync.clients.lastSync)。

  • 一种廉价且简陋的 IPC 形式 (!)(一些开发者工具偏好设置)。

其中一些(特别是最后两个)并不是 libpref 的理想用途。

C++ API 位于 Preferences 类中。XPIDL API 位于 nsIPrefServicensIPrefBranch 接口中。

高级设计

键(也称为偏好设置名称)是 8 位字符串,在实践中为 ASCII。约定使用点分隔形式,例如 foo.bar.baz,但段没有内置含义。

命名不一致,例如段具有各种形式:foo_barfoo-barfooBar 等。功能标志的偏好设置名称也同样不一致:foo.enabledfoo.enablefoo.disablefooEnabledenable-foofoo.enabled.bar 等。

通过偏好设置名称段将偏好设置分组为族是临时性的。其中一些族密切相关,例如,每种语言脚本都存在许多字体偏好设置。

某些偏好设置只有在与其他偏好设置结合考虑时才有意义。

许多偏好设置名称在编译时已知,但有些是在运行时计算的。

基本值

偏好设置值的基类型为布尔值、32 位整数和 8 位 C 字符串。

字符串用于编码多种类型的数据:标识符、字母数字 ID、UUID、SHA1 哈希、CSS 颜色十六进制值、不适合 32 位整数的大整数(例如时间戳)、目录名称、URL、逗号分隔列表、空格分隔列表、JSON 块等。字符串值长度限制为 1 MiB;更长的字符串将被直接拒绝。

问题:C 字符串编码不明确;一些 API 函数处理不受限制的 8 位字符串(即 Latin1),但有些要求 UTF-8。

某些 API 支持伪造浮点数,方法是在获取/设置时将它们从字符串转换为字符串。

问题:整数和浮点数之间的混淆会导致错误。

每个偏好设置都包含一个默认值和/或一个用户值。默认值可以在启动时从文件初始化,并且可以通过 API 在运行时添加和修改。用户值可以在启动时从文件初始化,并且可以通过 API 和 about:config 在运行时添加、修改和删除。

如果两个值都存在,则对于大多数操作,用户值优先,尽管有一些操作专门针对默认值。

如果用户值设置为与默认值相同的值,则用户值将被删除,除非偏好设置在启动时标记为粘滞

问题:最好有一个明确的“重置为默认值”的概念,至少对于具有默认值的偏好设置而言。

偏好设置可以锁定。这将阻止它们被赋予用户值,或者如果存在用户值则隐藏现有的用户值。

复杂值

某些 API 支持一些复杂值。

nsIFile 对象通过将文件名存储为字符串来处理,类似于通过将浮点数存储为字符串来伪造浮点数的方式。

nsIPrefLocalizedString 对象是其默认值指定一个属性文件,该属性文件包含一个名称与 prefname 匹配的条目。获取时,该条目的值将放入用户值中。设置时,给定值只是覆盖用户值,就像字符串偏好设置一样。

问题:这很奇怪,与所有其他偏好设置类型都不一样。

nsIRelativeFilePref 对象仅在 comm-central 中使用。

Pref 分支

基于 XPIDL 对偏好设置的访问是通过 nsIPrefBranch/nsPrefBranch 进行的,它允许您指定偏好设置树的一个分支(例如 font.),并且偏好设置名称相对于该点起作用。

此 API 可以从 C++ 中使用,但对于 C++ 代码,还可以通过 Preferences 类进行直接访问,该类使用绝对偏好设置名称。

线程

在大多数情况下,所有基本 API 函数仅在主线程上工作。但是,有两个例外。

狭义的例外是 Servo 遍历线程被允许获取偏好设置值。这仅在主线程暂停时发生,这使其安全。(注意:错误 1474789 指出这可能不正确。)

广泛的例外是,静态偏好设置可以具有偏好设置值的缓存副本,该副本可以从其他线程访问。见下文。

通知

有一个通知 API 用于在偏好设置的值更改时得到通知。C++ 代码可以注册回调函数,而 JS 代码可以注册观察者(通过 nsIObserver,这需要 XPCOM)。在这两种情况下,注册的实体都会在命名偏好设置值更改时或在与给定前缀匹配的任何偏好设置的值更改时收到通知。例如,可以通过添加 font. 前缀匹配观察者来观察所有字体偏好设置更改。

另请参阅下面有关静态偏好设置的部分。

静态偏好设置

有一种特殊类型的偏好设置称为静态偏好设置。静态偏好设置在 StaticPrefList.yaml 中定义。有关更多文档,请参阅该文件。

如果在 StaticPrefList.yaml 和偏好设置数据文件中都定义了静态偏好设置,则后者定义将优先。偏好设置不应同时出现在 StaticPrefList.yamlall.js 中,但对于偏好设置同时出现在 StaticPrefList.yaml 和特定于应用程序的偏好设置数据文件(如 firefox.js)中可能是合理的。

每个静态偏好设置都有一个镜像类型。

  • always:与偏好设置关联一个 C++镜像变量。该变量始终与偏好设置值保持同步。此类型很常见。

  • once:与偏好设置关联一个 C++ 镜像变量。该变量在启动时与偏好设置的值同步一次,然后不再更改。此类型不太常见,主要用于图形偏好设置。

  • never:没有与偏好设置关联的 C++ 镜像变量。这与普通偏好设置非常相似。

alwaysonce 静态偏好设置只能用于具有布尔值/整数/浮点值的偏好设置,不能用于字符串或复杂值。

每个镜像变量都是只读的,可以通过 getter 函数访问。getter 函数的基本名称与偏好设置的名称相同,但将“.” 或“-”转换为“_”。有时会添加后缀,例如 _AtStartup 用于镜像 once 类型。

镜像变量有两个优点。首先,它们允许 C++ 和 Rust 代码直接从变量获取偏好设置值,而无需进行缓慢的哈希表查找,这对于经常咨询的偏好设置非常重要。其次,它们允许 C++ 和 Rust 代码从主线程之外获取偏好设置值。如果从主线程之外读取镜像变量,则该变量必须具有原子类型,并且断言会确保这一点。

请注意,镜像变量可以通过普通回调在没有 API 支持的情况下实现,除了一个细节:libpref 会将其回调优先于普通回调,确保任何静态偏好设置如果被普通回调读取都将是最新的。

问题:如果删除了偏好设置,静态偏好设置的镜像变量应该发生什么情况尚不清楚?目前缺少 NotifyCallbacks() 调用,因此镜像变量保留删除之前的其值。最干净的解决方案可能是禁止删除静态偏好设置。

已清理的偏好设置

我们限制某些偏好设置进入 Web 内容子进程。在这些进程中,偏好设置可能会被标记为“已清理”,以指示它可能有也可能没有用户值,但该值在该进程中不存在。在父进程中,没有偏好设置被标记为已清理。

偏好设置清理用于两个目的

  1. 保护可能存储在偏好设置中的私人用户数据免受幽灵攻击者的攻击。

  2. 减少 IPC 使用和常用修改偏好设置的线程唤醒次数。

如果偏好设置与拒绝列表匹配是动态命名的字符串偏好设置(未通过允许列表豁免),则该偏好设置将被清理以防止进入 Web 内容进程,请参阅 ShouldSanitizePreference(位于 Preferences.cpp 中)。

加载和保存

默认偏好设置值从各种偏好设置数据文件初始化。值得注意的是

  • modules/libpref/init/all.js,所有产品都使用;

  • browser/app/profile/firefox.js,Firefox 桌面版使用;

  • mobile/android/app/geckoview-prefs.js,GeckoView 使用;

  • 文件 mail/app/profile/all-thunderbird.js,由 Thunderbird (在 comm-central 中) 使用;

  • 文件 suite/browser/browser-prefs.js,由 SeaMonkey (在 comm-central 中) 使用。

在发布版本中,这些文件都被放入 omni.ja 中。

用户首选项值从用户配置文件中的 prefs.js 和(如果存在)user.js 初始化。这仅在父进程中发生一次。请注意,prefs.js 由 Firefox 管理,并定期覆盖。 user.js 由用户创建和管理,Firefox 仅读取它。

这些文件不是 JavaScript;.js 后缀出于历史原因而存在。它们由 libpref 中的自定义解析器读取。

用户首选项文件的语法比默认首选项文件的语法稍微严格一些。在用户首选项文件中,允许使用 user_pref 定义,但不允许使用 prefsticky_pref 定义,并且不允许使用属性(例如 locked)。

**问题:**geckodriver 在 mozprofile crate 中有单独的首选项解析器。

**问题:**这些文件既没有语法版本也没有数据版本。这使得更改文件格式变得困难。

有 API 函数可以同步或异步(通过非主线程运行程序)保存修改后的首选项,保存到默认文件 (prefs.js) 或命名文件。当保存到默认文件时,如果没有修改首选项,则不会执行任何操作。

此外,每当修改首选项时,我们都会等待 500 毫秒,然后自动执行非主线程保存操作到 prefs.js。这提供了对 [持久性](https://en.wikipedia.org/wiki/ACID#Durability) 的近似值,但仍然可能出现某些错误(例如,父进程崩溃),并最终导致最近更改的首选项未保存。(如果发生这种情况,则会损害 [原子性](https://en.wikipedia.org/wiki/ACID#Atomicity),即一系列相关的多个首选项更改可能仅被部分写入。)

仅将值已从默认值更改的首选项保存到 prefs.js 中。

**问题:**每次保存首选项时,整个文件都会被覆盖——即使仅更改了一个值,也会覆盖 10s 或甚至 100s KiB 的数据。由于同步,这至少每 5 分钟发生一次。此外,在启动期间和启动后不久,各种首选项都会发生更改,这可能导致 10s MiB 的磁盘活动。

about:support

about:support 包含一个“已修改的重要首选项”表。它包含所有满足以下条件的首选项:(a) 其值已从默认值更改;(b) 其前缀与 Troubleshoot.sys.mjs 中的白名单匹配。匹配白名单是为了避免公开可能涉及隐私的首选项值。

**问题:**前缀的白名单与首选项本身分开指定。在首选项定义中添加一个属性会更好。

同步

在桌面设备上,如果存在相应的以 services.sync.prefs.sync. 为前缀的首选项,则首选项会通过同步同步到设备上。例如,如果首选项 services.sync.prefs.sync.foo.bar 存在且为 true,则首选项 foo.bar 将被同步。

以前,即使本地不存在以 services.sync.prefs.sync. 为前缀的首选项,也可以将首选项推送到设备上;但是,这种行为在 [bug 1538015](https://bugzilla.mozilla.org/show_bug.cgi?id=1538015) 中发生了变化,现在需要本地存在以 services.sync.prefs.sync. 为前缀的首选项。可以通过在目标浏览器上将单个首选项 services.sync.prefs.dangerously_allow_arbitrary 设置为 true 来重新启用旧的(不安全的)行为——随后,可以通过创建远程以 services.sync.prefs.sync. 为前缀的首选项将任何首选项推送到那里。

实际上,默认情况下,只有少量(约 70 个)首选项具有以 services.sync.prefs.sync. 为前缀的首选项。

**问题:**这很糟糕。在首选项定义中添加一个属性会更好,但在这一点上可能很难更改。

同步的首选项数量很少,因为首选项是在不同版本之间同步的;任何含义可能发生变化的首选项都不应该同步。此外,我们不会同步可能在不同设备(例如台式机与笔记本电脑)上不同的首选项。

移动设备上不会同步首选项。

Rust

可以通过 static_prefs::pref! 宏从 Rust 代码访问静态首选项镜像变量,前提是首选项使用 rust: true 选择加入此功能。其他首选项目前无法访问。libpref 的部分 C++ API 可以通过 C 绑定(手工制作或生成的)相当直接地提供给 Rust 代码。

首选项的成本

单个首选项的成本很低,但几千个首选项的成本相当高,包括以下内容。

  • 启动时解析和初始化。

  • 启动时和首选项值更改时的 IPC 成本。

  • 首选项值更改的磁盘写入成本,尤其是在启动期间。

  • 存储首选项、回调和观察者以及 C++ 镜像变量的内存使用情况。

  • 复杂性:大多数首选项组合都未经测试。某些首选项可以通过好奇的用户设置为错误的值,这可能产生 [严重影响](https://rejzor.wordpress.com/2015/06/14/improve-firefox-html5-video-playback-performance/)(阅读评论)。首选项也可能存在错误。现实生活中的例子包括拼写错误的首选项名称、all.js 条目类型错误(例如,混淆 int 与 float),这两者都意味着通过 about:config 或 API 更改首选项值将无效(请参阅 [bug 1414150](https://bugzilla.mozilla.org/show_bug.cgi?id=1414150) 以了解这两个方面的示例)。

  • 同步首选项的同步成本。

指南

我们的首选项太多了。这至少部分是因为长期以来,我们一直存在“如有疑问,添加首选项”的文化。此外,我们没有任何系统——无论是技术上的还是文化上的——来删除不必要的首选项。请参阅 [bug 90440](https://bugzilla.mozilla.org/show_bug.cgi?id=90440),了解一个 17 年来从未使用过的首选项。

简而言之,首选项是 Firefox 等同于 Windows 注册表的东西:任何事物的垃圾场。我们应该有关于何时添加首选项的指南。

以下是一些添加首选项的充分理由。

  • 用户可能确实希望更改它。例如,它控制可在 about:preferences 中调整的功能。

  • 启用/禁用新功能。功能成熟后,考虑删除首选项。首选项过期机制将对此有所帮助。

  • 用于某些测试/调试标志。理想情况下,这些标志在 about:config 中不可见。

以下是一些添加首选项的不那么充分的理由。

  • 我不确定此数值参数(缓存大小、超时等)。确定!实际上,很少有用户会更改它。添加首选项并不能免除您找到良好默认值的责任。然后将其设为代码常量。

  • 我需要在开发过程中尝试不同的参数。这是合理的,但请考虑在落地之前或功能成熟后删除首选项。过期机制将对此有所帮助。

  • 我有时会调整此值进行调试或测试。为了偶尔节省重新编译的时间,值得将其公开给全世界吗?考虑将其设为代码常量。

  • 在不同平台上需要不同的值。这可以通过其他方式完成,例如 C++ 代码中的 #ifdef

这些指南不考虑应用程序数据首选项(即通常没有默认值的首选项)。它们与其他类型的首选项非常不同。它们可能根本不应该成为首选项,而应该通过其他机制存储。

底层细节

关键思想是首选项数据库由两部分组成。第一部分是在创建第一个子进程时创建的首选项值的初始快照。此快照存储在不可变的共享内存中,并由所有进程共享。

此后发生的首选项值更改存储在第二个哈希表中。每个进程都有自己的此哈希表的副本。当父进程中的首选项值发生更改时,它会执行 IPC 以通知子进程更改,以便它们可以更新其副本。

此设计的动机是内存使用。每个子进程都拥有首选项数据库的完整副本是不可行的。

并非所有子进程都需要访问首选项。需要访问首选项的子进程包括 Web 内容进程、GPU 进程和 RDD 进程。

父进程启动

父进程最初仅具有哈希表。

在启动初期,父进程将所有静态首选项和默认首选项(主要来自 omni.ja)加载到该哈希表中。父进程还为静态首选项注册 C++ 镜像变量,初始化它们,并注册回调,以便它们在所有后续更新中都得到适当的更新。

在启动稍晚一点的时候,父进程加载所有用户首选项文件,主要来自配置文件目录。

当第一次调用 once 静态首选项的 getter 时,所有 once 静态首选项的镜像变量都将被设置,并且特殊的冻结首选项将被放入哈希表中。这些冻结首选项是 once 首选项的副本,其名称中添加了 $$$ 前缀和后缀。它们也被特别标记,以便在除了启动新子进程之外的所有情况下都忽略它们。它们的存在是为了让所有子进程都能获得与父进程相同的 once 值。

子进程启动(父进程端)

创建第一个子进程时,父进程会将其大部分哈希表序列化为共享的、不可变的快照。此快照存储在由 SharedPrefMap 实例管理的共享内存区域中。

经过清理的首选项(与动态命名的启发式算法的拒绝列表匹配)不包含在共享内存区域中。在构建共享内存区域后,父进程会清除哈希表,然后将清理后的首选项重新输入到其中。除了清理后的首选项外,哈希表随后仅用于存储已更改的首选项值。

创建任何子进程时,父进程会序列化哈希表中存在的所有首选项值(即自创建快照以来已更改的值),除了清理后的首选项之外,并将它们存储在第二个短暂的共享内存区域中。这表示子进程需要应用到快照上的更改集,并允许它构建一个哈希表,该哈希表应该与父进程的哈希表完全匹配,除了清理后的首选项。

父进程将两个文件描述符传递给子进程,每个内存区域一个。快照对所有子进程都是相同的。

子进程启动(子进程端)

在子进程启动初期,首选项服务会映射并反序列化父进程发送的两个共享内存区域,但会延迟进一步的初始化,直到 XPCOM 初始化请求。一旦发生这种情况,就会为静态首选项初始化镜像变量,但不会在哈希表中设置任何默认值,也不会加载任何首选项文件。

初始化镜像变量后,我们会为共享快照中任何具有用户值或已锁定的首选项分派首选项更改回调。这会导致镜像变量更新。

之后,从父进程(通过 changedPrefsFd)接收到的已更改的首选项值将添加到首选项数据库中。它们的值会覆盖快照中的值,并且会根据需要为它们分派首选项更改回调。once 镜像变量将从特殊的冻结首选项值初始化。

首选项查找

每个首选项数据库都具有哈希表和共享内存快照。给定的首选项可能在这两者中的一个或两个中都有条目。如果首选项在这两个中都存在,则哈希表条目优先。

对于首选项查找,首先检查哈希表,然后检查共享快照。哈希表中的条目可能具有类型 None,在这种情况下,首选项被视为不存在。静态快照中的条目永远不会具有类型 None

对于首选项枚举,两个映射都会被枚举,从哈希表开始。在迭代哈希表时,任何类型为None的条目都会被跳过。在迭代共享快照时,任何在哈希表中也存在的条目都会被跳过。这两个迭代的组合结果代表了首选项数据库的完整内容。

首选项更改

首选项更改只能在父进程中发起。如果在父进程之外运行,所有修改首选项的 API 方法都会以明显的错误(使用NS_ERROR)失败。

在创建初始快照之前发生的首选项更改很简单,并且发生在哈希表中。没有共享快照需要更新,也没有子进程需要同步。

一旦创建了快照,任何更改都需要在哈希表中进行。

如果已更改的首选项的条目已存在于哈希表中,则可以直接更新该条目。同样,对于哈希表或共享快照中都不存在的首选项:可以创建一个新的哈希表条目。

当已更改的首选项存在于快照中但不存在于哈希表中时,需要更加小心。在这种情况下,我们使用与快照条目相同的值创建一个哈希表条目,然后更新它……但*仅当*更改会产生影响时。如果调用者尝试将首选项设置为其现有值,我们不希望浪费内存创建不必要的哈希表条目。

必须告知内容进程任何可见的首选项值更改。(对默认值进行更改但被用户值隐藏则无关紧要。)发生这种情况时,ContentParent通过观察者检测到更改。经过清理的首选项不会产生更新;对于字符串首选项,它还会检查值不超过 4 KiB。如果检查通过,它会向子进程发送 IPC 消息(PreferenceUpdate),子进程会相应地更新首选项(默认值和用户值)。

问题:前缀的拒绝列表与首选项本身分开指定。在首选项定义中添加一个属性会更好。

问题:4 KiB 的限制可能导致父进程和子进程之间出现不一致。例如,请参阅错误 1303051

首选项删除

首选项删除更为复杂。如果要删除的首选项仅存在于父进程的哈希表中,则可以简单地删除其条目。但是,如果它存在于共享快照中,则需要保留(或创建)其哈希表条目,并将其类型更改为None。此条目的存在会屏蔽快照条目,导致首选项枚举器忽略它。