第2章 VCL的诞生和设计原理

VCL Framework的诞生

VCL Framework的架构设计

VCL Framework的对象服务

Object Pascal 的对象模型和TObject

虚拟方法(Virtual Method)和动态方法(Dynamic Method)

什么是VMT(Virtual Method Table)?VMT 的架构和内容


1.Borland VCL Framework的诞生

ⅤCL在设计之初就定下几个目标:

使用单一继承架构

不限于16或32位平台

开放组件,即程序员可以开发自定义组件

在设计时期就能提供功能

使用PEM(property-event-method)模型

必须使用面向对象技术设计和实现(单一继承而非C++那样多重继承)

必须完善地封装和分派窗口消息

后来又加入了设计模式、远程计算、COM、接口技术、Framework切割、Package技术

通用接口机制和Weak Link技术允许不被使用的VCL Framework程序单元不需要连接到Delphi程序当中

2.VCL的架构设计

VCL Framework提供了许多服务:对象基础服务、分配消息服务、对象持久化、RTTI、COM/COM+支持、数据库、分布式计算,在VCL Framework的底层是由Object Pascal程序语言、Delphi编译器支持,再上来是Windows SDK 和Windows Messaging的架构。这些基础架构之上派生了VCL Framework整体架构服务

本章讨论的内容集中在底层基础服务

3.从无到有--VCL对象生命的成形

VCL Framework的第一步便是提供VCL对象生命周期(Object Lifecycle)以及VCL对象管理(Object Management)的能力,依据此基础服务来派生出其它服务

基本的对象管理服务至少应该包含:

对象的创建和初始化

对象方法的分配

对象的消毁

上述三项工作,有些要由程序语言、编译器提供服务才能实现

一般的类都会定义对象的构造函数(constructor或ctor),而构造函数的目的就是为对象分配内存和进行初始化的工作

还要有析构函数(destructor或dtor),对象生命结束之后释放分配的内存,声明成虚函数,以便派生类有机会先释放本身类分配的资源

TObject = class

    constructor Create;

    destructor Destory; virtual;

end;

TMyOject = class(TObject)

    destructor Destory; override;

end;

destructor TMyObject.Destory;

begin

//释放TMyObject分配的资源

inherited Destory;//调用TObject.Destory

end;

obj := TMyObject.Create;

Object Pascal对象模型在此行代码内部进行了许多工作,包括内存分配、特殊字段设定和设置执行框架,因此上面代码可以分解为

TMyObject.Allocatememory;

TMyObject.InitializeSpecialFields

TMyObject.SetupExecFrame;

分配内存之后,Object Pascal对象模型会先初始化所有的内存为0

FillChar(Instance^,InstanceSize, 0);

接着设置特殊字段,包括接口计数,动态数组,Variant变量,虚函数

设置执行框架的目的在于把内存块转变成对象,为此就要把内存内容初始化为对象声明的字段、变量、方法,为对象设定正确的VMT并且串联起正确的继承架构

4.Object Pascal对象服务


对象创建服务

对象释放服务

对象识别服务

对象信息服务

对象消息分派服务

4-1对象创建服务

TObject类中和对象创建服务有关的函数

TObject = class

    constructor Create;

    destructor Destory; virtual

    class function NewInstance: TObject; virtual

    class function InitInstance(Instance: Pointer): TObject;

    class function InstanceSize: Longint;

    procedure AfterConstruction; virtual;

end;

NewInstance的功能即是为对象分配内存,并且调用InitInstance方法为对象设定对象支持的接口,调用完成后仍然无法使用,还需要设定对象的执行框架

InitInstance方法的功能是为对象设定类支持的接口

要创建TBase对象时除了以下常用的这行

aBObj := TBase.Create;

也可以使用如下代码

var

    aBObj : TBase;

    aObj : TObject;

begin

...

    try

        aObj := TObject(TBase.NewInstance);//分配内存和初始化

        aBObj := TBase(aObj.Create);//设定执行框架,Create发现已经调用过NewInstance,就跳过它

