65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2015年11月21日

CPOL

5分钟阅读

viewsIcon

7603

downloadIcon

105

在本文中,我将介绍一种使用智能指针处理 COM 服务器卸载问题的方法。

引言

COM技术是一项很棒的技术,它允许在Windows平台上进行透明的应用程序交互。但它也有其缺点,其中之一就是增加了应用程序开发的复杂性(顺便说一下,Delphi 使许多事情变得简单得多)。 

关于COM服务器应用程序的一个基本事实是,只要有活动的COM客户端保持其入口点接口的引用,COM服务器就必须保留在内存中。当最后一个入口点接口引用被释放时,COM服务器可以卸载。但有时,入口点接口引用会在COM服务器应用程序内部被错误使用。因此,COM服务器认为入口点接口仍然被引用,而实际上所有对它们的引用都来自COM服务器本身。这会导致COM服务器永远停留在内存中,直到有人强制终止它。

可以像普通应用程序一样带有用户界面的COM服务器是一种更糟糕的情况,因为它内部可能会使用入口点接口,即使应用程序不是作为COM服务器启动的。而且问题可能在用户长时间使用后发生。

首先,您需要跟踪此类卸载情况。有时用户会抱怨这些问题,有时则不会。坏消息是,您不总是有稳定的步骤序列来在开发环境中重现这些情况。在本文中,我想介绍一种技术,可以使调查COM服务器卸载案例的过程变得更加容易。

目录

本文由几部分组成。以下是文章各部分的完整列表

背景

在本文中,我将不涉及COM技术的细节。因此,假设您已具备一些基本知识。这个话题可能会引起那些熟悉COM技术的开发者的兴趣。对于初学者,市面上有很多不错的书籍和资料。

不卸载的服务器

为了说明这个问题,我实现了一个简单的COM服务器应用程序。 

它有一个入口点接口ITestUnloadApplication,看起来是这样的:

  ITestUnloadApplication = interface(IDispatch)
    ['{59DC4CB0-33EE-4B68-8209-C1CEC8E341A5}']
    procedure DoSomething; safecall;
    procedure DoLeak; safecall;
    ...
  end;

入口点接口的实现TTestUnloadApplication看起来是这样的:

type
  // Implementstion of COM-server entry object.
  TTestUnloadApplication = class(TAutoObject, ITestUnloadApplication)
  private
    { ITestUnloadApplication }
    procedure DoSomething; safecall;
    procedure DoLeak; safecall;
    ...
  public
    // Constructors.
    constructor Create;
    constructor CreateFromFactory(Factory: TComObjectFactory;
                                  const Controller: IUnknown);
    { TObject }
    destructor Destroy; override;
  end;

implementation
...

{ TTestUnloadApplication }

constructor TTestUnloadApplication.Create;
begin
  inherited;
  MessageBox(0, 
    'TTestUnloadApplication.Create', 
    'Test Unload Application', 
    MB_OK or MB_SYSTEMMODAL);
end;

constructor TTestUnloadApplication.CreateFromFactory(Factory: TComObjectFactory;
  const Controller: IInterface);
begin
  MessageBox(0, 
    'TTestUnloadApplication.CreateFromFactory', 
    'Test Unload Application', 
    MB_OK or MB_SYSTEMMODAL);
  inherited;
end;

destructor TTestUnloadApplication.Destroy;
begin
  MessageBox(0, 
    'TTestUnloadApplication.Destroy', 
    'Test Unload Application', 
     MB_OK or MB_SYSTEMMODAL);
  inherited;
end;

procedure TTestUnloadApplication.DoLeak;
var
  TestLeak: TTestLeak;
begin
  TestLeak := TTestLeak.Create(Self as ITestUnloadApplication);
end;

procedure TTestUnloadApplication.DoSomething;
begin
  MessageBox(0, 
    'TTestUnloadApplication.DoSomething', 
    'Test Unload Application', 
    MB_OK or MB_SYSTEMMODAL);
end;

initialization
  TTestUnloadApplicationFactory.Create(ComServer, TTestUnloadApplication, Class_TestUnloadApplication,
    ciMultiInstance, tmApartment);

您可以看到,COM客户端可以使用两个方法:

  • DoSomething方法只是显示一个消息框。
  • DoLeak方法创建一个TTestLeak对象。

DoLeak方法会造成内存泄漏,因为对象析构函数没有被调用。除了内存泄漏之外,此方法还会增加入口点接口的引用计数,因为入口点接口引用被作为TTestLeak构造函数的参数提供,并且后来会像您稍后看到的那样存储在TTestLeak对象字段中。

我实现了一个自定义的COM对象工厂TTestUnloadApplicationFactory,以确保当COM客户端创建入口点实例时,会调用我重写的CreateFromFactory构造函数。我还实现了OnLastRelease处理程序,以确保即使应用程序不是作为COM服务器运行(我将在本文后面讨论这种方法),在最后一个对象释放后应用程序也能关闭。所以工厂的实现如下:

type
  // Application factory.
  TTestUnloadApplicationFactory = class(TAutoObjectFactory)
  private
    // Handle last object release.
    procedure HandleLastRelease(
      // Shutdown flag.
      var Shutdown: Boolean);
    { TComObjectFactory }
    function CreateComObject(const Controller: IUnknown): TComObject; override;
  public
    // Constructor.
    constructor Create(ComServer: TComServerObject; AutoClass: TAutoClass;
      const ClassID: TGUID; Instancing: TClassInstancing;
      ThreadingModel: TThreadingModel = tmSingle);
    { TObject }
    destructor Destroy; override;
  end;

implementation
...

{ TTestUnloadApplicationFactory }

constructor TTestUnloadApplicationFactory.Create(ComServer: TComServerObject;
  AutoClass: TAutoClass; const ClassID: TGUID; Instancing: TClassInstancing;
  ThreadingModel: TThreadingModel);
begin
  inherited Create(ComServer, AutoClass, ClassID, Instancing, ThreadingModel);
  if ComServer is TComServer then
    (ComServer as TComServer).OnLastRelease := HandleLastRelease;
end;

destructor TTestUnloadApplicationFactory.Destroy;
begin
  if ComServer is TComServer then
    (ComServer as TComServer).OnLastRelease := nil;
  inherited;
end;

function TTestUnloadApplicationFactory.CreateComObject(
  const Controller: IInterface): TComObject;
begin
  Result := TTestUnloadApplication.CreateFromFactory(Self, Controller);
end;

procedure TTestUnloadApplicationFactory.HandleLastRelease(
  var Shutdown: Boolean);
begin
  Shutdown := True;
end;

TTestLeak对象只是像这样保留入口点接口引用:

type
  // Test leak class.
  TTestLeak = class
  private
    // Application.
    FApplication: ITestUnloadApplication;
  public
    // Constructor.
    constructor Create(
      // Application.
      const AApplication: ITestUnloadApplication);
    { TObject }
    destructor Destroy; override;
  end;

implementation
...

{ TTestLeak }

constructor TTestLeak.Create(const AApplication: ITestUnloadApplication);
begin
  inherited Create;
  FApplication := AApplication;
end;

destructor TTestLeak.Destroy;
begin
  FApplication := nil;
  inherited;
end;

应用程序包含一个主窗体,该窗体不包含任何逻辑。窗体只是在应用程序启动时创建,以确保我们可以进入应用程序的消息循环。主窗体在COM服务器和GUI启动模式下都是不可见的。正如您稍后将看到的,我们需要这样一个不可见的主窗体来确保应用程序在关闭其窗体时不会关闭(尤其是在应用程序作为COM服务器运行且COM客户端仍需要使用它时)。

应用程序还包含一个测试窗体TTestForm。当应用程序不是作为COM服务器启动时,将显示此窗体。窗体在创建时创建入口点接口引用,并在销毁时释放此引用。窗体包含两个按钮:

  • DoSomething只是调用ITestUnloadApplication.DoSomething方法。
  • DoLeak按钮只是调用ITestUnloadApplication.DoLeak方法。

启动时,应用程序会显示信息消息框,创建不可见的主应用程序窗体,并且如果不是作为COM服务器运行,则创建TTestForm应用程序窗体。启动代码如下:

begin
  MessageBox(0, 'Application launched', 'Test Unload Application', MB_OK or MB_SYSTEMMODAL);
  Application.Initialize;
  Application.ShowMainForm := False;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TMainForm, MainForm);
  if ComServer.StartMode <> smAutomation then
  begin
    Application.CreateForm(TTestForm, TestForm);
    TestForm.Show;
  end;
  Application.Run;
end.

要注册COM服务器,我们只需使用/regserver命令行开关执行它。

TestUnloadApp.exe /regserver

现在我们可以使用以下VBS脚本重现内存泄漏:

Dim TestUnloadApp
Set TestUnloadApp = WScript.CreateObject("TestUnloadApp.TestUnloadApplication")
Call TestUnloadApp.DoSomething
Call TestUnloadApp.DoLeak

当您执行脚本时,会发生以下情况:

  • 应用程序启动时,消息框会显示。
  • 创建入口点接口实例时,消息框会显示。
  • 调用DoSomething方法时,消息框会显示。
  • 之后不再显示任何消息框。

当脚本宿主应用程序终止时,TestUnloadApp.exe仍将在内存中。正如您所见,TTestUnloadApplication类实例是通过COM机制创建的,但之后从未销毁。

还有另一种方法会导致应用程序无法卸载。您可以手动启动TestUnloadApp.exe并按下DoLeak按钮,然后尝试关闭唯一的可见应用程序窗口。当您这样做时,会发生以下情况:

  • 应用程序启动时,消息框会显示。
  • 创建入口点接口实例时,消息框会显示。
  • TTestForm应用程序窗体将显示。
  • 当您点击DoLeak窗体按钮时,不会显示任何消息框。
  • 当您点击Close窗体按钮时,不会显示任何消息框。
  • 之后不再显示任何消息框。

所以,我们已经重现了我们感兴趣的卸载情况。在我们的情况下,找出卸载的原因并修复应用程序相对容易。但在真实的 codebase中,任务可能会更困难。

下一步

在本文的第一部分,我解释了我想要解决的问题。所以您现在可以看到,在实现COM服务器应用程序时,很容易遇到应用程序不卸载的情况。

下一步是了解应用程序卸载行为的深度。

我们的最终目标是生产一些工具,使解决应用程序卸载问题比开箱即用更容易。

© . All rights reserved.