以Delphi Package架构多人开发应用程序环境
■印象中的Package
在一般的AP开发时,我们知道在Delphi7.0整合环境中将Project->Options->选到Packages卷标页,Builder with runtime packages选项打勾,就会让编译出来的执行文件Size变小很多(以空白的Form1为例,编译出来的Size由367kb变成20kb),因为它把一些VCL共享模块的Loding放到*.bpl中;换句话说这个变小的EXE文件在执行时是需要那些*.bpl的,而原本较大的执行文件执行时则不需要那些*.bpl,这样看来其实只是换汤不换药罢了。
这种编译架构的差异有什么优点?试想,如果Project1.exe&Project2.exe都不使用Builder with runtime packages选项,Project1.exe跟Project2.exe编译出来的Size都是367kb,若有10个ProjectN.exe,则占用的Size将是全部exe加起来的总和,若是我们将其中共享的部分(如VCL共享模块)独立出来,每个EXE都将只有20K(以测试用的空白Form而言),缺点则只有每个exe执行时都要在系统搜寻路径中找得到那些VCL的共享模块(以*.bpl文件名存在);所以这种架构适用于一个项目系统内有多个独立执行文件或独立业务模块存在的情况,多个执行文件就可以分散给多个人去开发,也可依照应用程序不同的业务性质去区隔并分别设计。
将执行文件分散开来开发,这种做法跟一个主执行文件配合多个不同业务别的DLL开发没有什么差别,而降低每支执行文件的Size也只是Package最普通的应用;使用Package有一个更强大的优点——可以共享变量,如果将数据模块当成一个共享变量的观念去看待,我们不用在每次启动或关闭不同的子系统业务模块时,重新连接或释放数据库的Connection。不过,要真正能共享数据库模块是需要每个业务模块皆以*.bpl的方式存在,这也是实际应用上的状况。
项目架构种类 |
Package编译方式 |
说明 |
类型一 Project0.exe(MIS主系统) Project1.exe(会计子系统) Project2.exe(人事子系统) Project3.exe(库存子系统) |
所有EXE文件编译选项 □Builder with runtime packages |
1.每个子系统都是完整独立的EXE文件;每个EXE文件至少都有数百KB以上 |
类型二 Project0.exe(MIS主系统) Project1.exe(会计子系统) Project2.exe(人事子系统) Project3.exe(库存子系统) |
VCL共享模块(*.bpl) 所有EXE文件编译选项 ■Builder with runtime packages(打勾) |
1.每个子系统虽然都是EXE文件,但执行时需要VCL的共享模块存在;此种方式,每个EXE文件SIZE都只有几十KB 2.此种架构只有节省档案SIZE的优点 |
类型三 Project0.exe(MIS主系统) Project1.bpl(会计子系统) Project2.bpl(人事子系统) Project3.bpl(库存子系统) VCL共享模块(*.bpl) DataMoudle(db.bpl) |
所有EXE文件编译选项 ■Builder with runtime packages(打勾) |
1.将各子系统中共享的程序(如连接数据库的模块)独立出来共享 2.除主系统外其余子系统皆为BPL型式存在 3.节省档案SIZE外,并有共 用数据库连结模块的优点 |
注:
1.MIS主系统(Project0.exe)可能只是一个选单(Menu),用来当作启动各子系统的Shell
2.VCL共享模块(*.bpl)是Delphi在安装时即安装至C:\WINNT\SYSTEM32\中的那些*.bpl档案,不管你EXE文件是否Builderwithruntimepackages,那些*.bpl早已存在,端看你要不要用它而已(Builderwithruntimepackages是否打勾)
3.当AP开发是以Builderwithruntimepackages(打勾)编译时,RELEASE到客户端同时也要将C:\WINNT\SYSTEM32\中的那些*.bpl档案COPY到客户端,因为客户端没有安装Delphi,所以没有那些VCL共享模块(*.bpl)
4.BPL跟EXE一样是可以“LOAD”其它的*.bpl;如Project1.bpl(会计子系统)会”LOAD”VCL共享模块(*.bpl)以及DataMoudle(db.bpl)
5.在类型三中,作为唯一的EXE文件,Project0.exe编译时一定要Builderwithruntimepackages(打勾),否则在动态加载其它子系统模块时,会出现找不到类别的错误
■Package架构的优缺点
使用Package的优点:
1.类似DLL-应用程序可以被高度的模块化
2.优于DLL之处-可以共享变量
使用Package的缺点:
1.架构较复杂,需要花比较多的心思在模块化的设计
2.需要程度较高的开发人员,若对package的使用不熟,容易出trouble
■哪些东西可以开发成为Package
前面讲过,Package其实就是类似DLL的一种架构,但是是专门属于Borland C++Builder/Delphi使用的一种DLL架构,举凡有含Form的Unit文件、无Form的Unit文件(如自己写的函数库)、组件等等,都可以编译成为Package
■Package档案的类型
|
Package项目 |
相当于一般项目 |
相当于DLL项目 | |
编译前 |
*.DPK |
Package项目文件 |
DPR文件 |
DPR文件 |
*.PAS |
PackageSource |
PAS文件(Source) |
PAS文件(Source) | |
编译后(产出) |
*.DCP |
Package产出文件 |
|
LIB文件 |
*.BPL |
Package产出文件 |
EXE文件 |
DLL文件 |
Package中的DCP文件,跟DLL项目的LIB文件类似,DCP是提供为静态连结编译之用(注意:只是静态连结的“编译过程”用之),BPL则是直接提供为执行环境之用;DCP只有在提供给别的EXE或BPL静态连结的“编译过程”中会用到,所以不用RELEASE到客户端;在执行环境中,Package不管是被静态联结或是动态连结使用,一律以BPL型式存在。
例如Project0.exe以“静态连结”方式使用到Package1,Project0.exe在编译时需要Package1.dcp的档案存在,但编译后分发到客户端的档案则为roject0.exe以及Package1.bpl(Package1.dcp不用release),虽然它们之间是使用静态连结(加载)…
如果Project0.exe以“动态连结”方式使用到Package1,Project0在编译过程中根本不需要任何Package1的档案,而分发到客户端一样仍然只有roject0.exe以及Package1.bpl。
■Package的载入(从EXE载入BPL的角度来看)
静态加载–不管用得用不到,该*.dcp是一定都要加载的,也就是在exe项目选项中的Builder with runtime packages->Add加入,如Project0.exe(MIS主系统)一定要加载VCL共享模块(*.dcp),否则无法产生GUI接口;所以就用静态加载的方式加载它;静态加载的*.dcp,是在编译时就已决定的
动态加载–使用到时,才透过LoadPackage()这个API来呼叫,如Project0.exe(MIS主系统)呼叫各个子系统的*.bpl,是透过Coding的方式去加载子系统的*.bpl,Project0.exe(MIS主系统)并不需要在编译时将所有子系统加载编译
■Package的载入(从BPL载入BPL的角度来看)
静态加载–放在Package项目之Requires区段中的*.dcp文件,反正是“一定要用到”的,不需要利用动态加载的就放在这儿
动态加载–跟EXE动态加载BPL方式相同,使用到时,才透过Coding方式利用LoadPackage()这个API来加载的
■Package项目的建立
在Delphi7.0整合环境中,主菜单->File->Close ALL关掉所有的Form及项目;然后再一次:主菜单->File->New->Other...->选择Package,建立一个全新的Package项目
一个全新的Package应该是长的如下图所示,分为两个部分:Contains内放的是项目的主题,也就是我们要为这个Package开发的程序代码;Require内放的是要“静态连结” 的其它Package(*.dcp文件),也就是不需要利用LoadPackage()这个API来加载(动态加载)的Package就放到Require中;rtl.dcp是系统内建就已Require的Package,应该是VCL之类的东西吧
接下来我们要开始设计项目内容,帮这个Package项目加入一个空白的Form,到Delphi主菜单->File->New->Form;我们再回到Project Manager看看在Contains区段是否多了Unit1(Form1);前面说过,Contains内放的是项目的主题,也就是我们要为这个Package开发的程序代码,接下来要在Form1中放什么组件,写什么程序代码,都跟一般的Form没什么两样,只是若这个Form是写在一般的项目中(Project1.dpr)它可以被编译成EXE文件,而在Package项目中(Package1.dpk),它只能被编译成Package1.dcp(给别的项目静态连结编译时用)和Package1.bpl
Package1.dcp以及Package1.bpl这两个编译后的结果,Default是放到Delphi7.0目录的.\Projects\Bpl\中;若要改变,请至Delphi主菜单->Project->Options(如下图),修改项目的输出目录路径为目前路径(.\)
还有,很重要的一点,在Package项目中的Form由于将来会被别人加载使用(不管被静态或被动态加载);它都需要先向系统注册它的类别;所以在前面的例子中,将Unit1(Form1)开发好后,最后要在end.之前插入注册(RegisterClass)/注销(UnRegisterClass)的程序代码,注册内容就是自己的Form类别(如TForm1、TForm2等等)
(其它程序代码) ...... ...... initialization RegisterClass(TForm1); finalization UnRegisterClass(TForm1); end. |
■Package的应用限制
如果只是一个EXE文件附带一个BPL文件,这种架构还算单纯,但如果如文章开始所述:一个MIS主系统(Project0.exe)带着多个子系统(*.bpl),那会有什么限制发生呢?
1.各个Package(*.bpl)在开发过程中,彼此的Contains区段中不能有同名的Unit
2.共享的unit一定要放在package,也就是要把共享模块变成Package
我们现在来想想,如果是我们来主导这个系统,我们会如何设计呢?
1.虽然各项子系统是各自独立开发,甚至是交由不同的开发TEAM来完成,但为了接口的风格一致及操作统一(如Button的大小及位置),我们会有一个共通的BaseForm的雏形,让所有的子系统的主Form都由这个BaseForm继承而来,这样会让子系统(Package)的Contains区段都会有一个共同uses的BaseForm.pas
2.为了程序代码的一致性,也为了增加Coding速度,公司累积了程序代码经验,可能会有一个公用副函数集MySub供各个子系统呼叫,这样也会让子系统(Package)的Contains区段都会有一个共同uses的MySub.pas
为了不让BaseForm.pas及MySub.pas成为Package开发的限制瓶颈,所以我们要将BaseForm及MySub也变成Package(成为BaseForm.dcp及MySub.dcp),然后让各个子系统Package放在Requires中静态连结编译。
■Package的动态加载
前面已介绍了Package在静态加载以及编译的方法,现在要介绍Package应用的重头戏-动态加载;一般来说,共通的副函数或共享分享的数据库模块以及共享的继承样板会先被制作成Package,然后被主程序(EXE文件)及各个子系统模块(BPL文件)作为静态连结之用;而整个系统开发的主角,也就是各个子系统模块会被主程序或子系统模块(BPL文件)间,当成动态加载的目标。
var ModuleInstance1:HMODULE; {$R*.dfm} //--------------------------------------------------------------- //动态加载Package //--------------------------------------------------------------- procedureTForm0.Button1Click(Sender:TObject); begin ModuleInstance1:=LoadPackage('Package1.bpl'); end; //--------------------------------------------------------------- //将Package中的Form1带出 //--------------------------------------------------------------- procedureTForm0.Button2Click(Sender:TObject); var frm : TcustomForm; begin frm :=CreateFormByClassName('TForm1'); try frm.ShowModal; finally frm.Release; end; end; //--------------------------------------------------------------- //释放Package //--------------------------------------------------------------- procedureTForm0.Button3Click(Sender:TObject); begin UnloadAddInPackage(ModuleInstance1); end; |
注:
1.在上列的程序代码中,加载Package的LoadPackage()是系统内建函数
2.带出Package中的Form资源或是释放Package的函数由于要多做一些处理,所以把它包在CreateFormByClassName()以及UnloadAddInPackage()两个自订函数中
//--------------------------------------------------------------- //自订函数–CreateFormByClassName(),建立Form //--------------------------------------------------------------- Function TForm0.CreateFormByClassName(const ClassName:string) : TCustomForm; var AClass:TPersistentClass; begin AClass:=GetClass(ClassName); If AClass=nil then exit; Result:=TComponentClass(AClass).Create(Application) as TCustomForm; //或Result:=TCustomForm(TComponentClass(AClass).Create(Application)); end; //--------------------------------------------------------------- //自订函数–CreateDataModuleByClassName(),建立数据模块 //--------------------------------------------------------------- Function TForm0.CreateDataModuleByClassName(const ClassName: string):TDataModule; var AClass:TPersistentClass; begin Result:=nil; AClass:=GetClass(ClassName); If AClass=nil then exit; Result:=TComponentClass(AClass).Create(Application) as TDataModule; end; //--------------------------------------------------------------- //自订函数–UnloadAddInPackage(),释放Package //--------------------------------------------------------------- Procedure TForm0.UnloadAddInPackage(ModuleInstance:HMODULE); var i:Integer; M:TMemoryBasicInformation; begin for i:=Application.ComponentCount-1 downto 0 do begin VirtualQuery(GetClass(Application.Components[i].ClassName),M,SizeOf(M)); if (ModuleInstance=0) or (HMODULE(M.AllocationBase)=ModuleInstance) then Application.Components[i].Free; end; //下面这两个函数应该是只要取其中一个呼叫即可 UnRegisterModuleClasses(ModuleInstance);//直接注销Package UnloadPackage(ModuleInstance);//间接注销,呼叫Package中的finalization区段 end; |
■完整的Package项目架构范例
子系统名称 |
内部主要对象 |
补充说明 |
Project0.exe(MIS主系统) |
Form0(Unit0.pas) |
整个项目中唯一的EXE文件Project的Option选项要设 ■Builderwithruntimepackages并加入BaseForm,MySub两个DCP (静态连结) |
Package1.bpl(会计子系统) |
Form1(Unit1.pas) |
Requires区段要加入 BaseForm,MySub两个DCP (静态连结) |
Package2.bpl(人事子系统) |
Form2(Unit2.pas) |
Requires区段要加入 BaseForm,MySub两个DCP (静态连结) |
Package3.bpl(库存子系统) |
Form3(Unit3.pas) |
Requires区段要加入 BaseForm,MySub两个DCP (静态连结) |
MySub.bpl共享函数库 |
无,只有函数程序代码 MySub2003.pas |
并提供MySub.dcp供其它系统静态连结之用 |
BaseForm.bpl共享继承Form雏形 |
FormBase (UnitFormBase.pas) |
并提供BaseForm.dcp供其它系统静态连结之用 |
注:在此仍不厌其烦的提醒:每个Package的Source最后都要在end.之前插入注册(RegisterClass)/注销(UnRegisterClass)自己类别的程序代码
□Project0.exe(MIS主系統)
注:不管这个唯一的EXE主程序是否需要加入自订的共享模块(如本例的BaseForm,MySub两个DCP),没有的话,它仍然要将Builder with runtime packages打勾(就算只有使用那些系统内定的VCL、DCP),否则这个EXE文件执行后会无法动态加载其它的子系统模块,会出现找不到类别的错误,这个问题我花了一整个下午才找到问题原来在这里;但我不知原因是否为Delphi Bug;因为在我的认知,若不使用自订的共享模块(有的话也是放到静态链接中),主程式跟其它的子系统间则只有动态加载的关系,那静态链接则只有决定是否要将系统的VCL静态链接进来的问题;Builder with runtime packages打不打勾,单纯也只决定跟系统VCL间的关系,为何会影响其它子系统的动态加载??我不愿就看图说故事或凑答案的方式来牵强解释这个问题原因….只有推论为Delphi bug
□Package1.bpl(会计子系统)
□Package2.bpl(人事子系統)
□Package3.bpl(库存子系统)
(略,以上类推)
注:
1.在Requires区段(静态连结)中,除了手动加入的BaseForm,MySub两个DCP外,其它DCP是由系统自动加入的,不用理会它
2.Package的Contains区段中,彼此间不能有相同名称的Unit,也就是说Package2中的Unit不能跟Package1中一样也叫Unit1(要改名如Unit2或其它)
□MySub.bpl共享函数库(无Form,只有函数库的程序代码MySub2003.pas)
□BaseForm.bpl共享继承Form雏形
■Package项目的测试环境
Package在测试及除错上由于BPL不能直接执行验证结果,所以在开发中的测试是个问题;不过前面说过,除了项目文件架构不同外,Source开发式和一般Project项目是没什么两样的,以前面介绍过的Package1.bpl(会计子系统)为例,我们来看一下如何开发及测试
1.建立一标准可编译成EXE的Project1.dpr项目,加入Unit1(Form1),将项目写好后直接编译为EXE文件测试
2.当测试好后没问题,新开启一Package项目叫做Package1.dpk,将Contains区段纳入刚才开发的Project1.dpr项目中的Unit1(Form1);此时重新编译,会计子系统就变成一个BPL而不是EXE文件了
3.以后要修改会计子系统,就利用Project1.dpr项目来编译成EXE测试,再用Package1.dpk项目来编译成BPL
■Package项目网状加载问题
在先前介绍的系统中,各个子系统理应由Project0.exe(MIS主系统)这个唯一的EXE文件来切换进入或退出各子系统的时机;但是我们在实际的应用中,也可能发生直接由Package1.bpl(会计子系统)加载(跳到)Package3.bpl(库存子系统)的情况(记得BPL也是可以加载BPL的吧?),整个系统RUN过一段时间后,谁加载谁,或谁已被退出,已经不是很清楚了,若此时突然有个快捷方式直接回主程序中并结束程序,我们要能保证所有曾经被加载的BPL都要被全部完全释放干净;如果动态加载其它BPL的LoadPackage()函数是分散在各子系统中(人人都可以加载其它的BPL),我们如何去统一管理那些PackageHandle变量呢(HMODULE)?所以理想的方法是将加载BPL的动作统一放在共同函数库(如MySub中),主程序或各个子系统要加载或释放BPL都透过在共通函数库中包装好的Package载入或释放函数,该函数内部有List数组来记录管理所有的PackageHandle的消长情况;参考资料“DelphiPackage学习笔记”及“DelphiPackage无痛使用”中有个写好的共享函数库,叫做PkgUtils.pas,您可以把它里面的函数合并到您公司开发的共享函数库中(如MySub中),所有主程序或各个子系统的Package加载/释放动作都透过这个公用函数库包装的函数去执行动作。