Delphi中由TComponent.Owner引发的资源重复释放问题

案例情形:在通过控件的构造函数Create(AOwner: TComponent)创建对象a时传入Application,之后又自作多情的主动调用FreeAndNil释放此对象a,在程序退出时问题就会来了,由于Application会主动释放自己的Components内的元素,而我们自己再次调用FreeAndNil时就会出现对象的多次释放,导致程序无法正常退出!!!

反例代码:

//在Create时创建对象
FFoolPan := TPanel.Create(Application);
 
//在Destroy时释放资源
//旁白:不要以为做了Assigned判断就万事大吉了,遇到”悬空指定”你会死得很难看 
if Assigned(FFoolPan ) then FreeAndNil(FFoolPan );

 

好了,现在开始分析问题的原因,为了刨根问底,我们只有深入Delphi的VCL去探险了…
要知道TPanel.Create()到底做了什么,我们得去问TPanel的祖先类TComponent,因因它有了组件列表的概念,列表中的元素必须是TComponent的实例,且属于此TComponent,它会在其构造函数中主动的释放列表中的实例,注意,它只会调用列表中元素实例的Destroy方法,而不将其置为nil,“悬空指针”从此诞生。

//------在Create时将自己插入到组件列表Components当中  Start------//
constructor TComponent.Create(AOwner: TComponent);
begin
  FComponentStyle := [csInheritable];
  if AOwner <> nil then AOwner.InsertComponent(Self);       //将自己插入到AOwner的组件列表中
end;

//我们来看看InsertComponent到底做了什么    (PS:由贴出了与此问题相关的代码,下同)
procedure TComponent.InsertComponent(AComponent: TComponent);
begin
  AComponent.ValidateContainer(Self);
  ValidateRename(AComponent, '', AComponent.FName);
  Insert(AComponent);       //Insert就发生在此时
end;

procedure TComponent.Insert(AComponent: TComponent);
begin
  if FComponents = nil then FComponents := TList.Create;
  FComponents.Add(AComponent);
  AComponent.FOwner := Self;
end;
//------在Create时将自己插入到组件列表Components当中  Edn------//

 

//------TComponent释放组件列表Components中的实例  Start------//
destructor TComponent.Destroy;
begin
  Destroying;
  DestroyComponents;            //释放Components
  if FOwner <> nil then FOwner.RemoveComponent(Self);
  inherited Destroy;
end;

procedure TComponent.DestroyComponents;
var
  Instance: TComponent;
begin
  while FComponents <> nil do
  begin
    Instance := FComponents.Last;
    if (csFreeNotification in Instance.FComponentState)
      or (FComponentState * [csDesigning, csInline] = [csDesigning, csInline]) then
      RemoveComponent(Instance)
    else
      Remove(Instance);
    Instance.Destroy;           //只调用了Destroy,却没置为nil,引入悬空指针,情何以堪...
  end;
end;
//------TComponent释放组件列表Components中的实例  End------//

现在我们明白了Create(Application)和Create(nil)的一个重要的区别了:使用Create(Application)所创建的对象的释放由Application来做,Create(nil)构造的对象需要自己来做资源的释放。

那程序退出时,Delphi都做了些什么呢?

我们从简单的情况入手,看看在系统的主窗体关闭时,我们的程序都执行了些什么操作。我们来看TCustomForm的WMClose,它的声明如下:

procedure WMClose(var Message: TWMClose); message WM_CLOSE;
既然接收了WM_CLOSE消息,那到底做了什么呢?

procedure TCustomForm.WMClose(var Message: TWMClose);
begin
  Close;      //很简单,只是调用了Close而已
end;

真像会在Close里面吗?

procedure TCustomForm.Close;
var
  CloseAction: TCloseAction;
begin
  if fsModal in FFormState then
    ModalResult := mrCancel
  else
    if CloseQuery then
    begin
      if FormStyle = fsMDIChild then
        if biMinimize in BorderIcons then
          CloseAction := caMinimize else
          CloseAction := caNone
      else
        CloseAction := caHide;
      DoClose(CloseAction);
      if CloseAction <> caNone then
        if Application.MainForm = Self then Application.Terminate   //我们找到Application.Terminate了,不错
        else if CloseAction = caHide then Hide
        else if CloseAction = caMinimize then WindowState := wsMinimized
        else Release;
    end;
end;

Application.Terminate的作用时让程序终止执行,即退出应用程序。更详细的说明是Terminate会通过调用PostQuitMessage(0)发送消息WM_QUIT面终止程序。

function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
  Handled: Boolean;
begin
  Result := False;
  if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then   //在消息队列中获取消息
  begin
    Result := True;
    if Msg.Message <> WM_QUIT then
    begin
      Handled := False;
      if Assigned(FOnMessage) then FOnMessage(Msg, Handled);
      if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and
        not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
      begin
        TranslateMessage(Msg);
        DispatchMessage(Msg);
      end;
    end
    else    //如果接收到WM_QUIT消息,将退出标志置为true 
      FTerminate := True;           
  end;
end;

在中我们可以在TApplication.Run看到,系统会通过HandleMessage调用ProcessMessage处理消息,直到退出标志为true时,才终止。

 

 

 

procedure TApplication.Run;
begin
  FRunning := True;
  try
    AddExitProc(DoneApplication);    //将DoneApplication添加到TApplication.Run退出之后执行列表中
    if FMainForm <> nil then
    begin
      case CmdShow of
        SW_SHOWMINNOACTIVE: FMainForm.FWindowState := wsMinimized;
        SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized;
      end;
      if FShowMainForm then
        if FMainForm.FWindowState = wsMinimized then
          Minimize else
          FMainForm.Visible := True;
      repeat
        try
          HandleMessage;
        except
          HandleException(Self);
        end;
      until Terminated;         //退出标志为true时退出
    end;
  finally
    FRunning := False;
  end;
end;

 

Application.Ran退出后,我们看看DoneApplication会做些什么。

procedure DoneApplication;
begin
  with Application do
  begin
    if Handle <> 0 then ShowOwnedPopups(Handle, False);
    ShowHint := False;      
    Destroying;             
    DestroyComponents;      //调用Application.DestroyComponents方法
  end;
end;

 

我们现在又回到了DestroyComponents方法,很熟悉的感觉,Application的DestroyComponents会有什么不同呢?

情况并没有不同,它没有重写DestroyComponents,还是使用的TComponent.DestroyComponents方法。好了,现在我们也该明白为什么在TPanel.Create(Application)之后,不会再手动调用FreeAndNil(FFoolPan )了。

切忌:内存的重复释放引发的危害,远远比内存泄漏来得大来得猛烈。

有一篇博文就是讲“为什么重复free()比内存泄漏危害更大”,有兴趣的同学可以过去瞧瞧。

说了这么多,我们也该休息下了 :)

 

------------仅以此文,献给我自己、HOMS开发的同学们,还有深受客户端退出无响应的受害者------------

 PS:个人新干博客地址 http://www.lontoken.com/

posted @ 2012-09-24 02:01  lontoken  阅读(564)  评论(0编辑  收藏  举报