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