怎么样正确引用计数?
怎么样正确引用计数?
浅谈引用计数
前言
作为Delphi程序员,您可以不用看这节内容,但是如果您想更多的了解一些COM内部技术,或是在对象模型与引用模型之间可以进行很好的控制的话,笔者更希望你可以抽出些许时间来看这一切的内容,而益处提体的将很明显,您可以自由的用一些技巧来解决让您头疼的问题。好了,继续我们今天的交流;
在组件技术必备知识二中,我们对接口(Interface)进行了一些介绍,当我们并没有深入的对接口的实现/效率/优化等问题进行进一步的禅述,而了解它们的确对于我们以后的编程是有很大的帮助的,我们都知道,每个接口都会维护一个全局变量FRefCount (这是Object Pascal里的变量名称,如果是在C++里,它维护的是m_CRef),它专门用来控制接口的生命周期,或是组件的生命周期(组件/接口同样具有生命周期),当然,我们也可以给接口强制给值Nil同样可以释放接口,但那是不安全的或是不应该被推荐的。在此处之所以将引用技术做为一个课题例出来就是希望各位可以对组件的优化、效率方面认识一些。而FRefCount是在_AddRef and _Release中得以实现的,如下代码(本节所有代码摘自Delphi6中,只要您的参考版本是Delphi4以上,代码都是相同的)。
function TInterfacedObject._AddRef: Integer;
begin
Result := InterlockedIncrement(FRefCount);
end;
function TInterfacedObject._Release: Integer;
begin
Result := InterlockedDecrement(FRefCount);
if Result = 0 then
Destroy;
end;
从代码中我们可以看出,接口的生命周期是在_AddRef and _Release两个方法中控制的,事实上,这两个方面在组件编程中,就是专门用来控制组件的生命周期(关于组件生命周期和接口生命周期我们将会近一步的进行说明。),之外它们可以说是没有意义的,而引用计数变量(FRefCount)如果在不考虑组件的生命周期时,也是完全没有意义的。
AddRef and Release是实现的一种名为引用计数的内存管理技术,引用计数技术是使组件自己删除组件的最简单的同时也是效率最高的方法。COM组件将维护一个引用计数的变量来对自己生命周期进行管理,当客户从组件获得一个接口时,这个引用计数变量会进行增1操作(_AddRef),当客户释放了对接口的调用时,组件会自动的进行引用计数的减1操作(_Release),在基于Delphi的编程中,我们可以不去考虑什么时候进行调用这两个方法,但是如果您一旦脱离了Delphi的话,您可能必须要考虑什么时候调用这两个方法,如在C++中,您就一定要自己调用这两个方法,这也正是笔者为什么会写这一节的内容.简单的来说,引用计数我们平时不需要去考虑,但是在对象引用和接口引用中,您就需要自己去调用这两个方法,同时它还涉及到作为一个组件是去整个的释放还是单个的释放上以及最小单位的释放上是有必要去考虑引用计数的。如:对于一个COM组件而言,它封装了一些COM对象,但是用户通过接口可能需要调用COM组件中的几个COM对象提供的服务,那么问题就产生了,用户有可能在访问完了一个COM对象再去访问另一个COM对象或是进行互动的方式进行访问,很不幸运的是这个组件又是一个占用内存资用很大的组件,特别提体到用户所访问的两个或是更多的COM对象的同时,您如何对组件进行有效的管理呢?是用户访问完了一个COM对象之后就立马释放这个COM对象呢?还是当用户对组件访问完成之后再进行组件级的释放呢?或是您更详细的对每一个用户已经不用的接口进行释放呢?这都对组件的效率有些许影响。而此时我们选择不同的方式就有可能需要自己增加引用计数变量进行控制了,如:
var
oFRefCount : Integer;//对象一级的引用计数的应用
begin
…….
end;
var
cFRefCount : Integer;//组件一级的引用计数的应用。
begin
……
end;
或是直接引用FRefCount//接口一级的应用计数的应用。
这都是我们一定要考虑的。而在对象模型和引用模型中,特别它们的混合应用中,如果您还让Delphi为我们进行自动的优化(引用计数的调用)的话,那么将是一场噩梦、灾难!OK,我们先对这些有可能出现的问题进行讨论,或是归纳为引用计数的优化。
首先我们应该明白,常规下什么时候应该调用这两个方法,归纳如下:
² 在返回之前需要调用_AddRef使得FRefCount进行+1操作。对于那些返回指针接口的函数,在返回之前应该对相应的指针进行_AddRef操作。这些函数包括:QueryInterface、CreateInstance。
² 使用完接口这后应该对相应的接口进行_Release操作,在Reslease中进行接口的释放操作。
² 在赋值之后调用_AddRef方法。在接一个接口指针赋值给另一个接口指针之前应该调用其相应指针的_AddRef。换句话说,在调用一个接口的另外一个引用时,应增加相应组件的或是接口指针的引用计数。对象模型和引用模型在此提体的很明显。
在上一篇文章中,我在图示中有意的将COM对象的两种方式:集合和包容提体了出来,而这做为COM组件的特殊例子,在引用计数我们也可以不去考虑。记得在之前曾提起过,可以给接口、对象进行强制的释放,只要简单的给它们至NIL就可以进行释放。引用计数作为管理组件的生命周期的执长官,在很多地方需要进行权衡。如下图所示:
对于上图我们来进行分析引用计数技术。
作为一个COM组件,它可以包含有多个COM对象,而且每个COM对象所封装的逻辑规则也有大小的区别,比如COM对象1和COM对象2都封装了很多的逻辑规则,而COM对象3相对而言只是封装了一些很少的逻辑规则,如果用户A现在可能同时需要COM对象1和COM对象3所提供的服务,并且它里要先访问COM对象1这后才对进行COM对象3访问,再如果这个COM组件仅仅包含了这两个COM对象的话,当用户已经完成了COM对象1的访问,之后对COM对象3进行访问,那么我们是否还有让COM对象1在内存中的占用?而之前我们也已经说了在COM对象1所封装的逻辑规则比较多,它占用的内存量比较大,COM对象3还让存在吗?当然,它已经没有存在的必要了,但是,我们此时如何知道它的服务已经不再为用户A提供了?作为一个组件,它自身为我们维护一个组件级的引用计数,当这个组件的服务提供完成之后,它会自动的释放,但是COM对象并不会进行自动的释放,这时我们就可以在服务应用程序里边进行释放,如前,我们可以通过定义一个oFRefCount来进行观察。可以完成COM对象的释放,这样就可以让更多的内存得以重新利用,使得组件效率得以提高,但是有些情况并不一样,如果相反,我们是先调用的COM对象3,而之后才调用的是COM对象3。那么此时也没有释放的必要。所以这些都是我们在组件的编程中应该想到的和应该解决的。而且,在实际的编程中所遇到的情况远远的复杂于我们所举的例子,如果有多个对象并存的话情况将更复杂,但只要我们把握住一些问题是我们可以通过引用计数来解决的,而并非是简单的让系统为我们来完成这些引用计数,学会自己来进行观察、判断很重要,才能举一反三,才能正真的提高组件的效率!同时,作为一个组件,并非会将每个对象的接口都直接的给用户的,它也可以通一个共用的接口来进行其它的接口的引用。实例我将会给出。
再来谈一谈对象模型和引用模型中,引用计数的应用给程序带来的不同。
对象模型和引用模型,我们都比较熟悉,如:
var
pT : TCoClass;//对象模型。
……
var
pI : ICoClass;//引用模型。
……
事实上,接口模型已经在很大的程度上替代了引用模型,如上两段代码,我们都可以在程序中去进行COM对象的调用,那么此时就会用到AS操作符,将一个对象指针赋值给或是被赋值于一个接口指针,此时,Delphi中当应用了AS的时候,它会自动的调用_AddRef函数。接口使用完之后,它会调用_Release函数,用户的对象将被销毁。如下:
Procedure DoSomethingwithInterface(Intf : IFormattedNumber);
Begin
ShowMessage(Intf.FormattedString);
End;
Procedure CreateAndUserObject;
Begin
MyInterger := TformattedInteger.Create(12);
DoSomethingWithInterface(MyInteger as IFormattedNumber);
MyInteger.SetValue(10);
End;
在这个例子中,MyInteger是一个TformattedInteger类的对象。它是使用对象模型(也就是说,TformattedInteger.Create被赋值给了一个对象变量)所创建的。而在调用DoSomethingWithIntegerface时进行转换,用到了As操作符(这就是一个很明显的对象模型、引用或是接口模型的应用),而之前说了,在调用AS时,Delphi会进行_AddRef,而调用完完毕之后,它要进行_Release操作。此时,进行了As转换完毕之后,事实上IformattedNumber的引用计数FrefCount已经为0,那么接口被释放,所以再调用MyIntegerSetValue(10)将会出错,解决的方法是只要我们改变了函数声明,如下:
Procedure DoSomethingwithInterface(var Intf : IFormattedNumber); 或者
Procedure DoSomethingwithInterface(Const Intf : IFormattedNumber);
就可以解决这个问题,理由很简单,Const or var 类型合Delphi通过引用地址传递接口,而不是通过值传递。通过值传递接口将引起Delphi调用_AddRef和_ReLease。改正之后,可以作如下
Procedure DoSomethingwithInterface(var Intf : IFormattedNumber);
Begin
ShowMessage(Intf.FormattedString);
End;
Procedure CreateAndUserObject;
Begin
MyInterger := TformattedInteger.Create(12);
DoSomethingWithInterface(MyInteger);
MyInteger.SetValue(10);
End;
以上代码来自网上,这仅仅是解决又汇合模型的一种方法,其实,在某些时候,我们来进行手动的调用_AddRef or _Release是很有必要的。
再此还要说的一点时,组件的生命周期的确是根据FrefCount来进行动态的作用的,但是,并非是他一人进行掌管,下一篇您将会看到类厂,会发现它也同样的管理着组件的生命周期。