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
的以下成员函数之一:
queueTaskKeepingObjectAlive
queueCancellableTaskKeepingObjectAlive
queueTaskToDispatchEvent
queueCancellableTaskToDispatchEvent
这些函数将自动创建挂起活动,直到新入队的任务执行完毕。
或者,可以使用makePendingActivity
为活动 DOM 对象创建挂起活动令牌。这将使活动 DOM 对象的挂起活动保持,直到所有令牌都失效。
最后,当存在一个复杂的条件,使得挂起活动存在时,活动 DOM 对象可以重写virtualHasPendingActivity
成员函数,并在该条件成立时返回 true。请注意,只要将来有可能以任何方式分派事件或调用 JavaScript,virtualHasPendingActivity
就应该返回 true。换句话说,当对象在 C++ 中执行某些工作时,应该存在一个挂起活动,远远早于任何事件分派的计划。任何时候没有挂起活动时,对象的 JS 封装器都可能被垃圾回收器删除。