Const Interface Bug引起的内存泄漏(重发)

来源:互联网 发布:chrome下载 知乎 编辑:程序博客网 时间:2024/05/22 18:55

不好意思,重发一遍,原来的那篇好像不太对劲,不能修改。

明天就是感恩节,今天上班发现大家都想打发时间混日子,不好好干活,终于同事Alex的一封email让大家理直气壮的不务正业起来,^_^

 

他的email提出了这么两个函数,简便起见,我把他们称为HostClientClient是一个在Host中被调用的函数,另外,里面还有一个InterfacedObject类型的变量TManager,TManager=class(InterfacedObject, IManager).

 

Procedure Host;
Begin
       Client(TManager.Create);
End;

Procedure Client(const aMgr : IManager);
Begin
       If Assigned(aMgr) then beep;
End;

非常简单的两个方法,但是,里面有一个Bug!! 上述代码中有内存泄漏,如果看到这里哪位大侠已经知道是什么泄露了,那么请允许我景仰一下。另外大家如果不相信,请自行测试,特别可以将Host运行个一百万次,看看程序的内存使用。

 

这里的内存泄漏是因为TManager.Create所生成的对象一直到Host执行完毕都没有被释放。我做了一下实验以及Google,发现了问题所在。Alex提出的问题源于一个Delphi中接口机制的”Bug”,称为Const Interface Bug,这个Bug所说的是,如果一个接口被作为常量传递到Client方法,那么在Client方法里是不会自动生成对这个常量的_AddRef以及_Release代码的(关于Delphi中接口的引用计数机制我就不在这里罗嗦了,网上大把资料可以参考),而我读到了一篇关于这个Bug的帖子,大家可以参考一下,不过,我在Bug上加引号的原因是,典型的ConstInterface Bug直接后果是AVAccess Violation),这种结果很容易被发现,所以引发的后果并不严重。而我在这里想说的是,ConstInterface Bug可以引起很隐蔽的内存泄漏问题。

 

让我们言规正传(请大家在继续之前再重新看一边上面对Const Interface Bug的描述,呵呵), 让我给出内存泄漏的原因:

 

Procedure Host;
Begin
       Client(TManager.Create);
{
这里,毫无疑问我们为一个新对象分配了内存,但是注意,因为这里没有将这个接口赋值给任何local变量,因此Host方法不会对这个对象的计数进行增减,这个工作应该交给Client来完成。}
End; //
也就是说,Host在结束的时候,不会free这个对象。

Procedure Client(const aMgr : IManager);
Begin
       If Assigned(aMgr) then beep;
{
而在Client方法中,由于aMgr是作为常量被传递,所以Delphi不会自动生成_AddRef以及_Release}
End; //
Client结束的时候,也不会free这个aMgr

我给出另外三种情况,加上上面这一种,一共四种,大家可以比较一下,上面的case称为1

 

2.我推荐的标准写法

Procedure Host;
Var
       aManager : IManager;
Begin
       aManager := TManager.Create;
       Client(aManager);
{
这里,由于local变量aManagerDelphi将会在Host里生成_AddRef以及_Release,这就能保证在Client执行完毕返回以后,aManager的引用数为0,进而aManager所引用的接口被释放}
End;
Procedure Client(const aMgr : IManager);
Begin
       If Assigned(aMgr) then beep;
End;

3.我们可以看看如果没有const关键字会怎么样。

Procedure Host;
Begin
    Client(TManager.Create);
{
这里和1一样,Host不会对新生成的对象进行任何管理,其计数的引用由Client负责}

End;

Procedure Client(aMgr : IManager);
Begin
       If Assigned(aMgr) then beep;
{
这里的区别在于,aMgr作为一个一般参数,其实相当于一个local变量,因此Client会自动添加_AddRef以及_Release,因此,在Client结束的时候,aMgr计数为0,对象被自动释放。}
End;

4.和2类似的,我们可以把local引用加在Client里,像这样:

Procedure Host;
Begin
       Client(TManager.Create);
End;

Procedure Client(const aMgr : IManager);
Var
       aManager : IManager;
Begin
       aManager := aMgr;
       If Assigned(aManager) then beep;
End;

类似的,由于aManager的出现,使得aMgr被释放,但是这种Client方法的写法非常不符合常规,所以出现的可能性不大。

 

通过比较上面四个例子,大家可以发现问题的关键在于Const,在于ConstInterface Bug

 

下面我给出一个更怪异的例子,这个例子也会引起内存泄漏但是和Const Interface Bug无关,99%大家根本不会遇到这钟情况:

5.极端例子:

Procedure Host;
Begin
       Client(TManager.Create);
End;

Procedure Client(aMgr : IManager);
Begin
End;

注意,这里的Client是一个空函数,而且没有Const关键字(当然,如果有Const的话,参照第1个例子,自然会发生内存泄漏),这时,几乎所有的人都会发现有内存泄漏,你一定会问,为什么这里的aMgr就不是local变量,这里的aMgr就不会被Client管理计数而自动释放呢?答案是,这里的aMgrlocal变量,Delphi也为aMgr生成了_AddRef_Release,问题并不在aMgr。注意到我前面说的“几乎所有人”了吗?这是因为Delphi中,编译器的优化选项是默认打开的,在优化状态下,这个Client的函数体将会被删除,因此里面也不会生成什么_AddRef或者_Release,而我们的aMgr则再次成了孤魂野鬼。不信?关掉你的优化选项,重新编译所有单元再看看结果。

原创粉丝点击