用Delphi制作BPL包

Delphi制作BPL

2007-6-21

背景

GCM3构造时间长的问题由来已久。伴随着时间的流逝,系统功能越来越强大,模块越来越多,目前仅GCM一项,需要编译的客户端dll389个之多。在编译机上,一次完整的编译过程(GCM+GCC_PE)耗时更是长达100分钟。经常出现这样的情况:开发人员和测试人员并没有别的事情,就是在等待编译版本,比如发版前的完整构造,或者每日构造失败需要重新编译,这就浪费了我们很多宝贵的工作时间。

解决方案

GCM3构造过程的环节很多,主要包括源码下载、编译、数据库模板和升级脚本的生成、文件压缩和打包发布等。其中编译是一个重头,占到一半以上的时间。其中每个工程编译时间大约为78秒,这样仅GCM的客户端dll编译就需要4050分钟时间。于是如何减少编译时间成为减少总构造时间的一个解决思路。

由于GCM本身框架结构比较庞大,模块里的类继承层次也比较多,很多重复性的代码都被提取出来放在基类中,但在编译过程中这些基类中的代码将会被不断的重复编译。比如有200个工程中的主窗体继承自基类TListDetailForm,该类定义于单元文件ListDetailFrm.pas中,那么在编译过程中ListDetailFrm.pas将被重复编译200次。同样,由于GCM框架结构中继承关系为:TListDetailForm -> TDataBaseForm -> TBaseForm -> TBlankForm,因此这些基类所在的单元文件都会被重复编译200次。

为了避免这种无谓的重复编译,我们可以把公共代码(包括基类所在单元以及一些公共单元)编译成一个独立的部分,其它的工程都引用它。这样公共部分的代码就只需要编译一次,大大减少了编译的时间。

Delphi提供了带包编译(Build with runtime packages)的机制来避免重复编译相同的源码,方法如下:在编译工程时勾选带包编译方式(位于Project -> Options… -> Packages ->Runtime packages),并把前面编译好的BPL添加到运行时包中。当然,这里的BPL就是包含公共代码的包。

这种做法优点:

1.         提高FinalBuilder编译速度,缩短编译时间。同时也提高了打包的速度;

2.         减小编译出来的文件(如exedll文件)大小,同时减小了安装程序的大小,顺便提高了安装速度;

3.         若基类或公共单元做了修改,某些情况下,可以避免所有模块重编一遍,只需把这个包重新编一下即可。

实际做法

基本上,GCM的基类和公共单元都位于SHARE\Common以及3编码阶段\3.1SOURCE\_Common两个目录下。我们创建了一个运行期包GCMCommonPackage.bpl,把这两个目录下的单元文件(*.pas)基本上都打入了包里。编译其它工程时,都引用这个运行期包。

效果

采用上述编译改造策略后,对GCM项目进行一次全编译(GCM+GCC_PE),编译时间由原来的1小时40分左右减少为1个小时左右,减小了约40%的时间。提取BPL以前,编译每个模块平均需要7——8秒钟。提取以后的时间减少到3——6秒。用ASPack压缩后,每个dll由原来的200——300K减小到了60——70K,平均大小减小了70%左右。另外安装时间也缩短了一些。

目前存在的问题

由于编译和构造本身的复杂性以及GCM各模块之间存在一些比较特殊的千丝万缕的联系,改为带包编译以后还存在。例如客户端dll必须加编译选项“dllmode”,而服务器端COM以及其它exe文件不能加“dllmode”编译选项,这样GCMCommonPackage.bpl就不能满足所有工程。另由于隐式包含,一些非公共的单元也被包含到包里了,可能会带来一定的隐患,参见稍后的详述。


注意事项

1、         BPL包的优先级高于引用单元文件的优先级

例如:GCMCommonPackage.bpl引用了单元文件GCMUtils.pas,工程文件pCL_ZRZCD引用了GCMCommonPackage.bpl,则对GCMUtils.pas的引用,一定是bpl里的。即工程中某单元文件若usesGCMUtils,是从BPL里面取的,即使把GCMUtils.pas添加到工程中,仍然是这样。因此,如果修改了GCMUtils.pas,你会发现根本没有效果,这是因为BPL中的代码没变。这就引出了下面的调试问题。

2、         调试

虽然GCMUtils是从BPL中取的,但是仍然能在GCMUtils中设置断点调试。这就有个问题:如果修改了GCMUtils的代码而BPL没有重编,那断点岂不是无效吗?我试了试,发现断点所停的位置,并不是显示的所在行,而似乎是编译BPL时的那一行。例如编译BPL时,第10行是ShowMessage,现在移到第20行了,则若在第10行设置断点,实际上还是在ShowMessage行,即原先的第10行。这就需要调试者小心一些,避免莫名其妙的干扰。比如明明修改了按钮的Caption,编译工程后却发现修改不起作用,还是显示原来的Caption

3、         搜索路径顺序

DelphiEnvironment Options中的Library path,一般系统级的在前面,自定义的在后面。而若不填写Output directory,默认会把BPL输出到DelphiProjects\Bpl目录下。因此,对于编译带包的工程来说,默认设置就OK了。注意Projects\Bpl的优先级要高于system32目录。我原来把Output directory改为system32目录,发现不管用,编译工程的时候还是会从Projects\Bpl取。对于Finalbuilder,由于搜索路径中没有Projects\Bpl,因此是从5资源\GrandResource D7\bpl取的。

4、         隐式包含

BPL中引用了a单元,而a单元引用了b单元,则编译包时,实际上b也会被包含其中,并给出警告信息:“[Warning] Unit 'uLJJSDialog' implicitly imported into package 'GCMCommonPackage'”。由于代码的组织结构不完善,GCMCommonPackage包本应只包含一些公共单元,但实际由于这个原因,也包含了不少非公共的单元。像uJX_JXLBZDFrmuFB_JSD_DBCLMXCXDlg等,这样是会产生一些隐患的。

