1. 程式人生 > >“前.NET Core時代”如何實現跨平臺程式碼重用 ——程式集重用

“前.NET Core時代”如何實現跨平臺程式碼重用 ——程式集重用

  除了在原始碼層面實現共享(“前.NET Core時代”如何實現跨平臺程式碼重用 ——原始檔重用)之外,我們還可以跨平臺共享同一個程式集,這種獨立於具體平臺的“中性”程式集通過建立一種名為“可移植類庫(PCL: Portable Class Library)”專案來實現。為了讓讀者朋友們對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)所在程式集的全名。

  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);

  }

  }

  直接執行這段程式使之在預設版本的CLR(2.0)下執行會在控制檯上輸出如下的結果,我們會發現上述三個基本型別所在程式集的版本都是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

  現在我們直接在目錄“bindebug”直接找到以Debug模式編譯生成的程式集App.exe,併為之建立一個配置檔案(命名為App.exe.config)。我們編寫了如下一段配置,其目的在於選擇4.0版本的CLR執行這個程式。

  C#

  或者:

  C#

  在無需重新編譯(確保執行的依然是針對.NET Framework 2.0編譯生成的程式集)直接執行App.exe,我們會在控制檯上得到如下所示的輸出結果,可以看到三個程式集的版本程式設計了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

  這個簡單的例項體現了這麼一個特徵:執行過程中載入的.NET Framework程式集(承載FCL的程式集)是由當前執行時(CLR)決定的,這些程式集的版本總是與CLR的版本相匹配。包含在元資料中的程式集資訊提供目標程式集的名稱,而版本則由當前執行的CLR來決定,我們將這個重要的機制稱為“程式集一致性(Assembly Unification)”,下圖很清晰地揭示了這個特性。

09

  三、程式集重定向

  在預設情況下,如果某個程式集引用了另一個具有強簽名的程式集,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”,除了程式集檔名稱,它們的版本和公鑰令牌與編譯時引用的程式集均不相同。

10

  實際上通過PCL專案編譯生成的程式集所引用的都是這種能夠被重定向的程式集(以下簡稱Retargetable程式集)。與普通程式集相比較,這種可被重定向的程式集的唯一不同之處在於它多了一個如下所示的retargetable標記。

  C#

  普通程式集

  .assembly Lib

  可被重定向程式集

  .assembly retargetable Lib

  這樣一個標記可以通過按照如下所示的方式在程式集上應用AssemblyFlagsAttribute特性來新增。不過這樣的重定向僅僅是針對.NET Framework自身的程式集有效,雖然我們也可以通過使用AssemblyFlagsAttribute特性為自定義的程式集新增這樣一個retargetable標記,但是CLR並不會賦予它重定向的能力。

  C#

  1[assembly:AssemblyFlags(AssemblyNameFlags.Retargetable)]

  對於某個程式集來說,針對普通程式集的引用和Retargetable程式集的引用的不同支援會反映在自身的元資料中。下面的程式碼片斷體現了元資料對引用程式集的描述,我們可以看到針對Retargetable程式集的引用同樣具有一個retargetable標記。當CLR在定位目標程式集的時候就是根據這個標記決定是否需要重定向到當前執行時環境下與之匹配的程式集,並且這個程式集有可能在版本和公鑰令牌均與元資料描述不同。

  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

  }

  四、型別的轉移

  所謂型別轉移(Type Forwarding)就是將定義在某個程式集中的型別轉移到另一個程式集中。我們先通過一個簡單的例項讓讀者朋友們對型別轉移有一個感官上的認識。我們利用Visual Studio建立一個針對.NET Framework 3.5的控制檯應用,並編寫如下一端簡單的程式輸出兩個常用的型別(Function和TimeZoneInfo)所在程式集的名稱。現在我們直接執行這個程式,會在控制檯上得到如下所示的輸出結果,可以看出.NET Framework 3.5(CLR 2.0)環境下的這兩個型別定義在程式集System.Core.dll中。

  C#

  class Program

  {

  static void Main(string[] args)

  {

  Console.WriteLine(typeof(Func<>).Assembly.FullName);

  Console.WriteLine(typeof(TimeZoneInfo).Assembly.FullName);

  }

  }

  輸出結果:

  C#

  System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

  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之中。

  C#

  輸出結果:

  C#

  mscorelib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

  mscorelib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

  跨程式集之間的型別轉移幫助框架或者類庫的提供者解決這樣的難題:某個型別在框架1.0版本的時候定義在程式集A中,當升級到2.0的時候被轉移到了程式集B中,使用舊版本的應用可以在不做任何修改的情況下直接對使用的框架進行升級。型別轉移需要使用到一個特殊的特性TypeForwardedToAttribute,我們現在通過一個簡單的例項來演示如何利用這個特性來解決框架或者類庫升級過程在型別跨程式集轉移的問題。

