学习《深⼊核⼼VCL架构剖析》摘要
第⼀章
第⼆章
2-1
VCL Framework设计之初便设定了数个⽬标:
使⽤单⼀的继承构架以避免陷⼊C++多重继承的问题,同时这也有助于简化Delphi编译器的开发⼯作
VCL Framework 必须不限于16位或32位平台
VCL Framework 必须提供开放的组件架构,以允许程序员开发⾃定义组件
VCL Framework 必须进化成可在设计时期即提供功能的Framework
VCL Framework 必须使⽤PME(Property-Event-Mothed)模型
VCL Framework 必须使⽤⾯向对象的技术来设计和实现
VCL Framework 必须完善地封装和分派窗⼝消息
2-3-1
TObject = class
constructor Create;
destructor Destroy; virtual;
end;
析构函数Destroy声明为虚拟⽅法是因为在TObject的派⽣类中可能会分配额外的资源,因此派⽣类可以改写(override)TObject的析构函数。当派⽣类对象释放时先释放它⾃⼰分配的资源,再调⽤TObject的析构函数来释放TObject为对象分配的资源。如果TObject的析构函数不声明成虚拟⽅法,那么派⽣类的析构函数便会覆盖TObject的析构函数,如此⼀来只有派⽣类分配的内存会被释放,由TObject为对象分配的资源则可能没有释放,这就造成了内存/资源泄漏(leak)的问题。
Object Pascal的对象模型在这⾏程序代码之后进⾏了许多的⼯作,包括了分配内存、设定字段变量数据结构以及设定执⾏框架等⼯作TMyObject.AllocateMemory;
TMyObject.InitializeSpecialFields;
Obj := TMyObject.SetupExecFrame;
在分配了对象原始内存之后,Object Pascal的对象模型会先初始化所有的内存内容为0:
FillChar(Instance^, InstanceSize, 0)
在分配和初始化内存之后的动作就是为类中声明的特别字段进⾏初始化的⼯作,也许读者会问为什么?不是已经使⽤FillChar把所有的字段变量内容初始化为0了吗?没错,不过这是属于⼀般的字段变量,对于⼀些特别的字段变量,Object Pascal对象模型必须为这些字段变量进⾏初始化数据结构的设定⼯作。例如对于接⼝变量必须设定引⽤计数值为0,对于动态数组则必须初始化内存区块等。例如声明了Variant类型的字段变量,那么Object Pascal的对象模式便会为其进⾏特别初始化的⼯作,⾄于虚拟⽅法则会进⼊VMT之中
2-3-2
在Object Pascal的对象模型为对象分配了内存之后,还有另外⼀个极为重要的⼯作,那就是为对象创建执⾏框架(Execution Frame)。执⾏框架的⽬的在于把对象模型分配的内存内容为转变成活⽣⽣⽣存在于内存的对象,要把内存内容转变为对象模型就牵涉到初始化内存的内容为对象声明的字段变量
、⽅法,为对象设定正确的VMT并且串联起正确的继承架构
⾯向对象程序语⾔和传统程序语⾔不⼀样的地⽅就是在分配内存之后执⾏的对象模型设定的加值⼯作,这些动作也就是让原始内存形成对象雏形的关键原因
2-4
Object Pascal对象模型提供的基础服务包含:
对象创建服务 —— 提供创建对象的机制
对象释放服务 —— 提供对象释放的机制
对象识别服务 —— 提供对象判断,识别的机制
对象信息服务 —— 提供程序代码存取对象信息的服务
对象消息分派服务 —— 提供Object Pascal分派消息的服务,和VCL封装窗⼝消息密切的关系
TObject的NewInstance是虚拟⽅法,代表派⽣类可以改写(override)它,NewInstance的功能即是为对象分配内存,并且调⽤InitInstance ⽅法为对象设定对象⽀持的接⼝。NewInstance的返回值是TObj
ect,代表调⽤了NewInstance之后Object Pascal的对象模型已经在内存形成了TObject的⼫体(instance),不过此时内存中的TObject仍然⽆法使⽤,因为接着需要设定对象的执⾏框架
InitInstance⽅法的功能是为对象设定类⽀持的接⼝
⼀般使⽤Create⽅法创建对象时经过了3个过程,⾸先调⽤TObject的NewInstance分配内存以及调⽤InitInstance设定类⽀持的接⼝以及初始化⼯作,最后TObject会调⽤CreateClass为对象进⾏执⾏框架的设定。
类型转换(Type Casting)实际上就是让编译器产⽣调整执⾏框架的⼯作,让对象变量能够正确地存取到其执⾏框架之中的特性、⽅法和事件等。
2-4-3对象信息服务
MethodName似乎只会对从TComponent继承下来的类的published⽅法才有作⽤
类⽅法(Class Method),亦称静态⽅法(Static Method),类范围内的⽅法,可直接使⽤类名称来调⽤,⽽⽆须经由类对象调⽤
对象⽅法(Object Method),类⼀般定义的⽅法,必须经由此类的对象来调⽤
虚拟⽅法(Virtual Method),提供派⽣类(derived class)改写(override)⽗类定义的⽅法的机制
重载⽅法(Overload Method),定义同名,但是具有不同参数原型的⽅法,主要是使⽤了让程序员定义多个构造函数,或是接受不同参数,但是拥有相同名称的函数。⼀旦使⽤了重载⽅法之后,那么编译器便会以对象变量声明的类型作为绑定的依据,⽽不是执⾏时期以对象变量的实际内容为准则。
动态⽅法(Dynamic Method),类似于虚拟⽅法,但是可以⼤幅减少VMT的⼤⼩,不过效率稍为⽐虚拟⽅法缓慢⼀点
事件处理函数(Event Handler),组件阶层VCL对象的特别⽅法,可结合图形⽤户界⾯提供反应外界触发事件的能⼒
2-6 VCL对象的释放服务
基本上Object Pascal对于对象的分配机制是使⽤堆分配(Heap Allocation),那么当然就有释放对象服务了。⽽不像C/C++⼀样可以同时使⽤堆分配以及栈分配(Stack Allocation)。
var aObj: TBase; 只是定义了TBase类型的⼀个对象指针(对象引⽤),并没有实际地分配TBase或是TBase的派⽣类的物理内存。程序员必须调⽤对象创建服务相关⽅法才会让对象在内存中实际成形。
如果TObject的析构函数Destroy不声明称虚拟⽅法的话,那么会发⽣下⾯的设计问题:
派⽣类将⽆法分配⾃⼰的资源
TObject可能⽆法正确释放由TObject分配的资源
pascal语言面向对象吗Method 'Destroy' hides virtual method of base type 'TObject' —— 这代表程序仍然可以通过编译并且能够执⾏。但是Delphi的编译器做了最后⼀层的保护,因为Delphi编译器仍然会产⽣调⽤TObject.Destroy的机器码⽽不会调⽤派⽣类的析构函数,因此造成了TObject可以正确释放分配的资源,⽽派⽣类则没有正确释放的情况。
TObject释放资源分为两个步骤,第⼀个步骤是释放Delphi类分配的特别数据类型变量的空间,第⼆个步骤则是释放Delphi类占据的内存空间。
对于派⽣类所分配的资源,程序员只需要遵守:
1,改写Destroy虚拟析构函数
2,在改写的Destroy虚拟析构函数中先释放派⽣类分配的资源
3,最后使⽤inherited关键字调⽤⽗类的虚拟析构函数
2-7 类和对象的Metadata-VMT(Virtual Method Table)
Metadata是描述其它数据信息的数据。例如描述⼀个数据表(Table)中的字段名称、字段格式和字段⼤⼩等信息的数据就是数据表的Metadata,通常也称为Database Schema。⽽在⾯向对象程序语⾔中描述类和对象的Metadata就储存在称为VMT的数据结构中。
由于类⽅法是由所有的类对象共享,⽽字段变量却是每⼀个类对象各⾃拥有,因此把所有类的定义产⽣在⼀起是不恰当的,更何况操作系统可能也不允许把数据和程序代码编译在相同的地址区域中。
把编译器产⽣的内容分门别类,把每⼀个对象各⾃拥有的资源产⽣在对象的内存之中,并且把类共享的资源产⽣在另外⼀块独⽴的内存之中,最后再于对象内存中插⼊指向共享资源的指针,那么各⾃独⽴的对象就能够通过这个内嵌指针存取到共享资源,例如类字段或者类⽅法
编译器可以设计成把⼀般程序/函数产⽣在特定的内存地址(根据程序/函数在类中声明的顺序),同时为了能够处理⾯向对象中继承和多态的要求,编译器可以为每⼀个定义的类创建⼀个拥有特定数据结构的表格,这个表格就称为VMT。在这个VMT表格中将产⽣指到类中每⼀个虚拟⽅法的指针,通过这个指针可以调⽤虚拟⽅法。此外在这个表格中也将会产⽣⼀个指向另外⼀个产⽣的动态⽅法表格的指针,通过这个动态⽅法表格指针可以存取到所有类中定义的动态⽅法,这个储存着共享资源的数据结构VMT也就是Object Pascal中的Self指向的⽬的地。当然为解决继承的问题,在这个VMT表格中也将产⽣指向这个类的⽗类的VMT表格,以便允许编译器实现出多态的功能,最后编译器会在每⼀个对象
独⽴的内存中插⼊⼀个指到VMT表格的指针,以便让每⼀个对象实体能够经由这个指针存取到类定义的虚拟⽅法,动态⽅法以及存取到⽗类的VMT表格。
第三章⾯向对象程序语⾔和Framework
3-1 ⾯向对象程序语⾔和VCL Framework
⾯向对象的三项核⼼技术:继承(Inheritance)、封装(Encapsulation)和多态(Polymorphism)。
3-2 Framework使⽤⾯向对象程序语⾔的设计⼿法
抽象法,使⽤抽象类来定义⽗代服务类,然后再开发派⽣类来改写(override)⽗代抽象类以提供实现服务。抽象类有⼀些问题,第⼀是Object Pascal允许程序员创建抽象类对象,这会导致执⾏期错误,虽然创建抽象类对象在语义上有问题,但是在语法上却是合法的,为了避免产⽣问题,VCL Framework并不喜欢抽象类。第⼆是Object Pascal可以使⽤接⼝来取代抽象类,⽽且使⽤接⼝设计⽐较符合现代⾯向对象趋势,抽象类已逐渐被接⼝设计取代。第三,如果是混合抽象类以及⼀些实现⽅法,VCL Framework通常都倾向使⽤Palce Holder⽅法。Place Holder,是指⽗类的⼀些虚拟⽅法被实现为空⽩的函数⽽不声明为抽象⽅法。
逐渐增加法,是指⽗类提供了基础的实现,再交由派⽣类提供更多的实现。
三明治⼿法,是指派⽣类在改写⽗类的⽅法时,会在使⽤关键字inherited调⽤类的实现之前先加⼊⼀些派⽣类的程序代码,再调⽤类的实现⽅法,最后则再加⼊派⽣类的实现。
覆写类实现法,在⾯向对象的多态应⽤中也有⼀种情形是特定的派⽣类完全不使⽤⽗类提供的实现,因此决定完全覆写⽗类的实现⽽不是改写⽗类的实现,这种设计⼿法便称为覆写⽗类实现法。
BootStrap设计法,是指⽗类会定义各种服务⽅法,但是这些服务都需要特定的标地,例如Window Handle或是Window的Device Context Handle。⽗类在实现服务⽅法时都会使⽤特定的标地,但是这个特定的标地却只由派⽣类来提供,并不由⽗类提供。这种让特定的标地延迟到派⽣类才提供的设计便称为BootStrap设计法,这也就是说使⽤这种设计的类并不能且不应该创建⽗类对象,⽽只能创建派⽣类对象来执⾏。
3-3-1 VCL Framework的核⼼组件构架
TComponent类必须提供下⾯的基础服务以便让派⽣类能够不再重复撰写这些服务程序代码:
作为基础的根组件类
可同时扮演Container组件和单⼀组件的功能
基础组件管理功能
基础组件互动通知功能(Notification)
同时提供可视化和⾮可视化组件架构基础
3-4 这还不够,让它成为Windows控件吧
如果在TComponent类之下先设计⼀个控制类,这个控制类具备基本的控制服务,例如处理⿏标的服务,负责处理控制事件的服务以及处理光标(Cursor)服务等。让这个控制类成为其它具体控件类的⽗类,那么封装Windows控件的类或是其它VCL Framework⾃定义的控件类就可以从这个控制类继承下来并且⾃动拥有处理⿏标,光标和事件的基本功能,如此⼀来我们便可以让TComponent和封装Windows控件的派⽣类经由控制类分离,解决了紧耦合(Tight Coupling)的问题,也让Framework的控件类不成为Windows平台专属的类⽽提供能够封装其它控件的可能性。
3-4-1 TControl
TControl类提供的基础服务
控件基本信息
处理⿏标基本服务
控件使⽤的资源
分派消息基本服务
绘制控件区域基本服务
和⽗类对象互动的服务
3-4-2 封装Windows控件的TWinControl类
Double Buffer是计算机图形处理常⽤的技术之⼀,在DOS时代便有许多游戏软件使⽤Double Buffer来增加游戏反应速度。所谓Double Buffer是指当游戏或是图形软件需要显⽰下⼀个画⾯时,软件先在内存中分配⼀块⼤⼩和需要变动画⾯相同的区域,先在此区域中画完要显⽰的内容,然后再⼀次切换画完的内容到画⾯中。如此⼀来画⾯显⽰的速度可⼤幅增加,减少画⾯因为重画⽽造成闪烁的情形,这种使⽤另外⼀块内存预先绘制下⼀画⾯再使⽤类似Memory Move的汇编语⾔切换的技术便成为Double Buffer。
从TComponent、TControl和TWinControl这三个类的⽣命和实现看到了类从通⽤形式逐渐具体化⽽成为封装Windows控件的类,VCL Framework⼯程师经由在每⼀个派⽣类中加⼊具体的功能⽽形成封装特定功能的类。TComponent提供基础组件服务,TControl加⼊处理⿏标事件等功能⽽形成“准组件”类,
再到TWinControl直接为封装Windows控件⽽加⼊的属性和⽅法。从这三个类我们可以学习到如何在我们的应⽤系统中建构类架构,那就是从通⽤类,到准功能类,最后再开花结构形成提供特定服务的末端类。
第四章 VCL FrameWork和窗⼝消息
4-1 窗⼝消息和VCL FrameWork
封装窗⼝消息核⼼⾯对的问题:传统窗⼝是以窗⼝数据结构、回调函数、WinAPI等为基础的C/C++语⾔机制作为处理的⽬标,然⽽Object Pascal却是⼀个⾯向对象的程序语⾔,Object Pascal的数据类型和C/C++也不尽相同,更何况VCL FrameWork准备以组件架构来封装窗⼝消息以及消息的处理、分派的流程。
4-2 VCL的窗⼝消息封装机制
仔细观察传统Windows程序,我们可以知道Windows应⽤程序向Windows操作系统注册的回调函数正是消息分派枢纽,当Windows操作系统中有事件发⽣时,Windows操作系统经由调⽤Windows应⽤程序注册的回调函数⽽获得处理此事件的机会。因此VCL FrameWork想结合⾯向对象实现封装窗⼝消息的功能,回调函数正是最好介⼊的地⽅。
其实要让Windows操作系统调⽤到VCL Framework提供的封装组件⽽不是⼀般的回调函数并不困难。整个运算逻辑可以使⽤如下的想法来
表达:
到消息分派的⽬的VCL组件
经由虚拟⽅法机制调⽤⽬的VCL组件处理窗⼝消息的⽅法
VCL Framework就是使⽤这些想法来完成分派窗⼝消息的机制,当VCL Framework调⽤Windows API的RegisterClass注册窗⼝类时会使⽤VCL Framework内部提供的回调函数。在这个回调函数中会建⽴⼀个“调⽤通道(Tunnel)”,让Windows操作系统能够顺利地调⽤到VCL组件提供的窗⼝处理⽅法
4-2-1 从窗⼝回调函数到⾯向对象的类⽅法
在传统的Windows程序设计中,Windows操作系统是调⽤⼀般的回调函数的,⽽所谓⼀般的回调函数就是指C语⾔的函数类型。但是在⾯向对象程序语⾔中当程序代码调⽤对象的⽅法时,除了⽬标⽅法接受的参数之外,调⽤者(Caller)还需要传递⼀个额外的隐藏参数,那就是Object Pascal语⾔的Self或是C++语⾔中的this,也正是因为这个原因,在对象⽅法中才能够使⽤Self来进⾏存取对象本⾝的服务,因为VCL Framework要解决的问题就是如何从Windows操作系统调⽤到对象的⽅法。也就是说如
何把Windows操作系统要调⽤的⼀般的C函数类型转换成可调⽤的VCL Framework中⾯向对象的⽅法?
这很简单,我们可以先撰写⼀个使⽤Object Pascal语法但是符合C函数类型的窗⼝回调函数让Windows操作系统调⽤,然后在这正常的回调函数中先到⽬的VCL对象,再主动把Self推⼊栈中,再推⼊对象⽅法的参数,最后再调⽤对象⽅法即可让调⽤回调函数改变成调⽤对象⽅法,这不就可以完美地解决传统Windows回调函数进⼊⾯向对象程序语⾔世界问题了吗?不过在Delphi应⽤程序中,Borland为了提供VCL Framework分派窗⼝消息的执⾏效率,并不是把Self推⼊栈,⽽是把Self推⼊EAX缓存器(Register)中,以便使⽤Register的调⽤惯例以最快的执⾏速度来分派窗⼝消息。
4-3-4 TObject分派消息的原理和流程
在TObject的消息分派服务中,虚拟⽅法Dispatch是真正负责到特定VCL组件的事件处理函数的核⼼⽅法。 VCL窗⼝消息封装机制把窗⼝消息分派到VCL组件的消息处理函数WndProc之后,真正让窗⼝消息和VCL组件的事件处理函数串联起来的枢纽就是TObject的消息分派服务虚拟⽅法Dispatch。Dispatch虚拟⽅法会根据触发的消息种类来决定如何分派这个消息。
Dispatch虚拟⽅法使⽤两种⽅式来分派消息,第⼀种是分派能够被VCL组件事件处理函数处理的消息,对于这种消息Dispatch会在⽬标VCL 组件的动态⽅法数据表中搜寻拥有相同消息ID的动态⽅法,
到之后就直接调⽤到的VCL事件处理函数来处理触发的消息。第⼆种是如果Dispatch虚拟⽅法对于触发的消息不需要分派,那么Dispatch虚拟⽅法就会调⽤同属TObject消息分派服务之中的DefaultHandler虚拟⽅法来处理,⽽VCL Framework已经提供了默认的DefaultHandler实现⽅法,程序员可以直接使⽤,当然也可以在VCL组件中重载DefaultHandler 虚拟⽅法由程序员⾃⼰实现处理Dispatch不分派的消息。另外⼀个执⾏DefaultHandler虚拟⽅法的情形是当Dispatch⽆法在VCL组件的动态⽅法表格中到能够处理消息的事件处理函数,那么Dispatch也会调⽤DefaultHandler虚拟⽅法来处理这类尚未经处理的消息。
4-3-5 VCL消息分派架构
基本上VCL Framework中封装窗⼝消息的机制是经由InitWndProc取代TForm注册的窗⼝回调函数,因此Windows操作系统在产⽣了窗⼝消息之后实际上会调⽤InitWndProc函数,经由InitWndProc进⾏⼀些转换后再把控制权交给StdWndProc函数。
StdWndProc接着会根据处理此消息的VCL组件,调⽤VCL组件重载的窗⼝消息处理函数。当执⾏权分派到了VCL组件的窗⼝消息处理函数之后,VCL Framework便会经由⾯向对象的集成和虚拟⽅法机制让消息处理权流动到TObject的Dispatch虚拟⽅法。最后TObject.Dispatch 会正确地把执⾏权交给VCL组件的事件处理函数来处理窗⼝消息。
4-4 Delphi窗⼝应⽤程控者:TApplication
每⼀个⽤Delphi开发的Windows应⽤程序都是由TApplication开始形成⽣命的
4-4-1 TApplication对象的创建
在Controls程序单元被加载时它的initialization程序区块会被⾃动执⾏,⽽在initialization程序区块我们可以看到它调⽤了InitControls函
数,TApplication对象在此函数中被创建
4-4-2 TApplication和秘密窗⼝
TApplication是从TComponent继承下来的,TApplication并不是VCL Framework中代表窗⼝的控件,⽽是VCL Framework中的⼀般的组件类,因此就类的定义上来说TApplication和窗⼝没有直接的关系,但是我们再仔细观察TApplication类却可以发现TApplication定义了⼀个字段变量FHandle,FHandle的类型是HWnd,⽽HWnd类型正是VCL Framework中代表窗⼝Handle值的类型。TApplication类会在内部创建⼀个秘密窗⼝。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论