二、生成、打包、部署和管理應用程序及類型
2.1 .NET Framework部署目標
Windows多年來一直因爲不穩定和過於複雜而口碑不佳。存在所謂”DLL hell“、安裝的複雜性等繁瑣的問題,而.NET Framework 正在嘗試徹底解決DLL hell的問題,也在很大程度上解決了應用程序狀態在用戶硬盤中四處分散的問題。 >和COM不同,類型不再需要註冊表中的設置。……像Microsoft SQL Server這樣的宿主應用程序只能將少許權限授予代碼,而本地安裝的(自寄宿)應用程序可獲得完全信任(全部權限)
2.2 將類型生成到模塊中
System.Console是Microsoft實現好的類型,用於實現這個類型的各個方法的IL代碼存儲在MSCorLib.dll
public sealed class Program{
public static void Main(){
System.Console.WriteLine("Hi");
}
}
對於上述代碼,由於引用了Console類的WriteLine方法,要順利通過編譯,必須向C#編譯器提供一組程序集,使他能解析對外部類型的引用。因此需要添加r:MSCorLib.dll(此處”r“意爲reference)開關命令,完整編譯命令行應如下:
csc.exe /out:Program.exe/t:exe/r:MSCorLib.dll Program.cs
但由於其他命令均爲默認命令,本例中的編譯命令行可以簡化爲
csc.exe Program.cs
如果不想C#編譯器自動引用MSCorLib.dll程序集,可以使用/nostdlib開關。
2.2-1生成三種應用程序的編譯器開關
- 生成控制檯用戶界面(Console User Interface, CUI)應用程序使用/t:exe開關;
- 生成圖形用戶界面(Graphical User Interface, GUI)應用程序使用/t:winexe開關;
- 生成Windows Store應用程序使用/t:appcontainerexe開關;
2.2-2響應文件
編譯時可以指定包含編譯器設置命令的響應文件,例如:假定響應文件MyProject.rsp包含以下文本
/out:MyProject.exe
/target:winexe
爲了讓CSC.exe使用該響應文件,可以像下面這樣調用它
csc.exe @MyProject.rsp CodeFile1.cs CodeFile2.cs
C#支持多個相應文件,其先後順序服從就近原則,優先級爲控制檯命令>本地>全局。.NET Framework具有一個默認的全局CSC.rsp文件,在運行CSC.exe進行編譯時會自動調用,全局CSC.rsp文件中列出了所有的程序集,就不必使用C#的/reference開關顯式引用這些程序集,這會對編譯速度有一些影響,但不會影響最終的程序集文件,以及執行性能,開發者也可以自己爲全局CSC.rsp添加命令開關,但這可能爲在其他機器上重現編譯過程帶來麻煩。
另外,指定/noconfig開關後,編譯器將忽略本地和全局CSC.rsp文件。
2.3 元數據概述
再來回顧一下託管模塊的文件結構,託管PE文件由四部分構成,它們分別爲:PE32(+)頭,CLR頭,元數據以及IL,接下來將展開談元數據的內部結構和作用
- PE32(+)頭是所有windows程序的標準信息頭,詳情可參見
- CLR頭是一個小的信息塊,是託管模塊特有的,包含生成時所面向的版本號、一些標誌、和一個MethodDef token用來指定模塊的入口方法,最後,CLR頭還包含模塊內部的一些元數據表的大小的偏移量
- 元數據是由三種表構成的二進制數據塊,這三種表分別爲定義表(definiton talbe)、引用表(reference table)和清單表(mainfest table)。
表1 常用的元數據定義表
元數據定義表名稱 | 說明 |
---|---|
ModuleDef | 總是包含對模塊進行標識的一個記錄項,這個記錄項包含模塊文件名和擴展名(不含路徑),以及模塊版本ID(爲編譯器創建的GUID)。這樣可以在保留原始名稱記錄的前提下自由重命名文件,但強烈反對重命名文件,因爲可能妨礙CLR在運行時正確定位程序集。 |
TypeDef | 模塊定義的每個類型在這個表中都有一個記錄項,包含類型的名稱、基類、標誌(public/private etc.)以及一些索引,這些索引指向MethodDef中屬於該類型的方法、FieldDef表中該類的字段、PropertyDef表中該類型的屬性以及EventDef表中該類型的時間。 |
MetodDef | 模塊定義的每個方法在這個表中都有一個記錄項(包括入口方法)。每個記錄項都包含方法的名稱、標誌、簽名以及方法的IL代碼在模塊中的偏移量(通俗地說,位置)。每個記錄項還引用了ParamDef表中的一個記錄項,後者包括與方法參數有關的更多信息。 |
FieldDef | 模塊定義的每一個字段在這個表中都有一個記錄項。每個記錄項都包括標誌、類型和名稱。 |
ParamDef | 模塊定義的每個參數在這個表中都有一個記錄項。每個記錄項包含標誌(in/out/retval等)、類型和名稱。 |
PropertyDef | 模塊定義的每個屬性在這個表中都有一個記錄項。每個記錄項都包含標誌、類型和名稱。 |
EventDef | 模塊定義的每個事件在這個表中都有一個記錄項。每個記錄項都包含標誌和名稱。 |
表1:代碼中定義的任何東西都將在上表中的某個表創建一個記錄項。
表2 常用的引用元數據表
引用元數據表名稱 | 說明 |
---|---|
AssemblyRef | 模塊中引用的每個程序集在這個表中都有一個記錄項。每個及錄像都包含綁定(bind) |
ModuleRef | 實現該模塊所引用的類型的每個PE模塊在這個表中都有一個記錄項。每個記錄項都包含模塊的文件名和擴展名(不含路徑),如果存在別的模塊實現了你需要的類型,這個表的作用便是同哪些類型建立綁定關係 |
TypeRef | 模塊引用的每一個類型在這個表中都有一個記錄項。每個記錄項都包括模塊的文件名和一個引用(指向該類型的位置)如果類型在另一個類型中實現,引用指向一個TypeRef記錄項。如果類型在同一個模塊中實現,引用指向一個ModuleDef記錄項。如果類型在調用程序集內的另一個模塊中實現,引用指向一個ModuleDef記錄項。如果類型在不同程序集中實現,引用指向一個AssemblyRef記錄項 |
MemberRef | 模塊引用的每個成員(字段和方法,以及屬性方法和事件方法)在這個表中都有一個記錄項。每個記錄項都包含成員的名稱和簽名,並指向對成員進行定義的那個類型的TypeRef記錄項 |
①譯者注:bind在文檔中有時譯爲“聯編”,binder有時譯爲”聯編程序“,這裏譯爲“綁定”和“綁定器”
2.4 將模塊合併成程序集
程序集(Assembly)是一個或多個類型定義文件及資源文件的集合。在程序集的所有文件中,有一個文件容納了清單(Manifest),如上一節一開始所述,清單也是元數據的組成部分之一,表中主要包含作爲程序集組成部分的那些文件的名稱。此外還描述程序集的版本、語言文化、發佈者、公開導出類型以及構成程序集的所有文件。
CLR操作的是程序集,對於程序集,有以下幾點重要特性:
- 程序集定義了可重用的類型。
- 程序集用一個版本號標記。
- 程序集可以關聯安全信息。
對於一個程序集來說,除了包含清單元數據表的文件,程序集中的其他文件獨立時不具備以上特點
Microsoft爲何考慮要引入程序集這一概念?這是因爲使用程序集,可重用類型的邏輯表示和物理表示就可以分開。物理上,可以將常用的類型放在一個文件中,不常用的程序放在另一些文件中,只在使用時加載,但是在邏輯上,這些程序仍然被組織於同一程序集中,不需要編寫額外的代碼顯式進行鏈接。
提示:總之,程序集是進行重用、版本控制和應用安全性設置的基本單元。
表3 清單元數據表
清單元數據表名稱 | 說明 |
---|---|
AssemblyDef | 如果模塊標識的是程序集,這個元數據表就包含單一記錄項來列出程序集名稱(不包含路徑和擴展名)、版本(major,minor,build和revision)、語言文化、標誌、哈希算法以及發佈者公鑰(可爲null) |
FileDef | 作爲程序集一部分的每個PE文件和資源文件在這個表中都有一個記錄項(清單本身所在的文件除外,該文件在AssemblyDef表的單一記錄項中列出)。在每個記錄項中,都包含文件名和擴展名(不含路徑)、哈希值和一些標誌。如果程序集只包含他自己的文件 |
ManifestResourceDef | 作爲程序集一部分的每個資源在這個表中都有一個記錄項。記錄項中包含資源名稱、一些標誌(如果程序集外部可見,就爲public,否則爲private)以及FileDef表的一個索引(指出資源或流包含在哪個文件中)。如果資源不是獨立文件(比如.jpg或者.gif文件),那麼資源就是包含在PE文件中的流。對於嵌入資源,記錄項還包含一個偏移量,指出資源流在PE文件中的起始位置 |
ExportedTypesDef | 從程序集的所有PE模塊中導出的每個public類型在這個表中都有一個記錄項。記錄項中包含類型名稱、FileDef表的一個索引(指出類型由程序集的哪個文件實現)以及TypeDef表的一個索引。注意,爲了節省空間,從清單所在文件導出的類型不再重複,因爲可以通過元數據的TypeDef表獲取類型信息 |
①譯者注:所謂“如果程序集只包含他自己的文件“,是指程序集只包含他的主模塊,不包含其他非主模塊和資源文件。
指定以下任何命令行開關,C#編譯器都會生成程序集: /t: exe, /t: winexe, /t: appcontainerexe, /t: library 或者/t: winmdobj。這些開關會指示編譯器生成含有清單元數據表的PE文件。
除了這些開關,C#編譯器還支持/t: module開關。這個開關指示編譯器生成一個不包含清單元數據表的PE文件。這樣生成的肯定是一個DLL PE文件。CLR要想訪問其中的任何類型,必須先將該文件添加到一個程序集中。使用/t: module開關時,C#編譯器默認爲輸出文件使用.netmodule擴展名。
遺憾的是,不能直接從Microsoft Visual studio集成開發環境中創建多文件程序集,只能用命令行工具創建多文件程序集。
可以通過C#編譯器,AL連接器等方法生成多模塊程序集,下面將展開介紹
2.4-1通過C#編譯器生成程序集
如果用C#編譯器生成含清單的PE文件,可以使用/addmodule開關。假定有如下兩個源代碼文件:
- RUT.cs, 其中包含不常用類型
- FUT.cs, 其中包含常用類型
下面將不常用類型編譯到一個單獨模塊,這樣一來如果程序集的用戶永遠不使用不常用類型,就不需要部署這個模塊。
csc /t:module RUT.cs
上述命令行造成C#編譯器創建名爲RUT.netmodule的文件。這是一個標準的DLL PE文件,但是CLR不能但單獨加載它。
接着編譯常用類型模塊事實上由於該模塊現在代表整個程序集,所以將輸出的文件名改爲MultiFileLibrary.dll
csc /out:NultiFileLibray.dll /t:library /addmodule:RUT.netmodule FUT.cs
由於指定了.t: library開關,所以生成的是含有清單元數據表的DLL PE文件。/addmodule:RUT.netmodule 開關告訴編譯器RUT.netmodule文件是程序集的一部分,從而將其添加到FileDef清單元數據表,並將RUT.netmodule的公開導出類型添加到ExportedTypesDef清單源數據表。
編譯器最終創建如圖2所示的兩個文件,清單在右邊的文件中。
MultilFileLibrary.dll除了和RUT.netmodule一樣包括一些描述自身類型、方法、字段等的定義元數據表外,還包含額外的清單元數據表,這使MultiFileLibrary.dll(聯合RUT.netmodule)成爲了程序集。清單元數據表描述了程序集的所有文件(MultiFileLibrary.dll本身和RUT.netmodule)。清單元數據表還包含從MultiFuileLibraty.dll和RUT.netmodule導出的所有公共類型
以下供參考。元數據token試一個4字節的值。其中高位字節指明token的類型(0x01=TypeRef, 0x02=TypeDef, 0x23=AssemblyRef, 0x26=File(文件定義), 0x27=ExportedType)更多可參見 .NET Framework SDK包含的 CORHdr.h 文件中的CorTokenType枚舉類型。
在生成新程序集的時候,所引用的程序集中的所有文件都必須存在。
但在運行時,只有被調用的方法確實引用了未加載程序集中的類型時,纔會加載程序。換言之,爲了運行程序,並不要求被引用的程序集的所有文件都存在。
2.4-2 使用程序集鏈接器生成程序集
除了使用C#編譯器,還可以使用”程序集鏈接器“實用程序AL.exe來創建程序集。如果程序集要求包含由不同編譯器生成的模塊(而這些編譯器不支持與C#編譯器的/addmodule開關等家的幾種機制),或者生成時不清楚程序集的打包要求,程序集連接器就顯得相當有用。還可以用AL.exe來生成只含資源的程序集,也就是所謂的附屬程序集,他們通常用於本地化,本章稍後會討論附屬程序集的問題。
AL.exe能生成EXE文件,或者生成只包含清單的DLL PE文件。
爲了理解AL.exe的工作原理,讓我們改變一下MultiFileLibrary.dll程序的集成方式:
csc /t:module RUT.cs
csc /t:module FUT.cs
al /out:MultiFileLibrary.dll /t:library FUT.netmodule RUT.netmodule
圖3展示了執行這些命令後生成的文件。
程序集鏈接器不能將多個文件合併成一個文件。
2.4-3爲程序集添加資源文件
用AL.exe創建程序集時,可用/enbed[resource]開關將文件作爲資源添加到程序集。該開關獲取任意文件,並將文件內容嵌入最終的PE文件。也可用/Link[resource]開關獲取資源文件,但只指出資源包含在程序集的哪個文件,並不嵌入到PE文件中;該資源文件獨立,並必須與程序集文件一同被打包部署
相似的,C#編譯器用/resource開關將資源嵌入PE文件,用/linkresource開關添加記錄項引用資源文件。以上開關均會修改ManifestResourceDef清單表添加記錄項,外部引用的開關還會修改FileDef表以指出資源包文件。
2.4-4 使用VS IDE將程序集添加到項目中
一個項目所需的程序集,除了顯式的在代碼中引用命名空間外,還要在項目引用管理器中引用,爲此請打開解決方案資源管理器,右擊想添加引用的項目,選擇“添加引用”打開“引用管理器”對話框,如圖4所示
其中的COM選項允許從託管代碼中訪問一個非託管COM服務器。這是通過Visual Studio自動生成的一個託管代理類實現的。
2.5 程序集版本資源信息
AL.exe或CSC.exe生成PE文件程序集時,還會在PE文件中嵌入標準的Win32版本資源。
在應用程序代碼中調用System.Diagnostics.FileVersionInfo的靜態方法GetVersionInfo並傳遞程序集路徑作爲參數可以獲取並檢查這些信息。
生成程序集時,這些特性在源代碼中應用於assembly級別。
Visual Studio新建C#項目時會在一個Properties文件夾中自動創建AssemblyInfo.cs文件。可直接打開該文件並修改自己的程序集特有信息。
以下爲上圖中由IDE自動生成的AssemblyInfo.cs文件中的代碼片段,該代碼片段定義了程序集信息,右側的詳細信息窗口所來自的程序集便由此段代碼所屬項目生成。
// 有關程序集的一般信息由以下
// 控制。更改這些特性值可修改
// 與程序集關聯的信息。
[assembly: AssemblyTitle("LentilToolbox")]
[assembly: AssemblyDescription("Licensed under the MIT license")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("LentilToolbox")]
[assembly: AssemblyCopyright("Copyright © 2016 Lentil Sun")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
//將 ComVisible 設置爲 false 將使此程序集中的類型
//對 COM 組件不可見。 如果需要從 COM 訪問此程序集中的類型,
//請將此類型的 ComVisible 特性設置爲 true。
[assembly: ComVisible(false)]
// 如果此項目向 COM 公開,則下列 GUID 用於類型庫的 ID
[assembly: Guid("ac315d57-80ca-4e7a-b55c-064b94547552")]
// 程序集的版本信息由下列四個值組成:
//
// 主版本
// 次版本
// 生成號
// 修訂號
//
//可以指定所有這些值,也可以使用“生成號”和“修訂號”的默認值,
// 方法是按如下所示使用“*”: :
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.1.0.2")]
[assembly: AssemblyFileVersion("1.1.0.2")]
windows資源管理器的屬性對話框顯然遺漏了一些特性值。最遺憾的是沒有顯示AssemblyVersion這個特性的值,因爲CLR加載程序集時會使用這個值。
表4 版本資源字段和對應的AL.exe開關/定製特性
版本資源 | AL.exe開關 | 定製特性/說明 |
---|---|---|
FILEVERSION | /fileversion | System.Reflection.AssemblyFileVersionAttribute |
PRODUCTVERSION | /productversion | System.Reflection.AssemblyInformationalVersionAttribbute |
FILEFLAGS | (無) | 總是設爲VS_FFI_FILEFLAGSMASK(在WinVer.h中定義爲0x0000003F) |
FILEOS | (無) | 總是0 |
FILEOS | (無) | 目前總是VOS_WINDOWS32 |
FILETYPE | /target | 如果指定了/target:exe或target:winexe,就設爲VFT_APP;如果指定了/target:library,就設爲 VFT_DLL |
FILESUBTYPE | (無) | 總是設爲VFT2_UNKNOWN(該字段對VFT_APP和VFT_DLL無意義) |
AssemblyVersion | /Version | System.Reflection.AssemblyVersionAttribute |
Comments | /description | System.Reflection.AssemblyDescriptionAttribute |
CompanyName | /Company | System.Reflection.AssemblyCompanyAttrbute |
FileDescription | /title | System.Reflection.AssemblyTitleAttribute |
FileVersion | /version | System.Reflection.AssemblyFileVersionAttribute |
InternalName | /out | 設定爲指定的輸出文件的名稱(無擴展名) |
LegalCopyright | /copyright | System.Reflection.AssemblyCopyrighhtAttrubute |
LegalTrademarks | /trademark | System.Reflection.AssemblyTrademarkAttribute |
OriginalFilename | /out | 設爲輸出文件的名稱(無路徑) |
PrivateBuild | (無) | 總是空白 |
ProductName | /product | System.Reflection.AssemblyProductAttribute |
ProductVersion | /Productversion | System.Reflection.AssemblyInformationalVersionAttribute |
2.5-1 版本號
上表指出可向程序集應用幾個版本號,所有這些版本號都具有相同的格式如下
表5 版本號格式
- | major(主版本號) | minor(次版本號) | build(內部版本號) | revision(修訂號) |
---|---|---|---|---|
示例 | 2 | 5 | 719 | 2 |
注意:程序集有三個版本號,每個版本號都有不同的用途:
- AssemblyFileVersion:這個版本號存儲在Win32版本資源中供使用者參考,CLR既不檢查,也不關心,這個版本號的作用是說明該程序集的版本。
- AssemblyInformationalVersion:同上,這個版本號存儲在Win32版本資源中供使用者參考,CLR既不檢查,也不關心,這個版本號作用是說明使用該程序集的產品的版本。
- AssemblyVersion:存儲在AssemblyDef清單元數據表中,CLR在綁定到強命名程序集時會用到它。這個版本號很重要,它唯一性地標識了程序集。
2.6 語言文化
除了版本號,語言文化(culture)
①譯者注:文檔翻譯爲“區域性”(博主:面向用戶的界面大多直譯爲語言吧)
表6 程序集語言文化標記的例子
主標記 | 副標記 | 語言文化 |
---|---|---|
zh | (無) | 中文 |
zh | Hans | 中文(簡體) |
zh | Hant | 中文(繁體) |
en | (無) | 英文 |
en | GB | 英國英語 |
en | US | 美國英語 |
未指定具體語言文化的程序集成爲語言文化中性(Culture neutral)。
如果應用程序包含語言文化特有的資源,Microsoft強烈建議專門創建一個程序集來包含代碼和應用程序的默認(或附加)資源。生成該程序集時不要指定具體的語言文化,其他程序集通過引用該程序集來創建和操縱他的公開類型。
標記了語言文化的程序集稱爲附屬程序集(satellite assembly)
一般不要生成引用了附屬程序集的程序集。換言之,程序集的AssemblyRef記錄項只應引用語言文化中性的程序集。要訪問附屬程序集中的類型或成員,應使用第23章“程序集加載和反射”介紹的反射技術。
2.7 簡單應用程序部署(私有部署的程序集)
Windows Store應用對程序集的打包有一套很嚴格的規則,Visual Studio會將應用程序所必要的程序集打包成一個.appx文件。該文件要麼上傳到Windows Store,要麼side-load到機器。用戶安裝應用時,其中包含的所有程序集都進入一個目錄。CLR從該目錄加載程序集
對於非Windows Store的應用,程序打包的方式沒有限制。可以使用.cab文件(從Internet下載時使用,旨在壓縮文件並縮短下載時間)。還可以將程序打包成一個MSI文件,以便由Windows Installer服務(MSIExec.exe)使用。也可以使用Visual Studio內建機制發佈應用程序,具體做法是打開項目屬性頁並點擊“發佈”標籤。這個MSI文件還能安裝必備組件,以及利用ClickOnce技術,應用程序還能自動檢查更新,並在用戶機器上安裝更新。
第三章將討論如何部署可以由多個應用程序訪問的共享程序集。