...

    finally

        FreeAndNil(aBObje);

    end;

end;

在Delphi的应用程序中主程序都使用了类似下面的代码来创建TForm对象

Application.CreateForm(TForm1, Form1);

而CreateForm的源代码是

procedure TApplication.CreateForm(InstanceClass: TComponentClass; var Reference);

var

    Instance: TComponent;

begin

    Instance := TComponent(InstanceClass.NewInstance);//分配内存和初始化

    TComponent(Reference) := Instance;

    try

        Instance.Create(self);//设定执行框架

    except

        TComponent(Reference) := nil;

        raise;

    end;

...

end;

所以虽然函数称为CreateForm,但由于它可以创建任何TComponent派生对象,因此我们也可以使用它创建任何CVL组件,例如下面的程序创建一个TButton对象

Application.CreateForm(TButton, aButton);

aButton.Parent := Self;

aButton.Caption := 'CreateForm';

aButton.Show;

...

注:类型转换(Type Casting)实际上就是让编译器产生调整执行框架的工作让对象变量能够正确地存取到其执行框架之中的特性、方法、和事件。

类型转换之前应该用is来判断是不是属于某个类型,如:

if(aObj is TForm1)then

...

4-2对象识别服务

此服务目的是为了获得应用程序执行时期对象的信息

以下的方法即是TObject提供的基础识别服务方法:

TObject = class

...

class function ClassName: ShortString;

class function ClassNameIs(const Name: string):Boolean;

class function ClassParent: TClass;

class function InstanceSize: Longint;

class function InheritsFrom(AClass: TClass):Boolean;

...

类方法:相当于一个全局函数,而且程序代码中不需要这个类的对象实体来调用,可以直接使用,即TaClass.aClassMethod(); 这样即可调用,类似C++中的静态成员函数

如以下可以使用TObject或TObject的对象来调用ClassName这个识别服务方法

var

aObject: TObject;

begin

ListBox1.Items.Add(TObject.ClassName);

aObject:= TObject.Create();

try

ListBox1.Items.Add(aObject.ClassName);

finally

ListBox1.Free;

end;

end;

【注】Delphi的编译器在编译类的声明时会自动为类产生识别信息,比如在编译类时就决定了类对象的大小,而InstanceSize方法就返回由编译器决定的对象大小。

4-3对象信息服务

TObject提供的对象服务信息可以允许程序员深入追踪对象的构造函数信息,这些信息包括对象的VMT、VMT的内容、对象支持的接口、接口中的方法、对象方法地址等,通常这些服务是由VCL Framework内部使用,一般的应用程序很少使用,但这些信息服务方法却是深入Object Pascal对象架构的大门,以下是类提供的对象信息服务方法

TObject = class

...

//返回类的RTTI表格信息

class function ClassInfo : Pointer;

//返回published类方法的地址

class function MethodAddress(const Name: ShortString): Pointer;

//返回类方法名称

class function MethodName(Address: Pointer): ShortString;

//返回published的字段变量

function FieldAddress(const Name: ShortString): Pointer;

//返回类实现的接口信息

function GetInterface(const IID: TGUID; out Obj): Boolean;

//返回特定接口信息

class function GetInterfaceEntry(const IID: TGUID): PinterfaceEntry;

//返回类所实现接口的表格信息

class function GetInterfaceTable: PInterfaceTable;

//处理使用safe call异常的虚拟方法

function SafeCallException(ExceptObject:TObject; ExceptAdd: Pointer):HResult;virtual;

...

end;

MethodName只会对从TCoponent继承下来的Published方法有用

【注】使用reintroduce关键字明确告诉编译器要覆盖父类同名函数,这样就不会产生警告信息,当然不使用也会同产覆盖,只是会产生一个警告

方法种类

1). 类方法( class method),静态方法(Static method)

2). 对象方法( Object method)

3). 虚方法( virtual method)

4). 动态方法( dynamic method)

5). 重载方法(overload method)

6). 事件处理函数(Event Handler)

动态函数的功能和用法与虚函数一样,但可以大幅度减少VMT的大小,不过效率会比虚函数缓慢。

