如何使 C++ 类进行循环回收

我的类是否应该进行循环回收?

首先,您需要确定您的类是否应该进行循环回收。主要有三个标准

  • 它可以是强引用循环的一部分,包括引用计数对象和 JS。通常,当它可以保持存活并被循环回收的对象或 JS 保持存活时,就会发生这种情况。

  • 它必须是引用计数的。

  • 它必须是单线程的。循环回收器只能处理在单个线程上使用的对象。主线程、DOM 工作线程和工作线程分别有自己的循环回收器。

如果您的类满足第一个标准但不满足第二个标准,那么无论哪个类唯一拥有它都应该进行循环回收,假设它是引用计数的,并且此类应作为其一部分进行遍历和解除链接。

循环回收器支持 nsISupports 和非 nsISupports(在 CC 命名法中称为“原生”)引用计数。但是,如果两个通过继承相关的类需要不同的 CC 实现,我们不支持在存在继承的情况下进行原生循环回收。(这是因为我们使用 QueryInterface 来查找对象的正确 CC 实现。)

一旦您决定使一个类进行循环回收,您需要在其实现中添加一些内容

  • 循环回收引用计数。需要特殊的引用计数,以便 CC 可以识别对象何时创建、使用或销毁,以便它可以确定对象是否可能是垃圾循环的一部分。

  • 遍历。一旦 CC 确定一个对象**可能**是垃圾,它需要知道它持有对哪些其他循环回收对象的强引用。这通过“遍历”方法完成。

  • 解除链接。一旦 CC 确定一个对象**是**垃圾,它需要通过清除对其他循环回收对象的所有强引用来打破循环。这通过“解除链接”方法完成。这通常看起来与遍历方法非常相似。

遍历和解除链接方法以及循环回收所需的其它方法,出于性能原因,是在一个特殊的内部类对象(称为“参与者”)上定义的。参与者的存在主要隐藏在宏后面,因此您无需担心它。

接下来,我们将介绍这些部分的声明和定义是什么样的。(剧透:有很多ALL_CAPS宏。)这将主要涵盖最常见的变体。如果您需要略有不同的内容,您应该查看此处提到的宏的声明位置,并查看变体是否已存在。

引用计数

nsISupports

如果您的类继承自 nsISupports,您需要在类声明中添加NS_DECL_CYCLE_COLLECTING_ISUPPORTS。这将声明您需要实现 nsISupports 的 QueryInterface (QI)、AddRef 和 Release 方法,以及实际的引用计数字段。

在您的类的.cpp文件中,您必须定义 QueryInterface、AddRef 和 Release 方法。定义 QI 方法的来龙去脉超出了本文档的范围,但您需要使用宏的特殊循环回收变体,例如NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION。(这是因为我们使用 nsISupports 系统来定义一个特殊接口,用于动态查找对象的正确 CC 参与者。)

最后,您必须实际定义类的 AddRef 和 Release 方法。如果您的类称为MyClass,那么您可以使用声明NS_IMPL_CYCLE_COLLECTING_ADDREF(MyClass)NS_IMPL_CYCLE_COLLECTING_RELEASE(MyClass)来完成此操作。

非 nsISupports

如果您的类**不**继承自 nsISupports,您需要在类声明中添加NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING。这将为 AddRef 和 Release 方法以及实际的引用计数字段提供内联定义。

循环回收参与者

接下来,我们需要声明和定义循环回收参与者。这主要是隐藏在宏后面的样板代码,但您需要指定哪些字段需要遍历和解除链接,因为它们是对循环回收对象的强引用。

声明

首先,我们需要为参与者添加一个声明。和以前一样,假设您的类是MyClass

对于 nsISupports 类,声明它的基本方法是NS_DECL_CYCLE_COLLECTION_CLASS(MyClass)

如果您的类继承自多个继承自 nsISupports 类的类,例如Parent1Parent2,那么您需要使用NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(MyClass, Parent1)来告诉 CC 通过Parent1转换为 nsISupports。您可能希望选择它继承的第一个类。(循环回收器需要能够将MyClass*转换为nsISupports*。)

您可能会遇到的另一种情况是,您的 nsISupports 类继承自另一个循环回收类CycleCollectedParent。在这种情况下,您的参与者声明将类似于NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MyClass, CycleCollectedParent)。(这是为了让 CC 能够从 nsISupports 转换为MyClass。)请注意,我们不支持非 nsISupports 类的继承。

如果您的类是非 nsISupports,那么您需要使用NATIVE系列的宏,例如NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(MyClass)

除了这些修饰符之外,这些不同的变体还有进一步的SCRIPT_HOLDER变体,如果您的类保持 JavaScript 对象的存活,则需要这些变体。这是因为由此类保持存活的 JS 对象的跟踪必须与由此类保持存活的 C++ 对象的跟踪分开声明,以便垃圾回收器也可以使用跟踪。例如,NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(MyClass, Parent1)

这些宏还有WRAPPERCACHE变体,如果您的类是包装器缓存的,则需要使用这些变体。这些实际上是SCRIPT_HOLDER的一种特殊形式,因为缓存的包装器是由 C++ 对象保持存活的 JS 对象。例如,NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(MyClass, Parent1)

这些宏还有另一种变体,SKIPPABLE。本文档不会详细介绍它的工作原理,但基本思想是,一个类可以告诉 CC 它何时绝对处于存活状态,这可以让 CC 跳过它。对于活动文档中的 DOM 元素等内容,这是一个非常重要的优化,但您正在创建的新的循环回收类可能不够常见,因此无需担心。

实现

最后,您必须编写 CC 参与者的实际实现,在您的类的 .cpp 文件中。这将定义遍历和解除链接方法,以及一些其他随机的辅助函数。在最简单的情况下,这可以通过单个宏来完成,如下所示:NS_IMPL_CYCLE_COLLECTION(MyClass, mField1, mField2, mField3),其中mField1等是类的字段的名称,这些字段是对循环回收对象的强引用。有一些模板魔法说明了应该遍历和解除链接多少种常见的类型,例如 RefPtr、nsCOMPtr,甚至是一些数组。还有一个变体NS_IMPL_CYCLE_COLLECTION_INHERITED,当存在一个也是循环回收的父类时,您应该使用它,以确保父类的字段被遍历和解除链接。该父类的名称作为第二个参数传入。如果这两个方法中的任何一个有效,那么您就完成了。您的类现在是循环回收的。请注意,这对于 JS 对象字段无效。

但是,如果这不起作用,您需要更深入地了解细节。一个好的起点是复制NS_IMPL_CYCLE_COLLECTION的定义。

对于脚本持有者方法,您还需要除了遍历和解除链接之外,使用NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN和其他类似宏定义一个跟踪方法。您需要包含类保持存活的所有 JS 字段。跟踪方法将由 GC 和 CC 使用,因此,如果您遗漏了什么,最终可能会导致使用后释放崩溃。您还需要在类的构造函数中调用mozilla::HoldJSObjects(this);,并在析构函数中调用mozilla::DropJSObjects(this);。这将使用 JS 运行时注册(和注销)您的对象的每个实例,以确保它得到正确跟踪。如果您有一个没有其他 JS 字段的包装器缓存类,则此操作不适用,因为 nsWrapperCache 会为您处理所有这些操作。