■印象中的Packagehtml
在通常的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主系統)ui Project1.exe(會計子系統)spa 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類別(如TForm一、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加載/釋放動做都透過這個公用函數庫包裝的函數去執行動做。