RefPtr and PassRefPtr Basics

来源:互联网 发布:java中list的用法 编辑:程序博客网 时间:2024/04/28 20:31

如转载,请注明出处!

原文地址:http://www.webkit.org/coding/RefPtr.html

History

        在WebKit内核中,很多对象都有引用计数。操作引用计数的方法有两个,分别是用于增加引用计数的方法ref以及减少引用计数的方法deref。调用ref的次数与调用deref的次数必须相等。如果在对象的引用计数等于1时调用deref,那么对象就会被自动删除。类RefCounted提供了ref方法和deref方法的默认实现。如果某类c继承了RefCounted类,那么类c的对象就有引用计数。
        2005年的时候,我们发现有很多内存泄漏,在实现HTML编辑功能的代码中内存泄漏更为严重。内存泄漏的原因是错误地使用了ref方法和deref方法。我们想要使用智能指针来减少内存泄漏量。我们做了一些试验,发现智能指针对引用计数的操作耗时较大,严重影响了性能。例如,一个函数使用智能指针作为参数,返回同一类型的智能指针作为返回值。每产生一个临时智能指针,就会对引用计数操作2次。创建智能引用计数时,引用计数会增1。之后目标智能指针销毁时,会再将引用计数减1。传递实参、返回运算结果至少创建1个临时智能指针,最多创建2个临时智能指针。因此,这个过程会操作引用计数2到4次。我们想要找到一种减少引用次数操作次数的方法。
        C++标准库中的类auto_ptr给我们带来了启发。auto_ptr拥有一个对象的所有权。如果auto_ptr对象做赋值运算,那么源auto_ptr对象将不再持有对象所有权,目标auto_ptr对象持有对象的所有权。
        Maciej Stachowiak设计了两个模板类RefPtr、PassRefPtr。这两个类应用了传递所有权的思想。WebCore模块使用这两个类来管理引用计数。

raw pointer

        在讨论智能指针类型RefPtr模板类时,我们使用词组raw pointer来表示c++内嵌的指针类型。raw pointer的设置方法具有固定样式的,如下代码所示:
// example, not preferred styleclass Document {    ...    Title* m_title;}Document::Document()    : m_title(0){}Document::~Document(){    if (m_title)        m_title->deref();}void Document::setTitle(Title* title){    if (title)        title->ref();    if (m_title)        m_title->deref();    m_title = title;}

RefPtr

        RefPtr是一个简单的智能指针。在获取对象指针后执行对象的ref方法,在失去对象指针前执行对象的deref方法。只要对象具有成员方法ref和deref,那么RefPtr就可以持有该对象的指针。下面是设置RefPtr智能指针的示例代码:
// example, not preferred style class Document {    ...    RefPtr<Title> m_title;}void Document::setTitle(Title* title){    m_title = title;}

        仅仅使用RefPtr会导致引用计数从固定值v先增加1、再减小1、再增加1、再减1,重新变为v。后文中,将这种现象称为引用计数波动。如下示例代码:
// example, not preferred style; should use RefCounted and adoptRef (see below) RefPtr<Node> createSpecialNode(){    RefPtr<Node> a = new Node;    a->setSpecial(true);    return a;}RefPtr<Node> b = createSpecialNode();

        在分析上段代码之前,先假设新建Node对象的引用计数为0。Node对象的引用计数变化为:
  • 新建Node对象的引用计数为0。
  • 将新建Node对象负值给a后,Node对象的引用计数变为1。
  • 创建返回值后,Node对象的引用计数变为2。
  • 销毁临时变量a后,Node对象的引用计数变为1。
  • 把返回值赋值给b后,Node对象的引用计数变为2。
  • 返回值对象销毁后,Node对象的引用计数变为1。
        需要额外说明的是,如果编译器实现了return value optimization,那么可以减少返回值对象的创建,也就将引用计数加1、减1的次数各减去1次。
        如果方法参数类型是RefPtr,方法返回值类型也是RefPtr,那么引用计数波动带来的额外耗时更大。PassRefPtr可以减小引用计数波动。

