JS 封装器和 IDL 文件¶
概述¶
除了典型的 C++ 翻译单元 (.cpp) 和 C++ 头文件 (.cpp) 以及一些 Objective-C 和 Objective-C++ 文件外,WebCore 还包含数百个 Web IDL (.idl) 文件。Web IDL 是一种接口描述语言,用于定义 WebKit 中实现的 JavaScript API 的结构和行为。
在构建 WebKit 时,一个 perl 脚本 会在 WebKitBuild/Debug/DerivedSources/WebCore/ 目录下生成与这些 IDL 文件对应的 C++ 翻译单元和 C++ 头文件,其中 Debug 是当前的构建配置(例如,它可以是 Release-iphonesimulator)。
这些自动生成的文件以及手动编写的 Source/WebCore/bindings 文件统称为 JS DOM 绑定代码,它们为底层结构和行为用 C++ 编写的对象和概念实现了 JavaScript API。
例如,Node 的 C++ 实现是 Node 类,其 JavaScript 接口由 JSNode 类实现。对于调试构建,该类的声明和大部分定义都在 WebKitBuild/Debug/DerivedSources/WebCore/JSNode.h 和 WebKitBuild/Debug/DerivedSources/WebCore/JSNode.cpp 中自动生成。它还在 Source/WebCore/bindings/js/JSNodeCustom.cpp 中包含一些自定义、手动编写的绑定代码。类似地,Range 接口的 C++ 实现是 Range 类,而其 JavaScript API 由自动生成的 JSRange 类实现(对于调试构建,位于 WebKitBuild/Debug/DerivedSources/WebCore/JSRange.h 和 WebKitBuild/Debug/DerivedSources/WebCore/JSRange.cpp)。我们称这些 JSX 类的实例为 X 的*JS 封装器*。
这些 JS 封装器存在于我们称之为 DOMWrapperWorld 的环境中。每个 DOMWrapperWorld 为每个 C++ 对象都有自己的 JS 封装器。因此,一个 C++ 对象可能在不同的 DOMWrapperWorld 中拥有多个 JS 封装器。最重要的 DOMWrapperWorld 是主 DOMWrapperWorld,它运行 WebKit 加载的网页脚本,而其他 DOMWrapperWorld 通常用于运行浏览器扩展代码和嵌入 WebKit 的应用程序注入的其他代码。
JSX.h 提供了 toJS 函数,它在给定全局对象的 DOMWrapperWorld 中为 X 创建一个 JS 封装器,以及 toWrapped 函数,它返回底层的 C++ 对象。例如,Node 的 toJS 函数定义在 Source/WebCore/bindings/js/JSNodeCustom.h 中。
当给定 C++ 对象已经存在 JS 封装器对象时,toJS 函数将在给定 DOMWrapperWorld 的哈希映射中找到适当的 JS 封装器。由于哈希映射查找开销较大,一些 WebCore 对象继承自 ScriptWrappable,该类包含一个指向主世界 JS 封装器的内联指针(如果该封装器已创建)。
JS 封装器生命周期管理¶
通常,JS 封装器通过 WebCore 中所有 JS 封装器都继承的 JSDOMWrapper 模板类中的引用计数机制来保持其底层 C++ 对象的生命周期。但是,C++ 对象不会通过自身保持其在每个世界中对应的 JS 封装器的生命周期,因为这种循环依赖会导致内存泄漏。
WebCore 中有两种主要的机制来保持 JS 封装器的生命周期:
- 访问子节点 - 当 JavaScriptCore 的垃圾回收器在标记阶段访问某个 JS 封装器时,同时访问需要保持生命周期的其他 JS 封装器或 JS 对象。
- 可从不透明根访问 - 告知 JavaScriptCore 的垃圾回收器,某个 JS 封装器可从在标记阶段添加到不透明根集合中的不透明根访问。
访问子节点¶
访问子节点 是当一个 JS 封装器需要保持另一个 JS 封装器或 JS 对象的生命周期时我们使用的机制。
例如,ErrorEvent 对象在 Source/WebCore/bindings/js/JSErrorEventCustom.cpp 中使用此方法来保持其 "error" IDL 属性的生命周期,如下所示:
template<typename Visitor>
void JSErrorEvent::visitAdditionalChildren(Visitor& visitor)
{
wrapped().originalError().visit(visitor);
}
DEFINE_VISIT_ADDITIONAL_CHILDREN(JSErrorEvent);
这里,DEFINE_VISIT_ADDITIONAL_CHILDREN 宏生成 visitAdditionalChildren 的模板实例,该函数由 JavaScriptCore 的垃圾回收器调用。当垃圾回收器访问 ErrorEvent 对象的实例时,它还会访问 wrapped().originalError(),即 "error" 属性的 JavaScript 值。
class ErrorEvent final : public Event {
...
const JSValueInWrappedObject& originalError() const { return m_error; }
SerializedScriptValue* serializedError() const { return m_serializedError.get(); }
...
JSValueInWrappedObject m_error;
RefPtr<SerializedScriptValue> m_serializedError;
bool m_triedToSerialize { false };
};
请注意,JSValueInWrappedObject 使用 Weak,它本身不会保持引用对象的生命周期。我们不能使用像 Strong 这样的引用类型,因为它本身会保持引用对象的生命周期,而存储的 JS 对象也可能将此 ErrorEvent 对象作为其属性存储。由于垃圾回收器无法知道或清除 Strong 引用或假设版本的 ErrorEvent 中对 ErrorEvent 的属性,它将永远无法收集这两个对象,从而导致内存泄漏。
要使用此方法来保持 JavaScript 对象或封装器的生命周期,请将 JSCustomMarkFunction 添加到 IDL 文件中,然后在 Source/WebCore/bindings/js 下引入 JS*Custom.cpp 文件,并实现 template<typename Visitor> void JS*Event::visitAdditionalChildren(Visitor& visitor),如上述 ErrorEvent 所示。
visitAdditionalChildren 是在主线程运行时并发调用的。在 visitAdditionalChildren 中进行的任何操作都需要是多线程安全的。例如,它不能增加或减少 RefCounted 对象的引用计数,也不能从 CanMakeWeakPtr 创建新的 WeakPtr,因为这些 WTF 类不是线程安全的。
不透明根¶
可从不透明根访问 是当我们有一个底层 C++ 对象并希望保持其他 C++ 对象的 JS 封装器的生命周期时使用的机制。
为了理解原因,让我们考虑一个 StyleSheet 对象。只要这个对象还活着,我们还需要保持 ownerNode 属性返回的 DOM 节点的生命周期。此外,对象本身也需要保持生命周期,只要所有者节点还活着,因为这个 [StyleSheet 对象] 可以通过所有者节点的 sheet IDL 属性访问。如果我们使用*访问子节点*机制,那么每当垃圾回收器访问这个 StyleSheet 对象时,我们都需要访问所有者节点的每个 JS 封装器;每当垃圾回收器访问所有者节点时,我们都需要访问 StyleSheet 对象的每个 JS 封装器。但要做到这一点,我们需要查询每个 DOMWrapperWorld 的封装器映射,以查看是否存在 JavaScript 封装器。这是一个需要一直发生的昂贵操作,并且在 Node 和 StyleSheet 对象之间创建了紧密耦合,因为每个 JS 封装器对象都需要知道其他对象的存在。
不透明根 通过让垃圾回收器知道,只要垃圾回收器遇到了该 JavaScript 封装器关心的特定不透明根(即使垃圾回收器没有直接访问该 JavaScript 封装器),该 JavaScript 封装器就需要保持生命周期,从而解决了这些问题。不透明根只是一个 void* 标识符,垃圾回收器在每个标记阶段都会跟踪它,并且它不符合特定的接口或行为。它本来可以是一个任意的整数值,但出于方便起见使用了 void*,因为活动对象的指针值是唯一的。
在 StyleSheet 对象的情况下,只要垃圾回收器访问 ownerNode,StyleSheet 的 JavaScript 封装器就会告诉垃圾回收器它需要保持生命周期,因为一个它关心的不透明根已经被遇到。
在最简单的模型中,这种情况下的不透明根将是 ownerNode 本身。然而,每个 Node 对象也必须保持其父节点、兄弟节点和子节点的生命周期。为此,每个 Node 都将根节点指定为其不透明根。Node 和 StyleSheet 对象都使用这个唯一的不透明根作为与垃圾回收器通信的方式。
例如,当被要求在 JSStyleSheetCustom.cpp 中访问其子节点时,StyleSheet 对象会告知垃圾回收器这个不透明根。
template<typename Visitor>
void JSStyleSheet::visitAdditionalChildren(Visitor& visitor)
{
visitor.addOpaqueRoot(root(&wrapped()));
}
这里,void* root(StyleSheet*) 返回 StyleSheet 对象的不透明根,如下所示:
inline void* root(StyleSheet* styleSheet)
{
if (CSSImportRule* ownerRule = styleSheet->ownerRule())
return root(ownerRule);
if (Node* ownerNode = styleSheet->ownerNode())
return root(ownerNode);
return styleSheet;
}
然后,在 JSStyleSheet.cpp(对于调试构建,位于 WebKitBuild/Debug/DerivedSources/WebCore/JSStyleSheet.cpp)中,JSStyleSheetOwner(一个用于与垃圾回收器通信的辅助 JavaScript 对象)告诉垃圾回收器,只要垃圾回收器遇到了这个 StyleSheet 的不透明根,JSStyleSheet 就应该保持活动。
bool JSStyleSheetOwner::isReachableFromOpaqueRoots(JSC::Handle<JSC::Unknown> handle, void*, AbstractSlotVisitor& visitor, const char** reason)
{
auto* jsStyleSheet = jsCast<JSStyleSheet*>(handle.slot()->asCell());
void* root = WebCore::root(&jsStyleSheet->wrapped());
if (UNLIKELY(reason))
*reason = "Reachable from jsStyleSheet";
return visitor.containsOpaqueRoot(root);
}
通常,使用不透明根来保持 JavaScript 封装器的生命周期涉及两个步骤:
- 在
visitAdditionalChildren中添加不透明根。 - 当找到相关不透明根时,在
isReachableFromOpaqueRoots中返回 true。
第一步可以通过使用前面提到的 JSCustomMarkFunction 和 visitAdditionalChildren 来实现。或者,更优选的是,可以将 GenerateAddOpaqueRoot 添加到 IDL 接口中以自动生成此代码。例如,AbortController.idl 使用此 IDL 属性如下:
[
Exposed=(Window,Worker),
GenerateAddOpaqueRoot=signal
] interface AbortController {
[CallWith=ScriptExecutionContext] constructor();
[SameObject] readonly attribute AbortSignal signal;
[CallWith=GlobalObject] undefined abort(optional any reason);
};
这里,signal 是底层 C++ 对象的一个公共成员函数。
class AbortController final : public ScriptWrappable, public RefCounted<AbortController> {
WTF_MAKE_ISO_ALLOCATED(AbortController);
public:
static Ref<AbortController> create(ScriptExecutionContext&);
~AbortController();
AbortSignal& signal();
void abort(JSDOMGlobalObject&, JSC::JSValue reason);
private:
explicit AbortController(ScriptExecutionContext&);
Ref<AbortSignal> m_signal;
};
当 GenerateAddOpaqueRoot 在没有指定任何值的情况下被指定时,它会自动调用 opaqueRoot()。
与 visitAdditionalChildren 类似,添加不透明根是并发进行的,同时主线程正在运行。在 visitAdditionalChildren 中执行的任何操作都需要是多线程安全的。例如,它不能增加或减少 RefCounted 对象的引用计数,也不能从 CanMakeWeakPtr 创建新的 WeakPtr,因为这些 WTF 类的修改操作不是线程安全的。
第二步可以通过在 IDL 文件中添加 CustomIsReachable 并在 JS*Custom.cpp 文件中实现 JS*Owner::isReachableFromOpaqueRoots 来实现。或者,更优选的是,可以将 GenerateIsReachable 添加到 IDL 文件中以自动生成此代码,并带有以下值:
- 无值 - 将对类型 T 的底层 C++ 对象调用
root(T*)的结果作为不透明根添加。 Impl- 将底层 C++ 对象作为不透明根添加。ReachableFromDOMWindow- 将window()返回的DOMWindow作为不透明根添加。ReachableFromNavigator- 将navigator()返回的Navigator作为不透明根添加。ImplDocument- 将document()返回的Document作为不透明根添加。ImplElementRoot- 将element()返回的Element的根节点作为不透明根添加。ImplOwnerNodeRoot- 将ownerNode()返回的Node的根节点作为不透明根添加。ImplScriptExecutionContext- 将scriptExecutionContext()返回的ScriptExecutionContext作为不透明根添加。
与访问子节点或添加不透明根类似,是否可从不透明根访问是并行检查的。但是,它发生在主线程暂停时,与访问子节点或添加不透明根不同,后者在主线程运行时并发发生。这意味着 JS*Owner::isReachableFromOpaqueRoots 或由 GenerateIsReachable 调用的任何函数中的任何操作都不能具有线程不安全的副作用,例如增加或减少 RefCounted 对象的引用计数,或从 CanMakeWeakPtr 创建新的 WeakPtr,因为这些 WTF 类的修改操作不是线程安全的。
活动 DOM 对象¶
访问子节点和不透明根是表达 JS 封装器之间生命周期关系的绝佳方式,但在某些情况下,JS 封装器需要独立于其他对象而保持活动。考虑 XMLHttpRequest。在以下示例中,JavaScript 丢失了对 XMLHttpRequest 对象及其事件监听器的所有引用,但当接收到新响应时,对象上将分派一个事件,重新引入对对象的新 JavaScript 引用。也就是说,即使没有与其他“根”对象的任何关联,该对象也能在垃圾回收的标记和清除循环中存活下来。
function fetchURL(url, callback)
{
const request = new XMLHttpRequest();
request.addEventListener("load", callback);
request.open("GET", url);
request.send();
}
在 WebKit 中,我们认为此类对象具有*挂起活动*。表达这种挂起活动的存在是 ActiveDOMObject 的主要用例。
通过使一个对象继承自 ActiveDOMObject 并像这样注解 IDL,WebKit 将自动生成 isReachableFromOpaqueRoot 函数,只要 ActiveDOMObject::hasPendingActivity 返回 true,即使垃圾回收器可能在此实例中未遇到任何特定的不透明根,该函数也会返回 true。
在 XMLHttpRequest 的例子中,只要对象仍然存在活跃的网络活动,hasPendingActivity 就会返回 true。一旦资源完全获取或失败,它就不再有挂起活动。这样,只要有活跃的网络活动,XMLHttpRequest 的 JS 封装器就会保持活跃。
活动 DOM 对象还有一个相关的用例,即当文档进入前进/后退缓存时,以及当整个页面因其他原因必须暂停时。
当这种情况发生时,与文档关联的每个活动 DOM 对象都会被暂停。每个活动 DOM 对象都可以利用这个机会准备暂停任何挂起活动;例如,XMLHttpRequest 将停止分派 progress 事件,媒体元素将停止播放。当文档从前进/后退缓存中出来或因其他原因恢复时,每个活动 DOM 对象都会被恢复。此时,每个对象都有机会再次恢复之前挂起的活动。
创建挂起活动¶
有几种方法可以在活动 DOM 对象上创建挂起活动。
当相关的 Web 标准要求将任务排入队列以执行某些工作时,应使用 ActiveDOMObject 的以下成员函数之一:
queueTaskKeepingObjectAlivequeueCancellableTaskKeepingObjectAlivequeueTaskToDispatchEventqueueCancellableTaskToDispatchEvent
这些函数将自动创建挂起活动,直到新入队的任务执行完毕。
或者,可以使用makePendingActivity为活动 DOM 对象创建挂起活动令牌。这将使活动 DOM 对象的挂起活动保持,直到所有令牌都失效。
最后,当存在一个复杂的条件,使得挂起活动存在时,活动 DOM 对象可以重写virtualHasPendingActivity成员函数,并在该条件成立时返回 true。请注意,只要将来有可能以任何方式分派事件或调用 JavaScript,virtualHasPendingActivity 就应该返回 true。换句话说,当对象在 C++ 中执行某些工作时,应该存在一个挂起活动,远远早于任何事件分派的计划。任何时候没有挂起活动时,对象的 JS 封装器都可能被垃圾回收器删除。