记录技术旅程
分类: LINUX
2011-06-29 16:02:27
WebKit中的许多对象是引用计数的(reference counted),采用的模式就是类具有ref和deref成员函数增加和减少引用计数。每个ref调用必须有一个deref与之匹配。当在引用计数值为1的对象上调用deref方法时,对象删除。WebKit中的许多类通过继承RefCounted类模板应用该模式。
时间回溯到2005年,我们发现存在许多由于不正确调用ref和deref而引起的内存泄露,特别是HTML编辑的代码。
我们希望使用智能指针来减少这一问题。但是早期的试验表明智能指针会进行额外的引用计数处理而影响性能。例如,一个函数有一个智能指针的参数并返回该指针指针作为返回值,仅仅传入该参数并返回该值会进行2到4次的增加和减少引用计数值,因为对象从一个智能指针转移到另一个上。因此我们寻找一种方式来让我们使用智能指针同时避免引用计数跳变(churn).
我们从C++标准类模板auto_ptr获得灵感,这些对象实现了一种模型,赋值即是归属关系转移(transfer of ownership),当您从一个auto_ptr赋值到另一个,贡献值变成0.(When you assign from one auto_ptr to another, the donor becomes 0.)
Maciej Stachowiak设计了一对类模板,RefPtr和PassRefPtr,实现这一模式来解决WebKit中恼人的引用计数问题。
原始指针(Raw pointers)当我们讨论诸如RefPtr类模板之类的智能指针时,通常使用原始指针(raw pointer)指代C++语言中内置的指针类型。下面是经典的使用原始指针(raw pointer)的设置函数:
RefPtr是一个简单的智能指针类,它对来值(incoming value)调用ref,对去值(outgoing value)调用deref。RefPtr可用于任何有ref和deref成员函数的对象。下面是使用RefPtr写的设置函数实例:
出于讨论考虑,我们假设节点对象的引用计数起始值为0(稍后更多),当它赋值给a后,引用计数值增加到1。创建返回值后引用计数值又增加到2,接下来a销毁,引用计数值又减少到1。然后创建b后引用计数值增加到2,再下来createSpecialNode返回值销毁后,引用计数值减到1。
(如果编译器实现了返回值优化(return value optimization),就会少一次引用计数增加和减少。)
引用计数跳变(churn)在函数参数和返回值都涉及的情况下更严重,解决的方法就是PassRefPtr。
PassRefPtrPassRefPtr和RefPtr相似,但存在一处不同,当您拷贝一个PassRefPtr或者赋PassRefPtr值给RefPtr或另一个PassRefPtr时,原来的指针值设置为0,操作不改变引用计数。让我们来看一个新的版本的例子:
由于PassRefPtr类型指针赋值给另一个指针后本指针变成为0,所以使用PassRefPtr编程很容易容易导致错误。如下下列代码所示:
当wear方法被调用时,ring指针已经变为0了。为了避免这种情况发生,建议只是用PassRefPtr类型作为函数参数类型和返回值类型。并且,将参数类型为PassRefPtr的参数赋值给本地的RefPtr类型指针进行操作。如下:
Mixing RefPtr and PassRefPtr
除了函数传递参数和返回值时使用PassRefPtr类型外,建议使用RefPtr作为计数指针类型。但是,经常会需要RefPtr类型指针象PassRefPtr型指针一样传递指针的所有权。RefPtr类型有一个release成员函数,它通过设置原始的RefPtr指针为0,同时构建一个PassRefPtr类型指针,从而达到不改变指针引用计数的目的。示例代码如下:
上述代码与前面将a定义成PassRefPtr类型的示例代码具有相同的效果。这种用法,不仅保持了PassRefPtr类型指针的高效的特性,同时又避免了PassRefPtr容易出错的问题。
Mixing with raw pointers
当要获得RefPtr类型中的原始指针时,需要通过get函数来获得原始指针。
下面示例打印原始指针的值:
示例如下:
通常,RefPtr和PassRefPtr强制实施一个简单的规则,就是成对调用ref和deref,保证编程者不会漏掉对deref的调用。说的ref和deref就不能不说一下RefCounted类,这个类实现了指针的引用计数功能,提供了ref和deref方法。而RefPtr和PassRefPtr只是完成了对ref和deref的自动调用功能,因此这里所说的原始指针(raw pointer)通常是指继承自RefCounted类的指针,换句话说,是具有ref和deref功能的指针。这样的原始指针可以通过adoptRef函数转换成RefPtr类型。
从RefPtr类型转换成一个原始指针并且不改变引用计数,可以使用PassRefPtr提供的leakRef函数。
由于leakRef很少被使用,因此只有PassRefPtr类具有这个方法。RefPtr类型的变量使用此方法使,需要先调用release方法,然后才能调用leakRef方法。
RefPtr and new object
在以上讨论的例子中,对象的假设引用计数是从0开始的。这样做的目的为了讲解简单明了,但是RefCounted类的引用计数并不是从0开始的。在构建ReCounted类的构造函数中,引用计数被赋值为1。当deref被调用时,如果引用计数等于1,则删除这个对象。由于ref和deref是成对出现的,为了不遗失调用deref,推荐的用法是在创建对象时应该立即调用adoptRef。在WebCore中,使用create方法代替new方法去创建对象。示例如下:
因为adoptRef和PassRefPtr被应用,上述用法是非常高效的。以引用计数为1创建对象,而整个过程没有再操作引用计数。
对象node通过create函数创建,赋值给a并release,最后赋值给b,整个过程没有触及引用计数。
RefCounted类会进行运行时刻检查,如果创建一个对象,调用ref和deref方法而没有最先调用adoptRef函数,将会声明一个错误。
Guidelines
如果指针的所有权和生命周期可以被保证,一个本地变量可以是一个原始指针。
如果代码需要获得指针的所有权或者确保指针的生命周期,本地变量应该是RefPtr类型。
本地变量永远都不能是PassRefPtr类型。
Data members
如果所有权和生命周期可以被保证,数据成员可以是一个原始指针。
如果类需要获得数据成员指针所有权并保证其生命周期,则数据成员需要定义为RefPtr类型。
数据成员永远都不能是PassRefPtr类型。
Function arguments
如果一个函数不需要获得参数对象的所有权,参数应该使用原始指针。
如果函数需要获得一个对象的所有权,参数应个是一个PassRefPtr类型。这包括大多数的setter函数。除非参数非常简单,在函数的开始PassRefPtr参数应用赋值给一个本地的RefPtr变量。
Function Results
如果一个函数的返回值是一个对象,但是所有权没有被传递,这个返回结果应用是一个原始指针。
如果函数的返回值是一个被创建的对象,或者对象的所有权被传递,返回值应该是PassRefPtr类型。由于本地类型通常是RefPtr,因此在返回时需要调用release方法将RefPtr类型转换成PassRefPtr类型。
New Object
新创建的指针对象应该尽快放在RefPtr类型中,这样可以使智能指针自动完成所有的引用计数。
对于RefCounted类型指针,上述的操作应该由adoptRef函数完成。
最好的用法是使用私有的构造函数和公有的create函数,通过返回一个PassRefPtr指针从而实现对象的创建。