WTF 与内存管理:WebKit 的基础库
前置知识
- ›C++ 模板与智能指针(std::shared_ptr / std::weak_ptr 的相关概念)
- ›C++ 移动语义与 RAII
- ›第 1 篇:探索 WebKit —— 架构概览与代码库结构
WTF 与内存管理:WebKit 的基础库
WebKit 的每一个组件——从 JavaScript 引擎到 DOM 实现,再到 IPC 层——都建立在 WTF(Web Template Framework)之上。正如第 1 篇所介绍的,WTF 位于构建栈的第 2 层,直接处于 bmalloc 之上。它提供了容器类、智能指针、字符串类型和线程原语,既替代了 C++ 标准库中的对应设施,在许多场景下性能也更胜一筹。
本文将重点介绍贯穿整个代码库的所有权模型。只要深入理解 RefCounted、Ref、RefPtr、WeakPtr 以及 ProtectedThis 模式,你就能毫无障碍地读懂 WebKit 中几乎任何一个头文件。
WTF 存在的意义:替代标准库
WebKit 的诞生早于许多现代 C++ 特性。但 WTF 延续至今的原因,远不止历史惯性那么简单:
- 性能可控。
WTF::Vector支持内联存储缓冲区,对小尺寸数据可完全避免堆内存分配——在 DOM 树构建过程中需要创建数百万个小型 vector 时,这一优化至关重要。 - 安全加固。 标准容器无法与 bmalloc 的类型隔离分配机制集成,而 WTF 容器可以,具体通过
WTF_MAKE_TZONE_ALLOCATED等宏实现。 - 语义一致。
WTF::HashMap采用 Robin Hood 哈希的开放寻址方案,比基于链表节点的std::unordered_map具有更好的缓存局部性。 - WebKit 专属的线程安全保证。 WTF 智能指针在调试模式下内置了线程断言,可在开发阶段及时发现跨线程误用的问题。
官方 Introduction.md 将 WTF 描述为提供"常用容器类(如 Vector、HashMap、HashSet)以及在 WebKit 其余部分广泛使用的智能指针类型(如 Ref、RefPtr 和 WeakPtr)"。
提示: 当你在 WebKit 代码中看到直接使用标准库类型(如
std::unique_ptr)时,这是有意为之的——它表明此处只需单一所有权语义,不涉及引用计数。基于引用计数的共享所有权始终使用Ref/RefPtr。
引用计数:RefCounted、Ref 与 RefPtr
WebCore 中大多数对象使用的是侵入式引用计数,而非垃圾回收。基类 RefCountedBase 内部存储一个初始值为 1 的 uint32_t 引用计数:
class RefCountedBase {
public:
void ref() const { ++m_refCount; }
bool hasOneRef() const { return m_refCount == 1; }
protected:
bool derefBase() const {
auto tempRefCount = m_refCount - 1;
if (!tempRefCount) return true; // caller should delete
m_refCount = tempRefCount;
return false;
}
private:
mutable uint32_t m_refCount { 1 };
};
模板类 RefCounted<T> 继承自 RefCountedBase,并添加了 deref() 方法——当计数归零时负责销毁对象:
template<typename T> class RefCounted : public RefCountedBase {
public:
void deref() const {
if (derefBase())
delete const_cast<T*>(static_cast<const T*>(this));
}
};
管理这些对象的智能指针有两种:
classDiagram
class RefCountedBase {
+ref()
+deref()
-uint32_t m_refCount
}
class RefCounted~T~ {
+deref()
}
class Ref~T~ {
-T* m_ptr
+Ref(T& object)
+operator*()
+operator->()
}
class RefPtr~T~ {
-T* m_ptr
+RefPtr(T* ptr)
+get() T*
+operator bool()
}
RefCountedBase <|-- RefCounted
RefCounted <.. Ref : "manages"
RefCounted <.. RefPtr : "manages"
Ref<T>— 不可为空。构造时调用ref(),析构时调用deref(),不能默认构造。当你确定指针指向一个存活对象时,使用它。RefPtr<T>— 可为空。行为与Ref<T>相同,但允许持有nullptr。适用于可选所有权的场景。
WebKit 中标准的工厂模式是将构造函数设为私有,并提供一个静态 create() 方法:
class MyObject : public RefCounted<MyObject> {
public:
static Ref<MyObject> create() { return adoptRef(*new MyObject); }
private:
MyObject() = default;
};
其中 adoptRef() 的调用至关重要——它将新分配对象(初始引用计数为 1)的所有权转移给 Ref,而不会再次递增计数。如果省略 adoptRef,引用计数将变为 2,从而导致内存泄漏。
弱引用:WeakPtr 与 CanMakeWeakPtr
并非所有引用都需要延长对象的生命周期。WeakPtr<T> 提供了一种非持有引用,当所指对象被销毁后,它会自动变为空。
要让一个类支持弱指针,只需继承 CanMakeWeakPtr<T>:
class MyWidget : public CanMakeWeakPtr<MyWidget> {
// WeakPtr<MyWidget> can now point to instances of this class
};
其内部实现采用工厂模式。CanMakeWeakPtrBase 持有一个 WeakPtrFactory,该工厂会懒初始化一个共享控制块(WeakPtrImpl)。当对象被销毁时,工厂将 impl 置空,所有现有的 WeakPtr 实例在调用 get() 时都会返回 nullptr。
sequenceDiagram
participant Owner as Owner Code
participant WP as WeakPtr<T>
participant Impl as WeakPtrImpl
participant Obj as T (CanMakeWeakPtr)
Owner->>Obj: Create WeakPtr
Obj->>Impl: Lazily create impl
Owner->>WP: Store WeakPtr
WP->>Impl: Points to shared impl
Note right of Obj: Object is alive
Owner->>WP: wp.get()
WP->>Impl: Read pointer
Impl-->>WP: Returns T*
Note right of Obj: Object destroyed
Obj->>Impl: Nullify pointer
Owner->>WP: wp.get()
WP->>Impl: Read pointer
Impl-->>WP: Returns nullptr
WTF 还提供了 WeakHashSet<T> 和 WeakHashMap<K, V>——这些集合会自动清除已失效的条目。它们在 WebCore 的观察者模式中被大量使用,确保被观察对象不会阻止观察者被销毁。
提示:
WeakPtr头文件中明确指出,由于多了一层间接寻址,其性能比RefPtr差。在热路径中,优先选用RefPtr或CheckedPtr。
ProtectedThis 模式
在 WebKit 代码库中,protectedThis 是最常见的模式之一。它专门用来防范一种隐蔽而危险的 bug:对象在回调执行过程中将自身销毁。
以 DOM 容器节点移除子节点为例。移除操作可能触发 mutation 事件,事件处理中可能执行任意 JavaScript 代码,而这段脚本可能恰好释放了容器节点的最后一个引用。若没有保护措施,方法后续的代码将在已释放的内存上继续执行。
解决方法很简单,在方法开头加上一行:
Ref<ContainerNode> protectedThis(*this);
这会临时递增引用计数,确保 this 在函数结束、protectedThis 离开作用域之前始终存活。Introduction.md 以 ContainerNode::removeChild 为典型示例,对这一模式进行了详细说明。
sequenceDiagram
participant Caller
participant Container as ContainerNode
participant JS as JavaScript Engine
Caller->>Container: removeChild(oldChild)
Container->>Container: Ref protectedThis(*this)<br/>refCount: 1→2
Container->>Container: removeNodeWithScriptAssertion()
Container->>JS: Dispatch mutation event
JS->>JS: Script drops last external ref
Note right of Container: refCount: 2→1 (still alive!)
Container->>Container: dispatchSubtreeModifiedEvent()
Container->>Container: ~protectedThis()<br/>refCount: 1→0→delete
如果没有这个保护,引用计数会在 JavaScript 回调执行期间降为 0,dispatchSubtreeModifiedEvent() 的调用将造成 use-after-free 漏洞。
核心容器:Vector 与 HashMap
WTF 针对 WebKit 的使用场景,提供了一套经过优化的标准容器替代品。
WTF::Vector 是一个动态数组,其核心特性是可配置的内联存储缓冲区。Vector<int, 8> 可直接在 vector 对象内部存储最多 8 个元素,完全不涉及堆内存分配。由于许多 DOM 操作会创建大量小型临时 vector,这一设计在页面加载过程中可消除数以百万计的内存分配开销。
WTF::HashMap 采用支持自定义哈希特性的开放寻址方案,缓存表现优于 std::unordered_map 的链式结构。它以 KeyTraits 和 MappedTraits 为模板参数,允许精细控制空值与已删除值的表示方式,避免为哨兵值浪费额外内存。
| 容器 | 标准库对应 | 主要差异 |
|---|---|---|
WTF::Vector<T, N> |
std::vector<T> |
内联存储 N 个元素,超出后溢出到堆 |
WTF::HashMap<K, V> |
std::unordered_map<K, V> |
开放寻址,Robin Hood 哈希 |
WTF::HashSet<T> |
std::unordered_set<T> |
与 HashMap 相同的开放寻址策略 |
WTF::String |
std::string |
引用计数,支持 8 位/16 位表示,不可变 |
WTF::Deque<T> |
std::deque<T> |
循环缓冲区实现 |
bmalloc 与 IsoHeap:面向安全的内存分配
位于技术栈最底层的 bmalloc 是 WebKit 的自定义内存分配器。其最重要的安全特性是 IsoHeap(隔离堆)——一种将每种 C++ 类型隔离到专属堆页面上的机制。
为什么这很重要?Use-after-free 是浏览器安全漏洞中最常见的一类。攻击者触发 UAF 后,通常会尝试用另一种对象类型填充已释放的内存,从而制造类型混淆。如果 HTMLInputElement 和 JSFunction 共享同一批堆页面,攻击者就可以将已释放的 input element 内存重新分配为 function 对象,进而劫持控制流。
IsoHeap 从根本上解决了这个问题:用于 HTMLInputElement 分配的页面只会容纳 HTMLInputElement 对象,释放的槽位只能被相同类型重新使用。
启用该机制只需通过 WTF_MAKE_TZONE_ALLOCATED 宏声明:
class MySecuritySensitiveObject {
WTF_MAKE_TZONE_ALLOCATED(MySecuritySensitiveObject);
public:
// ...
};
当 USE(TZONE_MALLOC) 启用时(Apple 平台上为默认状态),所有内存分配都会通过 bmalloc 的类型隔离分配器处理;禁用时则回退到 FastMalloc。
flowchart TD
A["new HTMLInputElement"] --> B{USE_TZONE_MALLOC?}
B -->|Yes| C["bmalloc TZoneHeap<br/>Type-segregated page"]
B -->|No| D["FastMalloc<br/>General-purpose allocator"]
C --> E["Page contains ONLY<br/>HTMLInputElement objects"]
F["new JSFunction"] --> B2{USE_TZONE_MALLOC?}
B2 -->|Yes| G["bmalloc TZoneHeap<br/>Different type-segregated page"]
G --> H["Page contains ONLY<br/>JSFunction objects"]
WTF_MAKE_TZONE_ALLOCATED 几乎出现在 WebCore 和 WebKit 的每一个重要类中。例如,LayoutContext 在第 48 行声明了它。JavaScriptCore 中的 Lexer 由于是模板类,则使用 WTF_MAKE_TZONE_ALLOCATED_TEMPLATE。
衔接下一篇
至此,你已经掌握了 WebKit 的所有权词汇体系:Ref 用于保证存活的非空所有权,RefPtr 用于可空所有权,WeakPtr 用于非持有的观察引用,ProtectedThis 用于在回调中保护对象自身不被销毁。这些模式贯穿代码库的每一个角落。
下一篇文章将沿着构建栈向上一层,深入 JavaScriptCore——WebKit 的 JavaScript 引擎。我们将追踪一个 JavaScript 源文件经历完整四级执行流水线的全过程:LLInt 解释器、Baseline JIT、推测优化的 DFG JIT,以及由 B3 编译器驱动的最高吞吐量 FTL JIT。本文介绍的 WTF 类型,正是 JSC 赖以构建的基石。