除了在源代碼層面實現共享(“前.NET Core時代”如何實現跨平臺代碼重用 ——源文件重用)之外,我們還可以跨平臺共享同一個程序集,這種獨立于具體平臺的“中性”程序集通過創建一種名為“可移植類庫(PCL: Portable Class Library)”項目來實現。為了讓讀者朋友們對PCL的實現機制具有充分的認識,我們先來討論一個被我稱為“程序集動態綁定”的話題。
目錄
一、何謂程序集動態綁定?
二、程序集一致性
三、程序集重定向
四、類型的轉移
五、可移植類庫(PCL)
一、何謂程序集動態綁定?
我們采用C#、VB.NET這樣的編程語言編寫的源文件經過編譯會生成有IL代碼和元數據構成的托管模塊,一個或者多個托管模塊合并生成一個程序集。除了包含必要的托管模塊之外,我們還可以將其他文件作為資源內嵌到程序集中,程序集的文件構成一個“清單(Manifest)”文件來描述,這個清單文件包含在某個托管模塊中。
元數據使程序集成為一個自描述性(Self-Describling)的部署單元,除了描述定義在本程序集中所有類型之外,這些元數據還包括對引用自外部程序集的所有類新的描述。包含在元數據中針對外部程序集的描述是由編譯時引用的程序集決定的[1],引用程序集的名稱(包含文件名、版本、語言文化和簽名的公鑰令牌)會直接體現在當前程序集的元數據中。
在運行時,通過元數據描述的引用程序集信息是CLR定位目標程序集的依據,但是這并不意味著它與實際加載的程序集是完全一致的,后者實際上是根據當前執行環境動態加載的,我們姑且將這個機制成為“程序集動態綁定”。
二、程序集一致性
我們都知道.NET Framework是向后兼容的,也就是說原來針對低版本.NET Framework編譯生成的程序集是可以直接在高版本CLR下運行的。我們試想一下這么一個問題:就一個針對.NET Framework 2.0編譯生成的程序集自身來說,所有引用的.NET Framework程序集的版本都是2.0,如果這個程序集在4.0環境下執行,CLR在決定加載它所依賴程序集的時候,應該選擇2.0還是4.0呢?
我們不妨通過實驗來獲得這個問題的答案。我們利用Visual Studio創建一個針對.NET Framework 2.0的控制臺應用(命名為App),并在作為程序入口的Main方法上編寫如下一段代碼。如下面代碼片斷所示,我們在控制臺上輸出了三個基本類型(Int32、XmlDocument和DataSet)所在程序集的全名。
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: Console.WriteLine(typeof(int).Assembly.FullName);
6: Console.WriteLine(typeof(XmlDocument).Assembly.FullName);
7: Console.WriteLine(typeof(DataSet).Assembly.FullName);
8: }
9: }
直接運行這段程序使之在默認版本的CLR(2.0)下運行會在控制臺上輸出如下的結果,我們會發現上述三個基本類型所在程序集的版本都是2.0.0.0。在這種情況下,運行時加載的程序集和編譯時引用的程序集是一致的。
1: mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
2: System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
3: System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
現在我們直接在目錄“\bin\debug”直接找到以Debug模式編譯生成的程序集App.exe,并為之創建一個配置文件(命名為App.exe.config)。我們編寫了如下一段配置,其目的在于選擇4.0版本的CLR運行這個程序。
1: <configuration>
2: <startup>
3: <supportedRuntime version="v4.0"/>
4: </startup>
5: </configuration>
或者:
1: <configuration>
2: <startup>
3: <requiredRuntime version="v4.0"/>
4: </startup>
5: </configuration>
在無需重新編譯(確保運行的依然是針對.NET Framework 2.0編譯生成的程序集)直接運行App.exe,我們會在控制臺上得到如下所示的輸出結果,可以看到三個程序集的版本編程了4.0.0.0。
1: mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
2: System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
3: System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
這個簡單的實例體現了這么一個特征:運行過程中加載的.NET Framework程序集(承載FCL的程序集)是由當前運行時(CLR)決定的,這些程序集的版本總是與CLR的版本相匹配。包含在元數據中的程序集信息提供目標程序集的名稱,而版本則由當前運行的CLR來決定,我們將這個重要的機制稱為“程序集一致性(Assembly Unification)”,下圖很清晰地揭示了這個特性。
三、程序集重定向
在默認情況下,如果某個程序集引用了另一個具有強簽名的程序集,CLR在執行的時候總是會根據程序集有效名稱(Assembly Qualified Name,由程序集文件名、版本、語言文化和公鑰令牌組成)去定位目標程序集,如果無法找到一個與之完全匹配的程序集,一般情況下會拋出一個FileNotFoundException類型的異常。程序集的重定向機制實際上是讓CLR在定位目標程序集的時候“放寬”了匹配的條件,即指要求目標程序集的文件名與元數據描述的程序集一致即可。
如下圖所示,程序集(Lib.dll)在編譯的時候引用了可被重定向的程序集“Retargetable, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a”。在采用運行時Runtime1和Runtime2所在的執行環境下,真正綁定的目標程序集分別為“Retargetable, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35”和“Retargetable, Version=3.0.0.0, Culture=neutral, PublicKeyToken
=30ad4fe6b2a6aeed”,除了程序集文件名稱,它們的版本和公鑰令牌與編譯時引用的程序集均不相同。
實際上通過PCL項目編譯生成的程序集所引用的都是這種能夠被重定向的程序集(以下簡稱Retargetable程序集)。與普通程序集相比較,這種可被重定向的程序集的唯一不同之處在于它多了一個如下所示的retargetable標記。
1: 普通程序集
2: .assembly Lib
3:
4: 可被重定向程序集
5: .assembly retargetable Lib
這樣一個標記可以通過按照如下所示的方式在程序集上應用AssemblyFlagsAttribute特性來添加。不過這樣的重定向僅僅是針對.NET Framework自身的程序集有效,雖然我們也可以通過使用AssemblyFlagsAttribute特性為自定義的程序集添加這樣一個retargetable標記,但是CLR并不會賦予它重定向的能力。
1: [assembly:AssemblyFlags(AssemblyNameFlags.Retargetable)]
對于某個程序集來說,針對普通程序集的引用和Retargetable程序集的引用的不同支持會反映在自身的元數據中。下面的代碼片斷體現了元數據對引用程序集的描述,我們可以看到針對Retargetable程序集的引用同樣具有一個retargetable標記。當CLR在定位目標程序集的時候就是根據這個標記決定是否需要重定向到當前運行時環境下與之匹配的程序集,并且這個程序集有可能在版本和公鑰令牌均與元數據描述不同。
1: 針對普通程序集的引用
2: .assembly extern Lib
3: {
4: .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
5: .ver 1:0:0:0
6: }
7:
8: 針對Retargetable程序集的引用
9: .assembly extern retargetable Lib
10: {
11: .publickeytoken = (B7 7A 5C 56 19 34 E0 89)
12: .ver 1:0:0:0
13: }
四、類型的轉移
所謂類型轉移(Type Forwarding)就是將定義在某個程序集中的類型轉移到另一個程序集中。我們先通過一個簡單的實例讓讀者朋友們對類型轉移有一個感官上的認識。我們利用Visual Studio創建一個針對.NET Framework 3.5的控制臺應用,并編寫如下一端簡單的程序輸出兩個常用的類型(Function<T>和TimeZoneInfo)所在程序集的名稱。現在我們直接運行這個程序,會在控制臺上得到如下所示的輸出結果,可以看出.NET Framework 3.5(CLR 2.0)環境下的這兩個類型定義在程序集System.Core.dll中。
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: Console.WriteLine(typeof(Func<>).Assembly.FullName);
6: Console.WriteLine(typeof(TimeZoneInfo).Assembly.FullName);
7: }
8: }
輸出結果:
1: System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
2: System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
現在我們對該程序的配置文件(App.config)作如下的修改,其目的在于采用CLR 4.0來運行該程序。再次運行該程序集之后,我們會在控制臺上得到不一樣的輸出結果。通過如下所示的輸出結果我們可以看出當.NET Framework從3.5升級到4.0的時候,將原本定義在程序集System.Core.dll中的部分類型轉移到了程序集mscorelib.dll之中。
1: <configuration>
2: <startup>
3: <supportedRuntime version="v4.0"/>
4: </startup>
5: </configuration>
輸出結果:
1: mscorelib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
2: mscorelib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
跨程序集之間的類型轉移幫助框架或者類庫的提供者解決這樣的難題:某個類型在框架1.0版本的時候定義在程序集A中,當升級到2.0的時候被轉移到了程序集B中,使用舊版本的應用可以在不做任何修改的情況下直接對使用的框架進行升級。類型轉移需要使用到一個特殊的特性TypeForwardedToAttribute,我們現在通過一個簡單的實例來演示如何利用這個特性來解決框架或者類庫升級過程在類型跨程序集轉移的問題。
這個演示的場景如上圖所示:代表應用的App.exe在編譯的時候引用了代表框架的程序集Lib.dll,具體使用的是定義其中的類型Foobar,框架進行升級之后新增了一個程序集Lib2.dll,原來定義在Lib.dll中的類型Foobar被轉移到了Lib2.dll中。充分利用CLR針對類型轉移的支持,我們只需要直接部署新版本的Lib.dll(不包含類型Foobar)和Lib2.dll,現有的程序能夠照常運行。
我們利用Visual Studio創建了如上圖所示的解決方案。類庫項目Lib1代表版本1.0的框架,我們將編譯生成的程序集名稱設置成Lib,并在其中定義了一個類型Foobar。控制臺應用直接應用Lib1,并與其中編寫了如下一段簡單的程序,其目的在于確認類型Foobar所在的程序集。
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: Console.WriteLine(typeof(Foobar).AssemblyQualifiedName);
6: Console.Read();
7: }
8: }
類庫項目Lib2和Lib3編譯生成代表框架升級之后的兩個程序集,我們通過修改項目屬性將目標程序集名稱設置成Lib和Lib2,Lib2具有針對Lib3的項目引用。我們在Lib3中重新定義了代表被轉移的類型Foobar,而Lib2實際上是一個空的項目。要體現類型Foobar從Lib.dll轉移到Lib2.dll,我們需要在Lib2項目上應用如下所示的一個TypeForwardedToAttribute特性(定義在AssemblyInfo.cs中)。
1: [assembly:TypeForwardedTo(typeof(Foobar))]
現在我們對整個解決方案進行編譯,然后定位到控制臺App項目編譯后的輸出目錄(app\bin\debug),并將項目Lib1編譯生成的程序集Lib.dll刪除,而將Lib2和Lib3編譯生成的程序集Lib.dll和Lib2.dll拷貝到該目錄下。現在我們直接運行App.exe,我們會在控制臺上得到如下所示的輸出結果。
1: Lib.Foobar, Lib2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null
如果某個項目應用了TypeForwardedToAttribute特性指向定義在另一個程序集中的被轉出類型,類型轉移相關的信息會體現在編譯生成的元數據中。就我們的實例而言,項目Lib2編譯的生成的程序集通過如下的元數據來指向被轉移出去的類型所在的目標程序集。
1: .class extern forwarder Lib.Foobar
2: {
3: .assembly extern Lib2
4: }
當App.exe被執行的時候,由于元數據體現的依然是針對程序集Lib.dll的引用,所以CLR任然會試圖從該程序集中加載類型Foobar。但是通過分析程序集Lib.dll的元數據,CLR知道Foobar已經被轉移到程序集Lib2.dll中,所以定義在其中的同名類型Foobar最終會被加載。
五、可移植類庫(PCL)
就目前來說,創建PCL項目是實現跨.NET Framework平臺程序集共享唯一的方式。當我們采用Class Library(Portal)項目模板創建一個PCL項目的時候,需要在如下圖所示的對話框中選擇支持的目標平臺及其版本。Visual Studio會為新建的項目添加一個名為“.NET”的引用,這個引用指向一個由選定.NET Framework平臺決定的程序集列表。由于這些程序集提供的API能夠兼容所有選擇的平臺,我們在此基礎編寫的程序自然也具有平臺兼容性。
如果查看這個特殊的.NET引用所在的地址,我們會發現它指向目錄“%ProgramFiles%\Reference Assemblies\Microsoft\Framework\.NETPortable\{version}\Profile\ProfileX”。如果查看 “%ProgramFiles%
\Reference Assemblies\Microsoft\Framework\.NETPortable” 目錄,我們會發現它具有如下圖所示的結構。
如圖上所示,目錄“%ProgramFiles%\Reference Assemblies\Microsoft\Framework\.NETPortable”下具有三個代表.NET Framework版本的子目錄(v4.0、v4.5和v4.6)。具體到針對某個.NET Framework版本的目錄(比如v4.6),其子目錄Profile下具有一系列以“Profile”+“數字”(比如Profile31、Profile32和Profile44等)命名的子目錄,實際上PCL項目引用的就是存儲在這些目錄下的程序集。
對于兩個不同平臺的.NET Framework來說,它們的Core Library在API的定義上存在交集,從理論上來說,建立在這個交集基礎上的程序是可以被這兩個平臺中共享的。如下圖所示,如果我們編寫的代碼需要分別對Windows Desktop/Phone、Windows Phone/Store和Windows Store/Desktop平臺提供支持,那么這樣的代碼依賴的部分僅限于兩兩的交集A+B、A+C和A+D。如果要求這部分代碼能夠運行在Windows Desktop/Phone/Store三個平臺上,那么它們只能建立在三者之間的交集A上。
針對所有可能的.NET Framework平臺(包括版本)的組合,微軟會將體現在Core Library上的交集提取出來并定義在相應的程序集中。比如說所有的.NET Framework平臺都包含一個核心的程序集mscorelib.dll,雖然定義其中的類型及其成員在各個.NET Framework平臺不盡相同,但是它們之間肯定存在交集,微軟針對不同的.NET Framework平臺組合將這些交集提取出來并定義在一系列同名程序集中,并同樣命名為mscorelib.dll。 微軟按照這樣的方式創建了其他針對不同.NET Framework平臺組合的基礎程序集,這些針對某個組合的所有程序集構成一系列的Profile,并定義在上面我們提到過的目錄下。值得一提的是,所有這些針對某個Profile的程序集均為Retargetable程序集。
當我們創建一個PCL項目的時候,第一個必需的步驟是選擇兼容的.NET Framework平臺,Visual Studio會根據我們的選擇確定一個具體的Profile,并為創建的項目添加針對該Profile的程序集引用。由于所有引用的程序集是根據我們選擇的.NET Framework平臺“度身定制”的,所以定義在PCL項目的代碼才具有可移植的能力。
上面我們僅僅從開發的角度解釋了定義在PCL項目的代碼本身為什么能夠確保是與目標.NET Framework平臺兼容的,但是在運行的角度來看這個問題,卻存在額外兩個問題:
- 元數據描述的引用程序集與真實加載的程序集不一致,比如我們創建一個兼容.NET Framework 4.5和Silverlight 5.0的PCL項目,被引用的程序集mscorellib.dll的版本為2.0.5.0,但是Silverlight 5.0運行時環境中的程序集mscorellib.dll的版本則為5.0.5.0。
- 元數據描述的引用程序集的類型定義與運行時加載程序集類型定義不一致,比如引用程序集中的某個類型被轉移到了另一個程序集中。
由于PCL項目在編譯時引用的均為Retargetable程序集,所以程序集的重定向機制幫助我們解決了第一個問題。因為在CLR在加載某個Retargetable程序集的時候,如果找不到一個與引用程序集在文件名、版本、語言文化和公鑰令牌完全匹配的程序集,則會只考慮文件名的一致性。至于第二個問題,自然可以通過上面我們介紹的類型轉移機制來解決。
[1] 當我們執行C#編譯器(csc.exe)以命令行的形式編譯C#源代碼時,引用的程序集通過“/reference”開關指定。
文章列表