对于虚函数,每个子类对象都要复制一份父类的VMT,这是为了提高执行效率,而对于动态函数则不复制VMT,当要调用动态函数时,会在本类中的VMT找,找不到就往上级父类的VMT中找,如果还找不到,再向上级找,直到找到为止,虽然效率低了,但大大减少了VMT的大小。

事件处理函数是VCL Framework类继承的末端,执行速度需要快,所以事件处理函数都是使用register calling convention来实现的,以便提供最高的执行效率

Borland 的Together软件能够读取程序员开发的DOIH(Depth Of Inheritance Hierarchy)的数值。DOIH会检查程序员的源程序来找出哪些类架构的继承深度超出了特定的数值。

一般类架构以不超过5层为原则

动态函数一般使用在VCL Framework继承系列类中的中间的类,或是较少使用的虚拟方法,但对于属于继承架构的叶节点类,如TEdit,那么便会使用虚拟方法,以便以最有效率的速度调用到程序员写的事件处理函数。

以往C/C++ Framework在继承深度大于一定数量时便要使用大量的VMT空间,但在VCL Framework中混合虚拟方法和动态方法的设计就解决了这个问题,中间类或较少使用的虚拟方法改成动态方法来声明可能只比传统的虚拟方法慢了3%左右的时间,但是可以节省30%-50%的VMT空间,这便是以时间换空间的抉择

5.从原始基本对象到提供基本服务的VCL对象

TObject的功能就是让一块内存块成为能够提供服务的实用对象,类似其它的Framework,如MFC,OWL,.Net,Java Class Library都是使用这样的设计模型,定义一个提供核心服务的基础类,并且让其它类从这个基础类继承下来

6.VCL对象的释放服务

Object Pascal对于对象的分配机制是使用堆分配(Heap Allocation),而不象C/C++一样可以同时使用堆分配和栈分配(Stack Allocation),意思是象这样的代码

var

aObj : TBase;

begin

...

程序员只是定义了TBase类的一个对象指针(对象引用),并没有实际分配TBase或是TBase的派生类的物理内存,程序员必须调用创建服务才会让对象在内存中实际成形

而对于C/C++

void