11

  這個演示的場景如上圖所示:代表應用的App.exe在編譯的時候引用了代表框架的程式集Lib.dll,具體使用的是定義其中的型別Foobar,框架進行升級之後新增了一個程式集Lib2.dll,原來定義在Lib.dll中的型別Foobar被轉移到了Lib2.dll中。充分利用CLR針對型別轉移的支援,我們只需要直接部署新版本的Lib.dll(不包含型別Foobar)和Lib2.dll,現有的程式能夠照常執行。

12

  我們利用Visual Studio建立瞭如上圖所示的解決方案。類庫專案Lib1代表版本1.0的框架,我們將編譯生成的程式集名稱設定成Lib,並在其中定義了一個型別Foobar。控制檯應用直接應用Lib1,並與其中編寫了如下一段簡單的程式,其目的在於確認型別Foobar所在的程式集。

  C#

  mscorelib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

  mscorelib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

  類庫專案Lib2和Lib3編譯生成代表框架升級之後的兩個程式集,我們通過修改專案屬性將目標程式集名稱設定成Lib和Lib2,Lib2具有針對Lib3的專案引用。我們在Lib3中重新定義了代表被轉移的型別Foobar,而Lib2實際上是一個空的專案。要體現型別Foobar從Lib.dll轉移到Lib2.dll,我們需要在Lib2專案上應用如下所示的一個TypeForwardedToAttribute特性(定義在AssemblyInfo.cs中)。

  C#

  1[assembly:TypeForwardedTo(typeof(Foobar))]

  現在我們對整個解決方案進行編譯,然後定位到控制檯App專案編譯後的輸出目錄(appbindebug),並將專案Lib1編譯生成的程式集Lib.dll刪除,而將Lib2和Lib3編譯生成的程式集Lib.dll和Lib2.dll拷貝到該目錄下。現在我們直接執行App.exe,我們會在控制檯上得到如下所示的輸出結果。

  C#

  1Lib.Foobar, Lib2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null

  如果某個專案應用了TypeForwardedToAttribute特性指向定義在另一個程式集中的被轉出型別,型別轉移相關的資訊會體現在編譯生成的元資料中。就我們的例項而言,專案Lib2編譯的生成的程式集通過如下的元資料來指向被轉移出去的型別所在的目標程式集。

  C#

   class Program

  {

  static void Main(string[] args)

  {

  Console.WriteLine(typeof(Foobar).AssemblyQualifiedName);

  Console.Read();

  }

  }

  當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能夠相容所有選擇的平臺,我們在此基礎編寫的程式自然也具有平臺相容性。

image

  如果檢視這個特殊的.NET引用所在的地址,我們會發現它指向目錄“%ProgramFiles%Reference AssembliesMicrosoftFramework.NETPortable{version}ProfileProfileX”。如果檢視 “%ProgramFiles%

  Reference AssembliesMicrosoftFramework.NETPortable” 目錄,我們會發現它具有如下圖所示的結構。

14

  如圖上所示,目錄“%ProgramFiles%Reference AssembliesMicrosoftFramework.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上。

image

  針對所有可能的.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”開關指定。