用bpl!
参考下面李维的文章:
(本文章转自《Delphi未经证实的葵花宝典2.7》,在搜索栏搜索“动态包应用”即可)
动态包应用(图都丢了,完整的文章见原文)
作者:李维
相信许多人和我一样,在使用Delphi开发应用系统的时候,一定会想到如何的切割整个应用系统。是把所有的子系统撰写成一个很大(可能会有数M.Bytes的大小)的EXE檔呢?还是应该把每一个模组撰写在不同的EXE檔案之中的好。
事实上这两种方法都有它们自己的问题。如果是把所有的模组撰写在一个EXE檔案之中的话,那么不但执行檔太大,不易更新和维护。在开发时也不甚方便,因为要让数个人撰写同一支应用程式的确比较麻烦。那么如果我们把每一个模组让不同的程式师撰写成独立的EXE檔案,再由一个主程式分别启动不同的EXE檔不就好了吗?没错这是许多人使用的方法(包括我在内),但是这样切割应用系统有一个问题,那就是如果每一个独立的EXE模组,都需要使用资料库的话,那么当主程式启动个别的EXE檔案时,每一个EXE都必须重新再连结到资料库,开启资料库表格,再取得它需要的资料。这个过程通常都需要不少的时间。例如连结到Oracle并且开启一个资料库表格的话,通常需要五到十秒。如果EXE开启的资料库表格或是查询比较多的话,那么主程式在启动独立的EXE檔案时,通常需要30几秒到一分钟不等。这对于许多的使用者而言是非常不方便的。这样的状状甚至会造成你的专案无法交货(例如使用者要求在五秒之内EXE程式的画面必须出现)。除此之外,每一个独立的EXE又使用了额外的连结以便存取资料库,造成了资源的浪费。面对这种二难的局面,你现在的选择是什么呢?
事实上这个问题在我的心中也盘旋了许久。因为这一是我想要解决的问题,只是由于工作的繁忙让我一直无法花时间解决它。最近在手上的事情告一段落之后,又接到许多朋友的询问,所以决定花一些时间试着解决这个重要的问题。
增加应用程式载入的效率
如果我们仔细的思考这个问题的话,就可以发现问题出在每一支独立的EXE都需要重复的连结资料库所至。所以如果我们可以让连结到资料库的次数减少的话,不就可以加快应用程式载入的效率了吗?
这个想法当然很简单,但是问题是要如何的减少应用程式连结资料库的次数呢?事实上这个想法也很简单,最好是让应用系统连结资料库的次数变成一次,如此一来除了主程式需要连结资料库之外,其他的应用模组都能够公同使用由主程式载入的资料模组的话,那么一切问题不都解决了吗?请注意,在这里我所说的其他模组代表独立的EXE或是其他形式的应用程式,而不是指在单一一个EXE之中不同的表格或是子系统。
我们可以使用图一来表示这个想法。在这个构想中,我希望由应用主程式先负责载入公用的资料模组。在这个资料模组之中有其他子系统需要使用各个资料库表格,如此一来当主程式启动其他的子系统时,就不需要再让每一个子系统再连结,开启资料库表格了。
当这样还有一些设计上的问题,我们稍后再回来讨论这个问题,现在先我们看看如何的把这个构想实做出来,并且测试一下实际的结果是不是真的比较有效率。
要实做这个构想,我们必须想办法让公用的资料模组能够让每一个子系统存取到,并且不再需要每一个子系统都分别的和资料库建立一个连结的Session。我的第一个想法是使用DLL来解决这个问题。但是事实上使用DLL无法解决这个问题。在我花费了许多的时间之后,使用DLL仍然有会有AccessViolation的错误。我也曾在网路上搜寻相关的问题或是资料,我在宝兰的DiscussForum中也看到有人提出类似的问题,但是似乎都没有人确实的回答这个问题。我也想过干脆拿出我的Soft-Ice和Bounds-Checker看看为什么使用DLLAssembly打交道,这实在不是件好玩的事情。正打算放弃之时,突然想到Delphi3.0之中的Package不正是解决这个问题的好方法吗?于是我就决定试试看,果然一击中地,顺利的解决了这个问题。当然,要知道为什么使用Package可以解决这个问题,你需要知道Package和DLL的异同。
DLL和Package
为什么在我一开始使用DLL时无法解决多个模组共用一个资料模组DLL的问题呢?这主要是因为在Win95/NT中当每一个模组载入DLL时,对于每一个DLL之中的全域变数而言,每一个模组都会有一份独立的变数。这是什么意思呢?我们可以使用图二来说明。
图 二 Win 95/NT 中 全 域 变 数 和 模 组 的 关 系
当图二中的模组一和模组二分别的载入它们共用的DLL时,虽然在这个共用的DLL中有一个全域变数gAccount。但是模组一和模型二会分别的拥有一个gAccount变数。这也就是说模组一对于gAccount变数数值所做的修改并不会影响模组二中的gAccount变数数值。在Wn95/NT中的DLL行为是和Win3.x不同的,因为在Win3.x中所有的模组都使是共用一份DLL中的全域变数。
由于Win95/NT中DLL全域变数的这种特性,所以当你想把资料模组撰写在DLL之中让多个模组共同使用时,问题就来了。因为在这种情形下,每一个模组都会有一份它自己的资料模组。所以当每一个应用程式模组载入资料模组的DLL时,它仍然会连结资料库一次,所以你并无法减少连结资料库的次数。
那么使用Package有什么不同吗?在回答这个问题之前,请你先回想一下。在Delphi3.x中它允许你使用Package的功能来编译你的应用程式,如图三所示。
图 三 Delphi 3.x 允 许 你 使 用Package 的 功 能 编 译 应 用 程序
使用Package编译应用程式的好处除了可以减少应用程式的大小之外,事实上Package还有一个很重要的特性,那就是Package允许多个模组共用所有的全域变数。
我们可以使用图四来说明Package的特性。请你想一想,当你的应用程式使用Package的功能时,事实上它必须载入许多不同的Packages。不管你的应用程式是否使用了全域变数Application,许多的Packages都会使用Application这个全域变数。由于全域变数Application是存在于VCL.DPL之中,所以如果Application会对于每一个载入它的模组都产生一份独立的全域变数的话,那么整个应用程式便会产生不正确的结果。 所以由这个说明我们可以知道,在图四中的Application和Screen等全域变数对于所有使用它的模组而言一定是只有一份全域变数。
图 四 Package 中 全 域 变 数 的 特 性
事实上在Forms.PAS之中的程式码也透露着这些蛛丝马迹。例如下面便是Forms.PAS宣告Application和Screen这二个变数的程式码。从它们的注释中我们可以清楚的看到,它们是全域物件,即使是编译成Package时也是一样。
{ Global objects }
var
Application: TApplication
Screen: TScreen
Ctl3DBtnWndProc: Pointer = nil;
由于Package能够自动的将其中的全域变数编译成所有使用它的模组都共用一份的特性。所以我们就可以使用这个特性重新的建构图一的架构成为图五的形式。
图 五 改 良 过 的 公 用 资 料 模 组 示 意 图
在图五中我们可以把所有模组需要共同使用的资料模组撰写在一个Package之中,然后再把每一个子系统撰写成独立的Package。只有主程式是EXE,它负责载入共用的资料模组,以及在使用者启动每一个子系统时再载入相对应的子系统Package。
使用这种架构有许多的好处。第一个便是共用的资料模组只需要载入一次即可。而这个好处便是我们前面讨论需要解决的问题。如此一来在每一个子系统载入时便可以加快其执行的速度。第二个好处是在开发这整个系统时,我们仍然可以让不同的程式师负责发展不同的子系统,这样可以就可以解决前面讨论的分工的问题。此外如果一个子系统很庞大的话,你也可以再次的切割这个子系统成为更多的小系统。例如你可以再把图五中的会计子系统Package再分为一般会计,成本会计,和管理会计等不同的Package。第三个好处是使用Package就像是使用DLL一样,主程式可以在需要的时候才载入一个Package,在使用完毕之后可以立刻的释放这个Package,这样可以让你对于系统资源有更好的控制能力。最后一个好处是使用Package可以让你发展出Plug-and-Play的模组,当然这需要藉由结合虚拟介面或是虚拟类别的功能。藉由使用Package和虚拟介面的能力,你可以任意的替换应用系统之中的模组而不会影响系统的执行。这个功能对于许多使用Delphi开发套装软体的程式师来说是一个非常重要的功能。
现在你应该对于DLL和Package的差异有了基本的瞭解,现在是让我们看看如何使用Package的特性解决我们面对的问题的时候了。下一小节就让我们实际的撰写一个范例来证明Package对于全域变数的处理方式以及使用Package的确能够加快应用程式的载入速度。
实际的范例
由于平日我大部份的时间都是使用Oracle,MSSQLServer和InterBase(许多的读者都询问我为什么不使用Sybase或是Informix做为范例说明,这实在是因为我比较少使用它们,并没有其他的意思,所以请使用Sybase,Informix和DB2的读者见谅。不过我相信我们讨论的东西都可以使用在这些资料库之上)。在这三个资料库中,Oracle的连结速度一直都是令我非常头大的,因为在这三者之中,Orcale连结资料库和开启资料库表格的时间最久。所以本节的范例就以Oracle资料库为范例,看看使用了Package之后会不会有任何明显的改善。
首先请你先建立一个Package,并且在这个Package之中产生一个资料模组,并且使用Database和Query元件连结到Oracle的资料库如图六所示。
圖 六 存 在 於 Package 之 中 的 資 料
模 組 使 用 Database 連 結到 Oracle
在成功的編譯了這個Package之後,再讓我們設計範例程式的主表格如圖七一樣。
图 七 使 用 公 用 资 料 模 组 的 主 表 格
在主表格中有一个DataSource元件。这个DataSource组件会在资料模组的Package载入之后,连结到资料模组之中的Query元件以便显示Oracle资料库之中的资料。现在剩下的工作便是撰写程式码载入资料模组Package和显示资料库的资料。
首先在主表格启动时,它必须在FormActivate事件处理函数中载入资料模组Package。它呼叫LoaddbPackage这个程序。
procedure TMainForm.FormActivate(Sender: TObject);
begin
LoaddbPackage;
end;
LoaddbPackage是真正负责载入Package的程序。它在一个tryexcept程式区块中呼叫Delphi的LoadPackge函数载入指定名称的Package。这个函数在成功执行后会回传一个Package的handle值。我们的程式必须储存这个handle值以便稍后使用它,并且在最后藉由这个handle值释放载入的Package。如果呼叫LoadPackage成功的话,程式就呼叫LoadDataModule从Package中取得前面介绍的资料模组,否则在except程式区块中ShowMessage会显示发生错误的原因。
procedure TMainForm.LoaddbPackage;
begin //我们必须加载数据库Package以便连结到数据库
try
aDBConnect := LoadPackage(DBPackages);
LoadDataModule;
except on E : Exception do begin
MessageBeep(Word(-1));
ShowMessage(E.Message);
Application.Terminate;
end;
end;
end;
在LoadDataModule中我们必须先从资料模组Package之中取得资料模组的真正Meta-Class名称,然后使用这个Meta-Class建立真正的资料模组物件。所以LoadDataModule一开始会呼叫GetClass向Windows取得特定类别名称的Meta-Class。而GetClass传入的参数『TConcreteDataModule』,便是前面我们建立的资料模组的真正的类别名称。
由于一个独立的EXE要能够取得Package之中的Meta-Class必须使用指定的类别名称。所以当你在Package中撰写任何的类别时,你必须确定这个类别名称在所有Delphi或是应用程式载入的Package中都是唯一的。否则如果有在所有载入的Pakcage中有相同的类别名称时,Delphi或是应用程式会产生一个例外。
在成功的从Package取得了Meta-Class之后,你就必须使用这个Meta-Class产生真正的资料模组物件。请注意在下面的程式码中,我们使用了强制型态转换把TComponentClass的物件转换为TDataModule的物件。
在建立了TDataModule物件之后,我们就可以一一的搜寻资料模组之中的元件并且找到我们需要的Query元件,并且把主表格中DataSource的DataSet特性值设定为找到的Query元件。
procedure TMainForm.LoadDataModule;
var
iCounter : Integer;
begin
{Note that TApplication "owns" this form and thus it must be freed priorto unloading the package }
dataModuleClass := GetClass('TConcreteDataModule');
if dataModuleClass <> nil then
begin
admGlobal :=TDataModule(TComponentClass(dataModuleClass).Create(Application));
for iCounter := 0 to admGlobal.ComponentCount - 1 do begin
if UpperCase(admGlobal.Components[iCounter].ClassName) = 'TQUERY' then begin
aQuery := TQuery(admGlobal.Components[iCounter]);
DataSource1.DataSet := aQuery;
break;
end;
end;
end;
end;
由于在上面的程式码中我们使用了GetClass以取得特定名称的Meta-Class,所以你的资料模组Package必须在它的initialization程式区块中先注册它自己。下面的程式码便是资料模组Package注册的程式码。
initialization
RegisterClass(TConcreteDataModule);
现在就可以执行范例程式了,在主程式执行之后,你可以看到类似图八的画面。从主表格中我们可以证明的确可以藉由资料模组Package存取到Oracle的资料。
图八主表格执行的画面,它果然可以藉由资料模组Package存取资料
到这里为止我们只是证明了使用资料模组Package可以让应用程式正确的执行,但是主程序启动的时间和一般独立的EXE没有什么不同。但是接下来的情形就比较有趣了。因为我们要藉由接下来的程式码来证明使用Package可以大幅加快子系统的载入速度。
在主表格中有一个按钮『载入第二个UIPackage』。这个按钮的功能就是模拟载入子系统的功能。当使用者点选这个按钮之后,范例程式会载入另外一个只有使用者介面的Package,而且这个Package必须再使用刚才载入的资料模组Package来显示资料。藉由这个模拟的子系统功能,我们要证明资料模组Package可以在不同的模组中共用而且可以加快模组启动的时间。
这个按钮的OnClick事件处理函数和主表格的FormActvate事件处理函数非常的类似,它也是呼叫LoadUIPackage程序以便载入一个使用者介面Package。
procedure TMainForm.Button1Click(Sender: TObject);
begin
LoadUIPackage;
end;
LoadUIPackage和LoaddbPackage几乎一模一样,它也是呼叫Delphi的LoadPackage和GetClass载入Package,取得使用者介面表格的Meta-Class,建立使用者介面表格对象,搜寻资料模组中的Query元件,然后显示资料库的资料。
procedure TMainForm.LoadUIPackage;
begin //我们必须加载使用者接口Package以便连结到数据库
try
UIConnect := LoadPackage(UIPackages);
LoadUIModule;
excepton E : Exception do
begin
MessageBeep(Word(-1));
ShowMessage(E.Message);
Application.Terminate;
end;
end;
end;
procedure TMainForm.LoadUIModule;
var iCounter : Integer;
aDS : TDataSource;
begin
{Note that TApplication "owns" this form and thus it must be freed priorto unloading the package }
pkgModuleClass := GetClass('TUIPackageForm');
if pkgModuleClass <> nil then begin
aPkgForm := TCustomForm(TComponentClass(pkgModuleClass).Create (Application));
for iCounter := 0 to aPkgForm.ComponentCount - 1 do begin
if UpperCase(aPkgForm.Components[iCounter].ClassName) = 'TDATASOURCE' then
begin
aDS := TDataSource(aPkgForm.Components[iCounter]);
aDS.DataSet := aQuery;
break;
end;
end;
aPkgForm.Visible := True;
end;
end;
当我们完成载入使用者介面Package的功能,再执行范例程式并且按下『载入第二个UIPackage』按钮之后,可以看到类似图九的画面。
图 九 第 二 个 使 用 者 介 面 Package 启 动 的 画 面
令人惊讶的是,使用者介面Package之中的表格会立刻的显示出来,几乎完全不需要等待。表格一是我使用这个范例程式和使用二个独立的EXE应用程式比较的结果。从表格中你可以看到使用Package载入子系统比使用EXE载入子系统整整快了十倍。
使用独立的EXE应用程式 使用EXE加Package的功能
------------------------------------------------------------------
主程式启动时间 20 20
其余模组启动时间 20 2
------------------------------------------------------------------
从上面的结果我们就可以知道使用Package的好处,它不但可以让我们共用资源,也能够改善子系统启动的时间。
当然应用程式在使用完毕之后,它必须释放动态载入的Package以便释放系统资源。在主表格的FormClose事件处理函数中它呼叫了UnLoadAddInPackage程序并且传递Package的handle值做为参数以便释放Package。
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
UnLoadAddInPackage(dbConnect);
UnLoadAddInPackage(UIConnect);
end;
UnLoadAddInPackage先从全域物件Application中找到载入的资料模组和使用者介面表格,先释放这些动态载入,建立的物件,切断它们和Application和关系,再利用Package的handle值解除Package对Windows的注册,最后再呼叫UnLoadPackage释放载入到记忆体之中的Package。
procedure UnLoadAddInPackage(Module: THandle);
var i: Integer;
M: TMemoryBasicInformation;
begin
for i := Application.ComponentCount - 1 downto 0 do begin
VirtualQuery(GetClass(Application.Components
.ClassName), M, SizeOf(M));
if (Module = 0) or (HMODULE(M.AllocationBase) = Module) then
begin
ShowMessage(Application.Components.ClassName);
Application.Components.Free;
end;
end;
UnRegisterModuleClasses(Module);
UnLoadPackage(Module);
end;
当你结束主程式时,你应该会看到类似图十一和图十二的画面。这些画面证明了当主程式结束时,它能够正确的释放所有载入的Package以释放系统资源。
上面的范例程式证明了使用Package的确能够公用Package以及Package之中的全域资料,此外也能够帮助子系统加快载入和启动的时间。但是主程式在启动时仍然需要载入资料模组Package,连结资料库。但是对于许多的使用者而言,他们仍然会希望让主程式也能够很快的出现。要达成这个目标,你还需要藉助Delphi执行绪的能力。
加入多执行绪载入的能力
在上一节中你看到了使用Package的确可以大幅加快子系统载入的时间。但是我们在载入主程式时仍然需要20到30秒的时间,因为主程式还是需要载入共用的资料模组Package。但是对于许多案子来说,使用者经常会要求程式必须在10秒或是5秒之内第一个画面就必须出现。那么对于这种要求我们可不可以达到呢?
如果你想到Delphi中的多工执行绪功能Tthread物件的话,那么答案就出现了。我们只需要在主程序第一个表格出现之时,启动一个多执行绪物件让它负责载入公用的资料模组不就可以了吗?如此一来主表格可以立刻的出现在萤幕之上,而让执行绪在背景继续的载入资料模组。
为了要加入多执行绪的能力,你必须修改上一节的范例。在范例程式的FormActivate事件处理函数我们不再直接呼叫LoaddbPackage,而是建立一个Tthread物件,然后由这个Tthread物件真正的载入资料模组Package。
procedure TMainForm.FormActivate(Sender: TObject);
begin
DBThread := TOracleThread.Create(True);
aDBThread.OnTerminate := dbThreadTerminated;
aDBThread.Resume;
end;
在TOracleThread的Execute虚拟方法中,它呼叫了LoaddbPackage载入资料模组。在这里有一点非常重要的地方便是当你在执行绪中载入Package时,所有的handle值和物件参考值必须储存在主执行绪的变数之中。否则在这个执行绪执行完毕之后,这些handle值和物件参考值都会成为无效的数值。所以在下面的程式码中你可以看到执行绪在呼叫LoadPackage载入Package时会把LoadPackage回传的数值储存在主表格的变数之中。
procedure TOracleThread.Execute;
begin
{ Place thread code here }
LoaddbPackage;
end;
procedure TOracleThread.LoaddbPackage;
begin
//我们必须加载数据库Package以便连结到数据库
try
MainForm.aDBConnect := LoadPackage(DBPackages);
LoadDataModule;
except
on E : Exception do
begin
MessageBeep(Word(-1));
ShowMessage(E.Message);
Application.Terminate;
end;
end;
end;
procedure TOracleThread.LoadDataModule;
var
iCounter : Integer;
begin
{ Note that TApplication "owns" this form and thus it must be freed prior
to unloading the package }
dataModuleClass := GetClass('TConcreteDataModule');
if dataModuleClass <> nil then
begin
MainForm. admGlobal:= TDataModule(TComponentClass(dataModuleClass).Create(Application));
end;
end;
当我们修改完范例程式之后,就可以试着再次的执行它,看看主程式的载入时间是不是有任何的改善。下面的表格和图形显示出当我们使用了执行绪的功能之后,第一支主程式启动的时间果然大幅减少为3秒钟。比起原来的20秒果然改善了许多,真是令人印象深刻。
------------------------------------------------------------------
使用独立的EXE应用程式 使用EXE加Package的功能
------------------------------------------------------------------
主程式启动时间 20 3
其余模组启动时间 20 2
------------------------------------------------------------------
现在使用了执行绪功能之后,不但每一个子系统启动的时间加快了许多,主程式更是可以在瞬间出现,这样的执行速度应该是可以让大部份的人满意了吧。
应用程式,企业法则(企业物件)的切割
从上面的范例中我们可以知道,善用Delphi的Package和执行绪的功能可以让我们大幅的改善应用程式载入和执行的效率。但是上面的范例只是假设在很简单的状态之下。在许多实际的案子中,我们可能无法把所有的资料集元件放在一个单一的资料模组之中。在这种情形下,你可以把所有的资料集元件分别撰写在不同的资料模组Package之中,并且在每一个子系统需要特定的资料模组Package时再载入它们。
当我们从这个角度观察应用系统时,可以发现如何的切割资料集到不同的资料模组中似乎是一件非常重要的事情。因为这不但牵涉到应用系统执行的效率,更和系统资源的善用有很大的关系。事实上当我们开发N-Tier的应用系统时,你也会发现如何切割应用程式和企业物件对整个应用系统的架构有深远的影响。
所以应用程式和企业物件的切割似乎在未来新一代应用系统的开发中占有重要的地位。当然要能够适当,有效率的切割企业物件需要SA在分析系统时好好的做分析的工作,更需要SD能够通盘的设计整个应用系统运作的架构。
如果我们结合Delphi强大的N-Tier,Package以及执行绪功能的话,就可以使用如下的图形来表示。
图 十 四 N-Tier , Package 和 执 行 绪 结 合 使 用 的 架 构
请注意在图十四中说明了并不是只有资料模组可以存在于Package之中,我们也可以把应用逻辑或是企业物件封装在Package之中或是ActiveX之中,于应用程式需要时再载入执行它们。最后由于应用程式伺服器在大多数的情形下是执行在WindowsNTServer之中,所以我们可以更有效率的使用作业系统的执行绪能力来载入应用程式需要的Package。
更具威力的功能
就像我在前面说明的,我无法在一篇文章中为各位介绍所有有关Package的使用方法和各种功能。使用Package更高阶和更具威力的功能应该是和虚拟类别结合一起使用。此外Delphi3也提供了许多的程序和函数能够让你检查每一个Package使用了什么其他的Package,执行时期函式馆和全域函式。
结合Package和虚拟类别不但能够让你发展出Plug-and-Play的模组功能,更能够让你完全的控制Package中所有的物件和变数。更能够让你发展出一些自动化的工具来帮助你开发你的应用系统。我计划在以后的文章中再继续和各位讨论这些高等的主题。
结论
在本篇文章中我们看到了如何的使用Delphi3.0中的Package功能来解决应用载入时连结资料库效率的问题。许多人对于Package的认识只是它可以减少应用程式的檔案大小,除此之外似乎就没有什么其他的瞭解了。但是这篇文章就告诉你如何的发掘Package更有威力的一个应用。使用Packages可以立刻的降低模组启动需要的时间。
但是我们在这篇文章中就讨论了所有有关Package的功能来吗?那当然不,还需要许多更有威力的功能我并未讨论。例如你如果能够更进一步的结合Package,参考计数值以及抽象虚拟类别(AbstractVirtualClass)或是抽象介面的话,那么你就可以完全的控制Package中内含的类别以及表格,而不需要一定是Delphi内定的类别和表格。这样一来你可以设计一个Plug-and-Play的Package介面,以便让主程式能够载入任意的Package,存取它功能。此外你也可以取得Package中许多重要的资讯,例如这个Package输出了那些的方法,使用了那些其他的Package?Package和initialize以及finalization的关系等,也许有机会再让我们讨论这些更有意思的主题。
事实上从这篇文章中就可以出Delphi的多么的好用。如果今天你是使用VisualBasic或是PowerBuilder的话,那么抱歉,你绝对无法解决这个问题,也许再等到下一个VisualBasic或是PowerBuilder版本的话,就有『可能』解决这个问题吧。但是Delphi就不同了,它除了可以让你撰写一般的应用系统,但是在遇到特殊的情形时,Delphi也可以在短暂的时间内超越工具目前功能的限制,而解决这些问题,谁是比较好的主从架构开发工具就不言自明了。
-------------------------------------
Delphi中Package是一件非常强大的工具,用的好可以起到非常清晰的
划分程序结构的作用。因为他内建描述信息,可以和当前代码无缝集成起来,
可以保护包括类在内的任何元素,相当于VC中的MFC Extension DLL的作用。
但是一直以来的文章都只介绍静态连接的方法,这其实限制了Package的使用
因为静态连接的Package失去了其灵活性,可配置性等等。至于通过函数入口
方式访问,实在是大材小用,那不如直接用DLL还方便一些。
如何动态载入Package,使用其中的类、函数、变量等等?起始说穿了很
简单,就是做一个代理包。因为在一个Delphi程序中,每个unit只能存在
一份,否则发生冲突。要动态载入包,又得取得其中信息,又不能直接uses
包含信息的unit(否则引起冲突),解决办法是另外建一个代理包来作为桥梁
传递信息。下面是一个简单的例子,主程序使用到两个包,DemoPak包中有
一个简单的Form;RegPak是所谓的代理包,起到注册信息的作用。
主程序对RegPak静态使用(在Project Options里面设置了),对DemoPak
动态载入(通过LoadPackage),而DemoPak依赖于RegPak(requires),
并在初始化时向代理包RegPak注册自己的可用类,这里举例注册类信息,
你可以方便的改成注册其他信息
unit FormReg;
interface
uses
Classes, SysUtils, Forms, Contnrs;
type
TFormClass = class of TForm;
procedure RegisterFormClass(const AName: string; const AFormClass: TFormClass);
procedure UnregisterFormClass(const AName: string);
function FindFormClass(const AName: string): TFormClass;
implementation
var
g_lstNames: TStringList;
g_lstForms: TClassList;
procedure RegisterFormClass(const AName: string; const AFormClass: TFormClass);
begin
g_lstNames.Add(AName);
g_lstForms.Add(AFormClass);
end;
procedure UnregisterFormClass(const AName: string);
var
Index: Integer;
begin
Index := g_lstNames.IndexOf(AName);
if Index <> -1 then
begin
g_lstNames.Delete(Index);
g_lstForms.Delete(Index);
end;
end;
function FindFormClass(const AName: string): TFormClass;
var
I: Integer;
begin
for I := 0 to g_lstNames.Count - 1 do
begin
if g_lstNames = AName then
begin
Result := TFormClass(g_lstForms.Items);
Exit;
end;
end;
Result := nil;
end;
initialization
g_lstNames := TStringList.Create;
g_lstForms := TClassList.Create;
finalization
FreeAndNil(g_lstForms);
FreeAndNil(g_lstNames);
end.
以上是RegPak的主要代码,因为举例,代码很简陋。主要思想就是保存注册信息,
提供查询方法。让我们看看在DemoPak中的使用
unit AboutForm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TfrmAbout = class(TForm)
lblAbout: TLabel;
private
public
end;
var
frmAbout: TfrmAbout;
implementation
uses FormReg;
{$R *.dfm}
initialization
RegisterFormClass(TfrmAbout.ClassName, TfrmAbout);
finalization
UnregisterFormClass(TfrmAbout.ClassName);
end.
在初始化时向RegPak的FormReg单元提交自己的类信息,因为每个Package在载入时
无论动态静态都会自动初始化,而RegPak被主程序静态引用,肯定已经初始化,所以直接
注册即可,非常简单。最后看看主程序中的使用
uses
FormReg;
procedure TfrmMain.btnAboutClick(Sender: TObject);
var
hModule: THandle;
begin
hModule := LoadPackage('DemoPak.bpl');
try
with FindFormClass('TfrmAbout').Create(nil) do
try
ShowModal;
finally
Free;
end;
finally
UnloadPackage(hModule);
end;
end;
动态载入需要的包,查询需要的类的信息,使用之,最后卸载包。
很简单吧 起始很多东西都没有什么技术难度的,关键看你想不想得到
不知道各位用Delphi写数据库程序的朋友有没有碰到过这样的问题:写出来的
程序体积太庞大!我写的上一个项目中可执行文件竟然达到4.3M!很可怕的体积!
这样不仅分发程序比较困难,而且维护也很难:程序给客户后发现界面上某个标签
的字写错了,然后不得不把这样一个巨无霸重新编译,重新发给客户。
想到分割程序,用什么呢?COM/MTS?在小项目中太得不偿失了!用Dll?如果
是做非数据库程序还可以,如果做数据库程序就有麻烦了:每个Dll都会在自己独立
的对话中和数据库连接,造成资源的极大浪费,而且还有全局变量的问题。在当我
快绝望的时候看到了李维的一篇有关package的文章(关心package的朋友应该都看
过那篇文章),但那篇文章里写的不是很清楚,看了还有些不明白。大富翁上也有
很多朋友讨论,但都比较零碎,上一段时间结合网上查到的文章,还有自己一些摸索,
终于基本上搞清楚了package的一些用法,现在贴出来和大家交流。
package的使用和dll类似,有静态和动态调用两种方法。
我们用一个简单的数据库程序来说明,假设工程组成为:
ClassMgr.dpr--------------------工程文件
uGlobal.pas---------------------全局变量单元
frmDM.dfm(uDM.pas)--------------数据模块
frmMain.dfm(uMain.pas)----------主窗体
frmStudent.dfm(uStudent.pas)----学生档案窗体
frmScroe.dfm(uScore.pas)--------成绩输入窗体
frmQuery.dfm(uQuery.pas)--------成绩查询窗体
这里面的一些uses关系就不说了,大家应该都很清楚吧!
1、静态方法
现在的目标是把每个功能窗体放进一个包中,以后当需要修改相应的模块时只要发
布相应的包即可。为了达到这个目的,我们需要添加四个包:
basic.dpk
Contains: uDM.pas; uGlobal.pas
student.dpk
Contains: uStudent.pas
scroe.dpk
Contains: uScord.pas
query.dpk
Contains: uQuery.pas
因为所有的数据窗体都需要引用数据模块(uDM.pas)和全局变量(uGlobal.pas)单元,
所以我们把uGlobal.pas和uDM.pas放进basic.dpk中,至于为什么这么做我们等一下
再说。
在ClassMgr.dpr的Options中修改属性,使它Build with runtime packages,
package的列表为:
basic.bpl
student.bpl
scroe.bpl
query.bpl
另外,在student.dpk, scroe.dpk, query.dpk的Requires中添加basic.bpl;
basic.dpk的Requires中添加
vcl50.bpl
vcldb50.bpl
vclado50.bpl(假设用ADO连接)
接下来就是编译包和可执行文件,这里需要注意编译的顺序,正确的编译顺序应该为:
basic.bpl -> (student.dpk, scrod.dpk, query.dpk) -> Classmgr.dpr
编译完后看一下,classmgr.exe的体积是不是小很多了?差不多只用100k左右,因为
其实里面只包含了主窗体。每个bpl文件的大小也差不多100k左右(具体视代码规模)。
[red]这里需要额外说明的是:[/red]
在主窗体frmMain中也要引用uDM, uGlobal等单元,但是因为已经Build with相应
的包了,所以Delphi只会编译相应的声明,而代码的实现部分在相应的包里。
2、动态方法
我们还是拿上面的例子
在这之前需要将每个窗体单元(主窗体除外)作一些修改:
即在每个窗体单元的Initialization部分注册相应的窗体类。
代码例子如下:
initialization
RegisterClass(TfrmStudent);
finalization
UnRegisterClass(TfrmStudent);
end.
然后在主窗体需要调用该窗体的地方改成下面的方式:
procedure TfrmMain.btStudentClick(Sender: TObject);
var
h: HMODULE;
frmStudent: TForm;
begin
try
h := LoadPackage('student.bpl');
frmStudent := TForm(TComponentClass(FindClass('TfrmStudent')).Create(Application));
frmStudent.ShowModal;
finally
frmStudent.Free;
UnLoadPackage(h);
end;
end;
在这里先载入包student.bpl, 然后取得我们在包里注册的TfrmStudent类,
因为FindClass返回的值是TPersistentClass类型,需要强制转换为TComponentClass类
后创建相应的实例。窗体释放后再释放student.bpl
在这种方式下就不需要在frmMain引用其他的单元了(全局变量单元除外)。需要补充
一点的是,在这种方式下,工程classmgr.dpr也需要Build with runtime package,
否则会在FindClass时无法取得相应的类指针。只是student.bpl等包不需要在package
列表中,只需添加basic.bpl即可。
现在说说为什么要将uDM.pas和uGlobal.pas加入到basic.bpl包中。
大家都知道win32模式下Dll不能直接和主程序共享全局变量,package的高明之处
就在于package和主程序能共享全局变量,所要作的工作只是把相应的全局变量放到某
个包中,然后引用该全局变量的包和主程序只要引用该包即可。因为在这个程序中
uGlobal.pas中存放的是全局变量,主程序和其他包都需要引用这些全局变量,故将它
加入basic.bpl中,uDM中的数据模块也是一个全局窗体变量,也需要加入basic.bpl中。
所以在其他包的Requires中都有basic.bpl
3、总结
================= 静态调用模式 =====================
一些关系:
classmgr.dpr
Build with runtime package: student.bpl, score.bpl, query.bpl, basic.bpl
uMain.pas中:
uses
uGlobal, uStudent, uScore, uQuery;
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^不能删除
student.bpl
requires: basic.bpl
score.bpl
requires: basic.bpl
query.bpl
requires: basic.bpl
basic.bpl
requires: vcl50.bpl, vcldb50.bpl, vclado50.bpl
================= 动态调用模式 =====================
一些关系:
classmgr.dpr
Build with runtime package: basic.bpl
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^一定要
uMain.pas中:
uses
uGlogal, uStudent, uScore, uQuery;
^^^^^^^^^^^^^^^^^^^^^^^^^这几个单元不需要,但uGlobal仍需要
student.bpl
requires: basic.bpl
score.bpl
requires: basic.bpl
query.bpl
requires: basic.bpl
basic.bpl
requires: vcl50.bpl, vcldb50.bpl, vclado50.bpl
================= 两种模式的比较 =====================
理论上讲动态调用方式下会比较节省资源,因为相应的包只在需要的时候才
载入内存,但需要牺牲速度为代价。
但在实际使用中,项目窗体可能很多,包也可能有很多,频繁的载入包可能
会使开发者陷入一个比较混乱的状态,可能有的包载入了而没有释放掉,而且包
在什么时候释放也需要很好的控制,这样就不能达到节省资源的目的,相反却增
加了编程的复杂性和牺牲了速度。
另外,动态调用还有一个很明显的缺点是:调用时无法直接知道所取得的某
个类是否有某个方法或属性。
比如我们上面的例子中的frmStudent窗体有一个公有方法:GetStdInfo;
在动态调用时:
var
frmStudent: TForm;
begin
h := LoadPackage('student.bpl');
frmStudent := TForm(TComponentClass(FindClass('TfrmStudent')).Create(Application));
frmStudent.ShowModal;
这里我们就不能写入frmStudent.GetStdInfo这样的代码,因为事实上frmStudent
只是TForm类的一个实例,没有GetStdInfo这样的方法。在这方面动态package和COM有
点类似,但使用COM时编译器允许使用Variant方式来调用一个未知的方法,但Package
却不行。在静态调用时就不存在这种问题,因为代码的实现方式和不使用包时完全一样。
一点补充说明:不能在包之间交叉包含(Contains)某个单元,比如:在basic.bpl中已经
包含了uDM.pas单元,在student.bpl中就不能再包含uDM.pas这个单元,否则编译是不能
通过。
4、建议
我个人的意见是使用静态调用。但如果项目很大,package也就失去了她优势了,
到那个时候我想COM+/MTS会是一个比较好的选择。
欢迎对包感兴趣的朋友和我交流,email: wjjxm@etang.com; QQ: 3496469