{

TBase aObj;

...

这样aObj已经实际在堆栈形成了实体对象,程序员马上就可以直接使用TBase提供的服务了

为什么Object Pascal不象C/C++那样在堆栈中分配对象呢?

这是因为当初Delphi 1推出之时,首席架构师(Chief Architect)Anders Hejlsberg本想在后续的版本中加入垃圾收集(Garbage Collection)机制,自动帮助程序员回收不使用的对象,初步决定Object Pascal不需要栈分配机制,然而在Anders离开了Borland之后,Delphi一直尚未加入垃圾收集机制。不过以后当Delphi For .NET推出后就不需要在程序语言阶层实现垃圾收集机制了,因为.NET虚拟平台已经在系统阶层提供了这个能力,Anders终于在.NET中实现了他的理想(Anders离开Borland而加入Microsoftware负责.NET架构项目)。

以下是TObject类的释放服务

TObject = class

...

procedure CleanupInstance;

procedure FreeInstance;virtual;

destructor Destroy; virtual;

procedure Free;

...

end;

两个有用的Delphi函数源代码

procedure FreeAndNil(var Obj);

var

  Temp: TObject;

begin

  Temp := TObject(Obj);

  Pointer(Obj) := nil;

  Temp.Free;

end;

procedure TObject.Free;

begin

If self <> nil then

    Destroy;

end;

TObject的析构函数Destroy声明成虚拟,就让派生类有机会释放派生类中的资源

Delphi的在线帮助并没有说析构函数一定要使用Destroy,只说要使用关键字destructor,

Destroy源代码如下

destructor TObject.Destroy

begin

end;

上述源代码中并不代表Destroy没有执行任何代码,Delphi编译器会自动在Destroy方法中产生调用TObject.BeforeDestruction和ClassDestroy的程序代码

BeforeDestruction的目的是为了达到让C++Builder的对象模型和Object Pascal对象模型整合

ClassDestroy是一般的程序,它的实现程序代码主要是经由对象实体调用FreeInstance来释放TObject

procedure _ClassDestroy(Instance: TObject);

begin

  Instance.FreeInstance;

end;

procedure TObject.FreeInstance;

begin

  CleanupInstance;

  _FreeMen(Self);

end;

FreeInstance为TObject释放资源进行了两步:

1. 是CleanupInstance释放Delphi类分配的特别数据类型变量的空间

2. 是_FreeMen(Self);释放Delphi类占据的内存空间

CleanupInstance释放特别数据类型变量的空间,包括:字符串数组,宽字符串数组,Variant变量,未定义类型数组,记录,接口和动态数据

FreeInstance的工作非常重要且复杂,所以一般类不会改写它,而只会调用它,它被声明成虚拟方法主要是为了让VCL Framework中比较偏系统级的类来改写,如EHeapException类便改写了TObject的FreeInstance以便处理堆产生的异常

TObject以上述4个释放资源的服务方法以及搭配Delphi编译器自动产生调用FreeInstance虚拟方法的机器代码之后,就对VCL Framework以及所有派生类提供了基础的释放资源服务,对于派生类所分配的资源,只要遵守:

改写Destroy虚拟析构函数

在改写Destroy函数中先释放派生类分配的资源

最后使用inherited关键字调用父类的虚拟析构函数

7.类和对象的Metadata-VMT(Virtual Method Table)

VMT的实现会因为不同的面向对象程序语言而有差异,即使是不同的Delphi版本也会产生不同的VMT结构和内容。所以VMT的实现是需要程序语言和编译器共同协助实现的

编译器产生的内容进行分门别类,把每个对象各自拥有的资源产生在对象的内存之中,把类共享的资源产生在另一块独立的内存之中,最后在对象内存之中插入指向共享资源的指针,那么各自独立的对象就能够通过这个内嵌指针存取共享资源,如类字段,类方法

而共享资源可以分成两类,一是一般的程序和函数或是类方法,另一种是提供面向对象的虚拟方法和动态方法。

编译器把一般程序和函数产生在特定的内存地址(根据程序和函数在类中声明的顺序),同时为每一个类创建一个拥有特定数据结构的表格,即VMT,在这个VMT中将产生指到类中每一个虚拟方法的指针,通过这个指针可以调用到虚拟方法。此外在这个表格中也会包括一个指向动态方法表格的指针,能够经由这个指针存取到所有类中定义的动态方法。这个VMT表即是Object Pascal中的Self指向的目的地址。当然为解决继承问题,在这个VMT表格中也有一个指向父类的VMT表格,以便允许编译器实现多态功能,最后编译器会在每一个对象独立的内存中插入一个指到VMT表格的指针,以便让每一个对象实体能够经由这个指针存取到类定义的虚拟方法、动态方法以及存取到父类的VMT表格

更详细的例子,参考原书P85

类的VMT架构都是一样的,只内容不同。TObject的ClassType函数会返回TClass类型的结果值,其实就是VMT的内容,ClassType的源代码如下,即返回Slef指针:

function TObject.ClassType:TClass;

begin

  Pointer(Result):=PPointer(Self)^;

end;

更详细的例子,参考原书P90

VMT是在第一个类对象被创建时才会创建完整的VMT,同一个类的所有对象都是共享一个VMT表格的。

8.结论

本章讨论了TObject提供的服务以及TObject的生命周期,在TObject提供了所有类需要的基本服务后,派生类才得以专注不周的功能

VCL Framework经由TObject提供的基础服务而开始构造Framework的功能,TObject类加上Object Pascal对象模型、Delphi编译器提供的功能如VMT,虚拟方法、动态方法、类方法等

TObject和Delphi编译器只是最底层的辅助服务,VCL Framework还需要Object Pascal强大的支持,即面向对象的功能,请看下一章......

你可能感兴趣的:(第2章 VCL的诞生和设计原理)