智能指针和 COM 服务器卸载。第 4 部分





5.00/5 (1投票)
在本文中,我将介绍一种使用智能指针处理 COM 服务器卸载问题的方法。
引言
我将提醒大家,在本系列文章中,我研究了 COM 服务器应用程序未卸载的情况。在上一篇文章中,我介绍了智能指针的概念,这可能有助于跟踪应用程序卸载的情况。在本文的这一部分,我将制作一个完整的应用程序示例,该示例使用智能指针的概念来跟踪应用程序卸载的情况。
目录
本文由几部分组成。以下是文章各部分的完整列表
基本思想
首先让我描述一下基本思想。我打算用指向入口点接口的智能指针替换所有包含入口点接口引用的字段。这些智能指针将在创建时自动在全局注册表中注册它们自己。当然,它们也会在清理时从这个全局注册表中清除自己。除此之外,我将使每个显示的表单在全局表单注册表中注册它自己。当表单被隐藏时,它会从全局注册表中清除自己。并且在每个表单销毁后,将执行检查。此检查将检查是否没有可见的应用程序指针,并且全局注册表中是否还有智能应用程序指针。在这种情况下,检查器将创建所有剩余的智能应用程序指针的堆栈写入日志文件。使用此日志文件,应用程序开发人员很可能会发现哪些入口点引用未正确释放。
智能指针注册表
我简化了智能指针的实现,并摆脱了泛型,这让事情变得有点困难。结果实现如下所示
type
// Smart application pointer.
ISmartApplication = reference to function: ITestUnloadApplication;
// Create smart application pointer.
function CreateSmartApplication(
// Application.
const AApplication: ITestUnloadApplication): ISmartApplication;
implementation
...
type
// Smart application pointer implementation.
TSmartApplication = class(TInterfacedObject, ISmartApplication)
private
// Application interface reference.
FApplication: ITestUnloadApplication;
{ ISmartApplication }
function Invoke: ITestUnloadApplication; inline;
public
// Constructor.
constructor Create(
// Application interface reference.
const AApplication: ITestUnloadApplication);
{ TObject }
destructor Destroy; override;
end;
{ TSmartApplication }
function CreateSmartApplication(const AApplication: ITestUnloadApplication): ISmartApplication;
begin
if Assigned(AApplication) then
Result := TSmartApplication.Create(AApplication)
else
Result := nil;
end;
constructor TSmartApplication.Create(
const AApplication: ITestUnloadApplication);
begin
inherited Create;
FApplication := AApplication;
TSmartApplicationRegistry.Instance.RegisterApplication(Self);
end;
destructor TSmartApplication.Destroy;
begin
FApplication := nil;
TSmartApplicationRegistry.Instance.UnregisterApplication(Self);
inherited;
end;
function TSmartApplication.Invoke: ITestUnloadApplication;
begin
Result := FApplication;
end;
添加了 CreateSmartApplication
函数,以确保当指向的接口等于 nil
时,指向应用程序接口的旧智能指针也等于 nil
。此实现的关键部分是,每个应用程序智能指针都会在全局应用程序指针注册表中注册自己。
智能应用程序指针注册表是一个简单的单例对象,它跟踪智能应用程序指针的创建和销毁。单例实现很简单,所以我将跳过它的大部分无趣的部分。每次注册智能指针时,智能应用程序注册表类都会记住导致智能应用程序指针创建的调用堆栈(我使用 JCL 来捕获调用堆栈)。智能应用程序注册表实现的最有趣的部分如下
type
// Registry of smart applications.
TSmartApplicationRegistry = class
private class var
// Registry instance.
FInstance: TSmartApplicationRegistry;
private
// Dictionary of applications.
FApplications: TDictionary<Pointer,String>;
// Get smart application pointer count.
function GetCount: Integer;
public
// Constructor.
constructor Create;
{ TObject }
destructor Destroy; override;
// Create instance.
class function Instance: TSmartApplicationRegistry;
// Destroy instance.
class procedure DestroyInstance;
// Register smart application.
procedure RegisterApplication(
// Pointer to smart application.
const APointer: Pointer);
// Unregister smart application.
procedure UnregisterApplication(
// Pointer to smart application.
const APointer: Pointer);
// Write to log alive smart applications.
procedure WriteToLog;
// Smart application pointer count.
property Count: Integer read GetCount;
end;
implementation
...
function GetCallStack: String;
var
JclStackInfoList: TJclStackInfoList;
StringList: TStringList;
begin
JclStackInfoList := JclCreateStackList(True, 3, Caller(0, False));
try
StringList := TStringList.Create;
try
JclStackInfoList.AddToStrings(StringList, True, False, True, True);
Result := StringList.Text;
finally
StringList.Free;
end;
finally
JclStackInfoList.Free;
end;
end;
{ TSmartApplicationRegistry }
...
procedure TSmartApplicationRegistry.WriteToLog;
const
LOG_FILE_NAME = 'logfile.log';
APPLICATION_NOT_UNLOADED_ERROR_MESSAGE = 'Application is not unloaded.';
APPLICATION_SMART_POINTER_MESSAGE = 'Application pointer (0x%s). Call stack: ' + sLineBreak + '%s';
var
LogFile: Text;
LogFileName: String;
SmartPointer: Pointer;
begin
LogFileName := LOG_FILE_NAME;
Assign(LogFile, LogFileName);
try
if FileExists(LogFileName) then
Append(LogFile)
else
Rewrite(LogFile);
if FApplications.Count > 0 then
begin
WriteLn(LogFile, APPLICATION_NOT_UNLOADED_ERROR_MESSAGE);
for SmartPointer in FApplications.Keys do
WriteLn(LogFile, Format(APPLICATION_SMART_POINTER_MESSAGE,
[IntToHex(Integer(SmartPointer), 8), FApplications.Items[SmartPointer]]));
end;
finally
CloseFile(LogFile);
end;
end;
表单注册表
应用程序显示的每个表单都会在全局表单注册表中注册。当表单被隐藏时,它会从全局表单注册表中清除自己
procedure TTestForm.FormHide(Sender: TObject);
begin
TFormRegistry.Instance.UnregisterForm(Self);
end;
procedure TTestForm.FormShow(Sender: TObject);
begin
TFormRegistry.Instance.RegisterForm(Self);
end;
表单注册表的实现很简单,因此我将仅展示其接口部分
type
// Form registry class.
TFormRegistry = class
private class var
// Instance.
FInstance: TFormRegistry;
// Get form count.
function GetCount: Integer;
private
// Registered forms.
FForms: TList<TForm>;
public
// Constructor.
constructor Create;
{ TObject }
destructor Destroy; override;
// Create instance.
class function Instance: TFormRegistry;
// Destroy instance.
class procedure DestroyInstance;
// Register form.
procedure RegisterForm(
// Form.
const AForm: TForm);
// Unregister form.
procedure UnregisterForm(
// Form.
const AForm: TForm);
// Form count.
property Count: Integer read GetCount;
end;
...
卸载检查器
我实现了一个单独的类 TUnloadChecker
,它实现了应用程序未卸载检查。它创建了一个不可见的辅助窗口。使用此技巧是因为我们在表单析构函数中启动执行检查,此时并非所有资源都已清理完毕。所以我们需要在清理代码成功完成后执行一些代码。这就是为什么我使用 PostMessage
向辅助窗口发送用户消息,并在该辅助窗口的窗口过程中执行实际检查
type
// Class which performs non-unloading checks.
TUnloadChecker = class
private class var
// Instance.
FInstance: TUnloadChecker;
private
// Invisible auxiliary window handle.
FWindowHandle: THandle;
// Auxiliary window procedure.
procedure WindowProcedure(
// Window message.
var AMessage: TMessage);
public
// Constructor.
constructor Create;
{ TObject }
destructor Destroy; override;
// Create instance.
class function Instance: TUnloadChecker;
// Destroy instance.
class procedure DestroyInstance;
// Post non-unloading check.
procedure PostCheck;
end;
implementation
...
{ TUnloadChecker }
procedure TUnloadChecker.PostCheck;
begin
PostMessage(FWindowHandle, WM_CHECK_APPLICATION_UNLOAD, 0, 0);
end;
procedure TUnloadChecker.WindowProcedure(var AMessage: TMessage);
begin
with AMessage do
begin
if Msg = WM_CHECK_APPLICATION_UNLOAD then
begin
if ComServer.StartMode <> smAutomation then
begin
if (TFormRegistry.Instance.Count = 0) and
(TSmartApplicationRegistry.Instance.Count > 0) then
TSmartApplicationRegistry.Instance.WriteToLog;
end;
Result := 0;
end
else
Result := DefWindowProc(FWindowHandle, AMessage.Msg, AMessage.WParam, AMessage.LParam);
end;
end;
正如我之前所说,检查过程是在表单析构函数中启动的
destructor TTestForm.Destroy;
begin
inherited;
TUnloadChecker.Instance.PostCheck;
end;
最后的代码更改
我们剩下要做的就是用智能指针替换所有引用入口点接口的字段,并用一个函数替换对这些字段的赋值,该函数将从入口点接口创建智能指针。代码的其余部分可以保持不变。
我对 TTestForm
类所做的更改如下
type
// Test application form.
TTestForm = class(TForm)
...
private
// It is now smart pointer!
FApplication: ISmartApplication;
...
end;
implementation
...
{ TTestForm }
procedure TTestForm.FormCreate(Sender: TObject);
begin
// We now need to call this function to create smart pointer!
FApplication := CreateSmartApplication(TTestUnloadApplication.CreateFromFactory(
ComClassManager.GetFactoryFromClass(TTestUnloadApplication), nil));
end;
我对 TTestLeak
类所做的更改如下
type
// Test leak class.
TTestLeak = class
private
// It is now smart pointer!
FApplication: ISmartApplication;
...
end;
implementation
...
{ TTestLeak }
constructor TTestLeak.Create(const AApplication: ITestUnloadApplication);
begin
inherited Create;
// We now need to call this function to create smart pointer!
FApplication := CreateSmartApplication(AApplication);
end;
收益
现在我可以启动 TestUnloadApp.exe
,单击 DoLeak
按钮,并且在应用程序表单关闭后,将在日志文件中生成以下记录(仅显示日志的一部分)
Application is not unloaded.
Application pointer (0x02AA2EA0). Call stack:
... SmartApplicationRegistry.GetCallStack$qqrv (Line 57...)
... SmartApplicationRegistry.TSmartApplicationRegistry.RegisterApplication$qqrpxv (Line 108...)
... SmartApplication.TSmartApplication... (Line 55...)
... SmartApplication.CreateSmartApplication... (Line 43...)
... TestLeak.TTestLeak... (Line 31...)
... TestUnloadApplication.TTestUnloadApplication.DoLeak... (Line 57...)
... TestFm.TTestForm.DoLeakButtonClick (Line 47...)
...
如您所见,我们可以设置我们的应用程序来创建这样的日志文件。在日志文件中,我们将看到应用程序未在预期卸载时未卸载的每种情况。并且在每种情况下,我们将看到在应用程序最后一个可见窗口关闭时未释放的入口点接口引用的列表。