5、         注意保持两个目录下包的一致性

BPL重编了,而引用它的工程没有重编,常常会报错说找不到程序入口点,或者说某单元有个不同的版本。需要把工程也重编一下,注意保持systemProjects\BPL两个目录下包的一致性,因为运行带包的工程时,是会先找system32目录的,所以虽然编到Projects\BPL目录下,编译工程OK,但是运行时可能会出错。

6、         never-build package XXX need always-build package YYY

编译GCMCommonPackage时,常见的错误提示。解决办法是把YYY的选项“Rebuild as needed”改为勾选“Explicit rebuild”,然后重编YYY。具体原因我也没搞清楚。

7、         dcp文件的作用

简言之,dcp之对于bpl,类似于dcu之对于exe/dll。需要注意的是,如果编译带包的工程,则不仅需要BPL,同时也需要dcp。不过在运行工程时,只需要BPL即可。

8、         自动修改系统搜索路径

在包中引入单元文件后,Delphi会自动在Library Path中加上这个单元文件的路径,这就是为什么一开始我在本机上编译GCMCommonPackageOK,但是在编译机上却总是找不到文件,搞得我莫名其妙,因为编译机无法自动添加Library Path


遇到的bug及解决方法

下载公司材料报错

材料字典模块,点击“下载公司材料”按钮,弹出二级窗口后,下载或者直接取消,然后无论点什么按钮都报错,如下图所示:

报错后,GCM无法关闭,只能杀进程。而如果不带GCMCommonPackage包,就一切OK

由于对BPL了解不够深入,加上上述一些注意事项没有注意,此外似乎还有一些莫名其妙的原因(说到这里我还真得有点怀疑我的Delphi有点问题,一直报各种莫名其妙错误,无法顺利调试,我下午差点重装Delphi),对这个bug,跟踪了两三天,想了各种方法,一直找不到问题所在,束手无策。后来今天下午不知道怎么,Delphi忽然肯合作了,让我能够顺利设置断点调试,定位到了问题所在:创建二级窗体后,原窗体的GCMInfo.cdsTableInfo变为nil了,所以后面在对cdsTableInfo操作时,会报越界错误。我跟了N次,却找不到它是何时修改的。此时我已头晕眼花,筋疲力竭,于是请来黄山川和老贾帮我调试,老贾不愧牛人,想到一招:设置地址断点,于是乎,顺利找到了修改处(这里不得不再次感谢Delphi的合作),原来这里有一个IFNDEF dllmode,我这才恍然大悟,原来编译GCMCommonPackage的时候,根本没想到设置dllmode编译选项。没想到这里居然会有影响!于是加上了dllmode编译选项,compile……怎么还报错?原来是没覆盖dcp文件,赶紧覆盖一下。再试一次,OK了!明天再测试一下新版本,应该没问题了。

由于CommonPackage编译时加了dllmode选项,所以只适用于dll,而不适用于其它的工程,比如主窗体,项目管理工具,其它工具等,否则也会报错(因为它们需要执行IFNDEF处的代码)。所以最终决定编译dll使用这个包,而编译主窗体和项目管理工具等时,不带这个包,当然也可以创建两个BPL,一个带dllmode选项,一个不带dllmode选项,编译dllexe时,根据情况决定用哪个包。

COM注册失败

设置好GCMGCC的关联后,在项目基本信息模块,点击“上报”按钮,报错如下:

既然提示检查设置,那就先看看设置,经详细检查,排除了设置的问题。再仔细看错误信息:没有注册类别。于是到GCCCOM里面去跟踪调试,后来发现是一个COM组件没有注册:pdoSJSB3_GCC.dll。注册COM组件应该是在安装时,自动注册的。尝试到组件服务中手工注册,结果注册失败,如下图所示:

对这个问题,找了半天也找不到问题出在哪里。于是把Jiayp拉过来看,试了半天……还是不行。主要认为是它引用的包没有找到。Jiapy又把Linc拉过来看,使用了一些工具,像DependencyPE Explorer等,可忙了半天也查不到问题。后来Linc想到一个方法:直接LoadLibrary,报如下的错误:

一开始认为是GCMCommonPackage.BPLpdoSJSB3_GCC.dll版本不兼容,后来发现即使是从编辑机编出来的gip包里取这两文件,还是一样报错。这就排除了版本的原因。

仔细看一下这个错误,意思是在Udocommon3executor.pas单元,找不到TdoCommon3Executor.CreateDOData方法。

于是查看udoCommon3executor.pas单元,按Ctrl+G,果然找不到CreateDOData方法,再仔细一看,原来是有这个方法的,只不过是抽象方法:

function CreateDOData(ADBInfo: OleVariant; AClientType: Integer; AOwnerData: OleVariant): TCommonDOData; virtual; abstract;

我猜想编译BPL包的时候,没有把抽象方法处理进去,而注册COM的时候,又需要该方法,所以报这个错误。具体原因还没有深究。解决方法是把该方法改为空的虚方法而非抽象方法,试验了一下,果然OK了。

后来Jiapy又提示我,可能是因为子类调用了inherited CreateDOData,我找了一下,果然在udoSJSB3Executor_GCC.pas中,有如下代码:

Result := inherited CreateDOData(ADBInfo, AClientType, AOwnerData);

明明父类是抽象方法,这里怎么还调用呢。于是把该处代码注释掉。果然,父类改回抽象方法,也OK

你可能感兴趣的:(Delphi,VCL组件开发与应用)