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/