RefPtr and PassRefPtr Basics

如转载,请注明出处!

原文地址: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 style
class 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 -> operator
a->clear();

// null check in an if statement
if (a)
    log("not empty");

// the ! operator
if (!a)
    log("empty");

// the == and != operators, mixing with raw pointers
if (a == b)
    log("equal");
if (a != b)
    log("not equal");

// some type casts
RefPtr<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 ref
RefPtr<Node> node = adoptRef(rawNodePointer);

        另外,WebKit内核也支持反过程。即,不改变引用计数的同时将对象指针赋值给raw pointer。方法是执行PassRefPtr的成员方法leakRef。如下示例代码:
// warning, results in a pointer that must get an explicit deref
RefPtr<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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值