PassRefPtr

        PassRefPtr与RefPtr类似,但不完全相同。将PassRefPtr赋值给另一个PassRefPtr,或者将PassRefPtr赋值给RefPtr,并不会改变对象的引用计数。还是假设新建对象的引用计数为0。如下示例代码:
// example, not preferred style; should use RefCounted and adoptRef (see below)PassRefPtr<Node> createSpecialNode(){    PassRefPtr<Node> a = new Node;    a->setSpecial(true);    return a;}RefPtr<Node> b = createSpecialNode();

        代码运行过程中,Node引用计数的变换过程如下:
  • 新建Node的引用计数为0。
  • 把Node对象赋值给a后,引用计数变为1。
  • 返回值对象创建后,a不再持有Node对象的指针,引用计数仍然为1。
  • 返回值对象赋值给b后,返回值不再持有Node对象的指针,引用计数仍然为1。
        PassRefPtr赋值操作过程中,源PassRefPtr持有的指针会被设置为0。这样就可以减少对象引用计数加1、再减1的波动了。Safari团队发现这样的行为很容易导致新问题产生。如下代码:
// warning, will dereference a null pointer and will not work static RefPtr<Ring> g_oneRingToRuleThemAll;void finish(PassRefPtr<Ring> ring){    g_oneRingToRuleThemAll = ring;    ...    ring->wear();}

        在执行ring->wear()方法时,ring已经不持有任何对象了,ring相当于0。为了避免出现这样的问题,我们强烈建议只将PassRefPtr用于参数和返回值。在使用智能指针之前,必须将PassRefPtr赋值给RefPtr类型局部变量。如下示例代码:
static RefPtr<Ring> g_oneRingToRuleThemAll;void finish(PassRefPtr<Ring> prpRing){    RefPtr<Ring> ring = prpRing;    g_oneRingToRuleThemAll = ring;    ...    ring->wear();}

Mixing RefPtr and PassRefPtr

        PassRefPtr的复制、赋值都会把对象所有权传递出去。PassRefPtr作为参数和返回值,可以减少引用计数波动。我们建议在函数体内尽量使用RefPtr来操作对象。函数体内也会遇到直接传递对象所有权的需求。RefPtr提供了一个成员方法release用于传递对象所有权。release方法会先将自己持有的raw pointer备份到一个局部变量ptr,然后将自己的raw pointer设置为0,最后创建一个PassRefPtr拥有ptr的所有权。这个过程不会改变ptr对象的引用计数。如下示例代码:
// example, not preferred style; should use RefCounted and adoptRef (see below) PassRefPtr<Node> createSpecialNode(){    RefPtr<Node> a = new Node;    a->setCreated(true);    return a.release();}RefPtr<Node> b = createSpecialNode();

        不使用RefPtr的release方法,需要编译器自己将return 语句转换为return PassRefPtr(RefPtr)类型的代码。这个过程依赖于编译器的实现。如果编译器实现的语义功能有缺陷,那么就无法实现减小引用计数波动的目标。使用RefPtr的release方法可以在减少引用计数波动的同时,降低编译器语义错误发生的概率。

Mixing with raw pointers

        如果某个方法需要raw pointer作为参数,那么可以使用RefPtr成员方法get获得raw pointer。如下示例代码:
printNode(stderr, a.get());

        RefPtr(PassRefPtr)类重载了许多操作符。有些情况下,可以直接使用RefPtr(PassRefPtr)。如下示例代码:
RefPtr<Node> a = createSpecialNode();Node* b = getOrdinaryNode();// the * operator*a = value;// the -> operatora->clear();// null check in an if statementif (a)    log("not empty");// the ! operatorif (!a)    log("empty");// the == and != operators, mixing with raw pointersif (a == b)    log("equal");if (a != b)    log("not equal");// some type castsRefPtr<DerivedNode> d = static_pointer_cast<DerivedNode>(a);

        Reftr和PassRef会自动调用对象的ref方法以及deref方法,以保证ref方法调用次数和deref调用次数肯定相等。这样可以减小程序员的工作复杂度,降低内存泄漏发生的概率。有一个问题必须特殊处理,即如果对象raw pointer的引用计数大于0,那么如何将这个对象的所有权传递给RefPtr和PassRefPtr呢?赋值语句以及拷贝构造函数会增加这个对象的引用计数,这不符合“传递所有权不改变引用计数”这条规则。WebKit内核提供了一个全局函数adpotRef来解决这个问题。如下示例代码:
// warning, requires a pointer that already has a refRefPtr<Node> node = adoptRef(rawNodePointer);

        另外,WebKit内核也支持反过程。即,不改变引用计数的同时将对象指针赋值给raw pointer。方法是执行PassRefPtr的成员方法leakRef。如下示例代码:
// warning, results in a pointer that must get an explicit derefRefPtr<Node> node = createSpecialNode();Node* rawNodePointer = node.release().leakRef();

        因为leakRef的应用场景比较少,所以只有PassRefPtr提供了这个方法。可以先通过release方法获得PassRefPtr,在执行leakRef方法。

RefPtr and new objects

        在前述小节的讨论中,一直假设新建对象的引用计数是0。但实际上,新建RefCounted对象的引用计数是1。在执行deref时,发现对象引用计数已经是1了,那么就会直接删除对象。也就是说,对象的引用计数永远不会为0。这样做的好处很明显,减少引用计数额外的操作可以提高速度。为了防止内存泄漏,最好使用RefPtr智能指针来管理对象。这样可以防止出现“忘记执行deref”的情况。这也意味着一旦执行完new操作,必须立刻执行adoptRef方法。在WebCore模块,我们会执行类方法create来创建对象,而不是直接调用new运算符。如下示例代码:
// preferred style PassRefPtr<Node> Node::create(){    return adoptRef(new Node);}RefPtr<Node> e = Node::create();

        adoptRef和PassRefPtr都不会改变对象的引用计数。因此,采用示例代码中create的实现方式,可以保证只在创建对象时对引用计数做了赋值操作,其余情况都不会改变引用计数的值。提高了代码的运行效率。
        RefCounted加了一个运行时检查的逻辑。如果在执行ref和deref方法之前,没有执行adoptRef,那么一个assert断言就会失败。

GuideLines

我们总结了一些RefPtr和PassRefPtr的使用原则。
1. 局部变量
  • 如果当前作用域既不拥有对象所有权也不需要管理对象生命周期,那么可以直接使用raw pointer。
  • 如果当前作用域既拥有对象所有权也需要管理对象生命周期,那么需要使用RefPtr。
  • 永远不要使用PassRefPtr作为局部变量。
2. 成员变量
  • 如果当前类既不拥有对象所有权也不需要管理对象生命周期,那么可以直接使用raw pointer。
  • 如果当前类既拥有对象所有权也需要管理对象生命周期,那么需要使用RefPtr。
  • 永远不要使用PassRefPtr作为成员变量。
3. 函数参数
  • 如果函数既不拥有对象所有权,那么可以使用raw pointer。
  • 如果函数拥有对象所有权,应该使用PassRefPtr。应该在函数的起始位置就将PassRefPtr赋值给RefPtr类型的局部变量,局部变量最好具有前缀”prp”。这种情况常见于setter方法中。
4. 函数返回值
  • 如果函数返回值是一个已存在的对象,且不需要传递对象所有权,那么使用raw pointer。这种情况常见于getter方法中。
  • 如果函数返回值是一个新建对象,或者需要传递对象所有权,那么应该使用PassRefPtr。因为局部变量是RefPtr类型,所以建议将RefPtr的release方法返回值放在return语句中。
5. 新建对象
  • 今早将新建对象所有权传递给智能指针RefPtr。
  • 如果新建对象是RefCounted类型,那么使用adoptRef来传递对象所有权。
  • 建议将对象的构造方法设置为私有的。另外,提供类方法create。create方法的返回值是PassRefPtr。