从普通函数到对象方法
------Windows窗口过程的面向对象封装
开始,由VirtualAlloc想起
我在查看VirtualAlloc这个API的时候,思绪竟然跳到另一个地方去了。那是以前阅读VCL源码时遗留下来的问题,Classes单元的MakeObjectInstance函数调用了VirtualAlloc,我甚是不解,为什么Delphi提供了那么多内存分配函数,而MakeObjectInstance偏偏要用系统提供的API,更令我不解的是,之后再也不见有VirtualFree的调用,也就是说,VCL其实存在内存泄漏?这个问题我在网上也看到相关的讨论,有人认为这的确是VCL的Bug,有人甚至修改了Classes单元,在单元的结束节处调用VirtualFree以释放以前分配的内存。
不过我对这个问题始终持保留态度,MakeObjectInstance是一个非常重要的函数,担负着窗口过程到对象方法的转换,Borland没有理由留着这个“Bug”不理。
于是我重新阅读了MakeObjectInstance这个倍受李维赞誉的函数,我想我这次是读懂了,为什么不调用VirtualFree,因为没有必要,进程在结束的时候会毫无保留的回收所有的内存,而经由VirtualAlloc分配到的内存就保留在由TInstanceBlock记录所组成的链表中,这个链表组成的内存并不是使用一次即弃掉的,它是可重用的,调用一次FreeObjectInstance,那张链表便空余出TObjectInstance大小的内存,以供下一次使用。所以,这其实是一个内存池,提供了更上一层的内存分配机制。而在结束的时候调用VirtualFree就显得没有任何必要了。
回到上面提出的第一个问题,为什么要调用VirtualAlloc,而不用Delphi提供的内存分配函数,如果没有看到System单元的这两个变量,我想永远也不可能找到答案:
var
AllocMemCount: Integer; { Number of allocated memory blocks }
AllocMemSize: Integer; { Total size of allocated memory blocks }
这两个变量的确是记录内存使用的总量,前提是你调用Delphi提供的内存管理函数,如果调用Windows原生的API,则VCL是没有办法感应到的。写到这里,再看看上面的描述,也许一切都了然了。
然而,这只是我写这篇文章的导火线,真正原因是我读懂了MakeObjectInstance,以前的许多疑惑已经拨云见日,窗口过程到对象方法的脉络在我的脑中从未有过这么清晰,因此欲罢不能,作此文记之。
使用,将窗口过程转成对象方法的步骤
从SDK的角度来讲,设置窗口过程有两种方法(我所能想到的),一是调用RegisterClass,另一个是调用SetWindowLong,第一种用在创建窗口的时候,另一种用在改变窗口过程的时候。在Delphi中,假设你写了一个自定义窗口类,那么你可以重载WndProc,这个方法就相当于窗口过程。可以确定,VCL在开始时肯定也是用上面所说的方法,设置窗口过程,只是后来经过一些转换,最终使窗口过程调用到对象实例的WndProc,所以WndProc可以当成窗口过程来使用。
这个转换的步骤从表面上看很简单,现在我们不必去深究其原理,只要知道通过下面的做法,就可以将一个窗口过程转成对象的方法。
首先,到Controls单元的TWinControl类,这是所有窗口的父类,转换过程就在这里面完成。TWinControl的构造函数中写了这一句:
constructor TWinControl.Create(AOwner: TComponent);
begin
... ...
FObjectInstance := Classes.MakeObjectInstance(MainWndProc);
... ...
end;
其中的MainWndProc就是代替窗口过程的对象方法。
接着,在InitWndProc有如下代码:
function InitWndProc(HWindow: HWnd; Message, WParam,
LParam: Longint): Longint;
Begin
... ...
SetWindowLong(HWindow, GWL_WNDPROC,
Longint(CreationControl.FObjectInstance));
... ...
end;
InitWndProc就是刚开始的窗口过程,而调用了SetWindowLong之后,窗口过程就转成了FobjectInstance了。而实际上最终得到调用是却是MainWndProc。
最后,在TWinControl的析构函数中还写了如下语句:
destructor TWinControl.Destroy;
begin
... ...
if FObjectInstance <> nil then
Classes.FreeObjectInstance(FObjectInstance);
... ...
end;
这是为了回收由MakeObjectInstance使用的内存,让这块内存可在下一次重用。
上面就是TWinControl的窗口过程到对象方法的转换步骤,这的确是很神奇的事情,它们在某些情况下是很有用的,比如TComboBox,在这个控件里面有一个用于编辑的Edit和一个用于下拉选择的ListBox,这两个控件是在ComboBox创建的时候一起创建的,VCL没有办法对它们进行封装,但有时候需要处理他们的消息,这时,上面的方法就派上用场了,事实上TComboBox就是运用上面的方法,将Edit和ListBox的窗口过程转换成TcomboBox内部的方法的,有兴趣者请查阅一下VCL。
对上面进行一次总结:
1、 假设你通过原生的API创建了一个窗口,如果你想让这个窗口的窗口过程被指定为一个类的方法,那么可以在类的内部调用MakeInstanceObject,传进类的一个方法(如上面的MainWndProc,当然这个方法必须是TwndMethod类型的),并保留函数返回的指针。
2、 调用SetWindowLong,用类保留的指针替换原来的窗口过程。到这里,窗口过程就被传进MakeInstanceObject的对象方法所代替了。
3、 在消毁这个类的实例时,别忘了调用FreeObjectInstance,并传回保留的指针。如果这时窗口还未消毁,还得用SetWindowLong恢复原来的窗口过程。
知道如何使用并不是我们的最终目的,我们要更进一步,为什么会是这样,请看下一节。
实现,窗口过程到对象方法的转换技术
窗口过程实际上是一个回调函数,向API传递函数的地址,Windows保留着这个函数地址,在适当的时候调用这个函数。那么对象方法与普通函数有什么不同呢,对于同一种调用规则来说,不同之处就是对象方法在第一个参数之前有一个隐藏的参数,这个参数就是对象的实例(如果是C++应该叫实例指针,而Delphi的对象实例就是一个指针,只已经为大多数人所共知的事实)。
另一方面,Windows的API使用的是Stdcall的调用规则,从机器指令的角度看,就是在Call某个函数之前,先将函数的参数从右向左地压栈。而Delphi为了提高效率,默认使用了Register调用规则,粗略的讲就是从左向右传递参数,且前三个参数分别放在EAX,EDX,ECX寄存器中,其后则依次入栈。若要知道详细的规则,请查看Delphi的帮助主题:Calling conventions。
现在,如果我们想让窗口过程流入某个对象的方法,要解决两个问题:
1、 在进入对象方法的入口时,先将对象实例作为第一个参数传入,其次再将窗口过程的参数依次传入。对于Register调用规则来说,就是将对象实例赋值给EAX,再将其他参数按照规则赋给相应的寄存器或者压栈。
2、 Stdcall规则到Register规则的转换,这个不是必须的,因为Delphi也支持StdCall规则,但对Register规则来说效率更高,另一方面Delphi对Register规则作了更多的支持,比如Published的属性就只能指定Register规则的方法。
现在让我们围线着这两个问题开始探索VCL是如何做的。
VCL在开始的时候同样要遵守Win32的做法,首先填充一个窗口类结构然后注册窗口类,注意TWinControl.CreateWnd中的这一句:
WindowClass.lpfnWndProc := @InitWndProc;
它将窗口过程指定为InitWndProc函数。
接下来就创建窗口类,在TWinControl.CreateWindowHandle中:
FHandle := CreateWindowEx(ExStyle, WinClassName, Caption, Style,
X, Y, Width, Height, WndParent, 0, WindowClass.hInstance, Param);
现在来看,一切都似乎正常,但其实在调用CreateWindowEx的时候,事情正在稍稍发生变化。CreateWindowEx的时候系统将发送(请注意是发送而不是投递)一个WM_CREATE消息给窗口,处理这个消息的是谁呢,正是上面看到的InitWndProc。
有必要看一下这个函数的代码,我顺便作了详细的注释:
01 function InitWndProc(HWindow: HWnd; Message, WParam,
02 LParam: Longint): Longint;
03 Begin
04 //CreationControl就是窗口类,TWinControl在CreateWnd的时候将Self赋给它
05 //由此可以看到VCL的窗口类是非线程安全的。
06 CreationControl.FHandle := HWindow;
07 //重设窗口过程,从此之后,这个函数再也不会得到调用了
08 SetWindowLong(HWindow, GWL_WNDPROC,
09 Longint(CreationControl.FObjectInstance));
10 if (GetWindowLong(HWindow, GWL_STYLE) and WS_CHILD <> 0) and
11 (GetWindowLong(HWindow, GWL_ID) = 0) then
12 SetWindowLong(HWindow, GWL_ID, HWindow);
13 //设置该窗的一些属性,与我们讨论的无关,可不去理会它们
14 SetProp(HWindow, MakeIntAtom(ControlAtom), THandle(CreationControl));
15 SetProp(HWindow, MakeIntAtom(WindowAtom), THandle(CreationControl));
16 //主动调用一次FobjectInstance
17 asm
18 PUSH LParam
19 PUSH WParam
20 PUSH Message
21 PUSH HWindow
22 MOV EAX,CreationControl
23 MOV CreationControl,0
24 CALL [EAX].TWinControl.FObjectInstance