淺談.NET中程序集的動態加載


我想有不少人像我一樣,剛開始使用.NET中動態加載程序集的功能時,會被Assebmly中那么多加載程序集的方法搞得無所適從。當求助於MSDN和Baidu、Google后,可能會更加迷茫——說實話MSDN中相關的說明確實很難理解甚至有自相矛盾的地方,網上的大多數資料也講得不甚明了。所以,我在這里分享一下自己對這些函數及其背后相關概念的理解,希望能幫到大家。文中如有錯誤,還請大家指正。

本文的內容主要基於MSDN和Steven Pratschner的《Customizing the Microsoft® .NET Framework Common Language Runtime》一書,這本書應該算是每個想深入研究CLR的程序員的必讀書目。


 

首先,向大家介紹一個非常好的工具——fuslogvw.exe(程序集綁定日志查看器)。使用它,我們可以查看CLR加載每一個程序集的決策過程。fuslogvw是與.NET Framework SDK一起發布的,如果你安裝了Visual Studio,那么就可以通過在Visual Studio命令提示中鍵入fuslogvw運行它,其運行界面如下圖所示。

image

在使用之前,需要點擊“設置”按鈕,勾選“記錄所有綁定到磁盤”選項。如下圖所示。

image

要查看代碼中某個程序集的加載過程,可以先點擊“全部刪除”以清空當前日志,然后運行要檢查的程序,之后點擊“刷新”按鈕,就可以看到相應程序集對應的條目了。左側列表中的每一項對應一個應用程序加載的一個程序集,選中一個條目,點擊“查看日志”按鈕即可顯示相應的內容,如下所示。

 1 *** 程序集聯編程序日志項 (################) ***
 2 
 3 操作成功。
 4 綁定結果: hr = 0x0。操作成功完成。
 5 
 6 程序集管理器加載位置:  C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
 7 在可執行文件下運行  D:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\TraceDebugger Tools\IntelliTrace.exe
 8 --- 詳細的錯誤日志如下。
 9 
10 === 預綁定狀態信息 ===
11 日志: 用戶 = ##########12 日志: DisplayName = System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
13  (Fully-specified)
14 日志: Appbase = file:///D:/Program Files (x86)/Microsoft Visual Studio 10.0/Team Tools/TraceDebugger Tools/
15 日志: 初始 PrivatePath = NULL
16 日志: 動態基 = NULL
17 日志: 緩存基 = NULL
18 日志: AppName = IntelliTrace.exe
19 調用程序集: (Unknown)。
20 ===
21 日志: 此綁定從 default 加載上下文開始。
22 日志: 正在使用應用程序配置文件: D:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\TraceDebugger Tools\IntelliTrace.exe.Config
23 日志: 使用主機配置文件: 
24 日志: 使用 C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config 的計算機配置文件。
25 日志: 通過在 GAC 中查找找到了程序集。
26 日志: 綁定成功。從 C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll 返回程序集。
27 日志: 在 default 加載上下文中加載了程序集。

此日志中大部分內容都比較直觀,需要特別注意的是:

  • 第12-13行:表示此次綁定使用的是Fully specified reference。
  • 第21-27行:表示此次綁定是從“default加載上下文”開始的,並成功從全局程序緩存中找到了程序集,而加載到了“default加載上下文”中。如果你還不是很清楚什么是“default加載上下文”也不要緊,這正是本文想要澄清的內容之一。而且這個名詞與MSDN或其它資料中的名稱也不一致,相關情況我們會在后面說明。

此外,如果在加載程序集時出現異常,也可以參考異常中的FusionLog屬性,其內容與fuslogvw的內容類似。

然后,我們明確一些概念,以便在后面的討論不至於產生混淆。現在回想起來,我在當初學習時,也正因為沒有明確區分這些概念,而造成了不小的困惑。

* Strong-Named的程序集和Weak-Named的程序集:也許這兩個單詞並不標准,但我覺得在本文中,這樣比“具有強名稱的程序集”和“不具有強名稱的程序集(或者說具有弱名稱的程序集)”更簡潔,也更能明確表達我的意思。一個程序集如果經過了密鑰簽名,則稱之為Strong-Named,否則稱之為Weak-Named。更精確的定義,請參考MSDN。

*程序集的Friendly Name和File Name(程序集對應的文件系統的文件名):一般情況下二者是相同的,但沒有什么能阻止你把Utilities.dll改名為Util.dll,而這兩者之間有什么關系呢?我總結得出以下幾點:

  • 當試圖調用 gacutil –i Util.dll 將改名后的程序集安裝到GAC中時,會出現錯誤“將程序集添加到緩存失敗: 文件或程序集名稱無效。文件名必須是程序集名稱后加上 .dll或 .exe 擴展名。”;
  • 當Load(“Utilities”)加載程序集時,如果GAC查找失敗,運行時將在某些目錄中搜索名稱為Utilities.dll或Utilities.exe的文件,這時改名后的文件將不可能被搜索到;
  • 所以改名后的Util.dll只可能會在Assembly.LoadFrom(“path\to\Util.dll”)和Assembly.LoadFile(“path\to\Util.dll”)這樣的調用中被加載,而加載時的具體行為也會依賴於程序的具體執行狀態。詳細內容我們會在后面講述。

* Assembly’s identity:關於這個概念,各種資料中至少有幾種不同的定義:

  • MSDN 1:http://msdn.microsoft.com/en-us/library/wd40t7ad(v=vs.100).aspx中有這樣一段文字——“A strong name consists of the assembly's identity—its simple text name, version number, and culture information (if provided)—plus a public key and a digital signature”。按這種說法,Assembly’s identity包含Friendly name, Version number, Culture三部分;
  • MSDN 2:http://msdn.microsoft.com/en-us/library/system.reflection.assemblyname.aspx中有這樣一段文字——“An assembly's identity consists of the following: Simple name. Version number. Cryptographic key pair. Supported culture”。按這種說法,Assembly’s identity包含Friendly name, Version number, Culture, Cryptographic key pair;
  • MSDN 3:http://msdn.microsoft.com/en-us/magazine/dd727509.aspx中有這樣一段文字——“A fully qualified assembly identity consists of four fields: the simple name of the assembly, the version, the culture, and the public key token”。按這種說法,Assembly’s identity包含Friendly name,Version number, Culture, Public key token;
  • 《Customizing the Microsoft® .NET Framework Common Language Runtime》:中有這樣一段文字——“An assembly's identity consists of four parts: Friendly name,Version,Public key, Culture”。按這種說法,Assembly’s identity包含Friendly name,Version number, Culture, Public key;
  • 在分析fuslogvw的輸出時,有時會看到如下文字“ 建議為程序集提供完全指定的文字標識,並由簡單名稱、版本、區域性和公鑰標記組成”。

你糊塗了嗎?

可以看到,Friendly name, Version number和Culture作為Assembly’s identity的一部分是沒有問題的,主要分歧在於和密鑰相關的內容是否應該包含在Assembly’s identity中,如果應該包含,那么要包含什么內容。我個人比較傾向於MSDN 3的定義,即Public key token是Assembly’s identity的第四項,理由如下:

  • 既然是Identity,就應該能夠唯一標識程序集的內容,而不同的組織完全可以放出Friendly name, Version number和Culture都相同的程序集,而(經過認證的)公鑰則被用於標識不同的組織,所以MSDN 1不合理;
  • 當我們拿到一個程序集時,是不可能知道其相關的Private key的信息的,所以Private key不應該算做程序集Identity的一部分,所以MSDN 2不合理;
  • Public key token是Public key經過SHA1變換得來的,可以認為它們二者是等價的,而我們在引用程序集時大多數情況下都會使用前者。相比之下,將前者作為程序集Identity的一部分更簡潔直觀些。

在本文中提到Assembly’s Identity時將使用MSDN 3的定義。

* Fully specified reference和Partially specified reference:當我們調用Load(string)或Load(AssebmlyName)加載程序集時所指定的參數即為"reference”,以string類型的參數為例,形如 

ExampleAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a5d015c7d5a0b012
ExampleAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

這樣指定了程序集的Friendly name, Version, Culture和PublicKeyToken的形式稱之為Fully specifed reference。也許你對第二種情況也是Fullly specified reference感到奇怪,但規則就是這樣——null也是值,它與沒有指定值是不同的。如果Version, Culture, PublicKeyToken不完整,則為Partially specified reference。所謂的“不完整”包括以下兩種情況: 

缺少某一項。比如:
ExampleAssembly, Version=1.0.0.0, PublicKeyToken=a5d015c7d5a0b012 
ExampleAssembly, Version=1.0.0.0

版本號不全。比如:
ExampleAssembly, Version=1.0, Culture=neutral, PublicKeyToken=a5d015c7d5a0b012 

應該特別注意其中“版本號不全”的情況!

針對這兩種引用方式,CLR在加載程序集時的處理方式是不同的。對於Fully specified的情況,CLR會按以下順序進行處理:

  1. 根據Version policy確定要加載的程序集版本;
  2. 在GAC中查找程序集;
  3. 在Codebase中查找程序集;
  4. 在ApplicationBase及其子目錄中查找程序集。

而對於Partially specified的情況,CLR會按以下方式處理:

Load

其中尤其需要注意的是使用提取的Fully specified reference重新查找程序依集的步驟,后面討論LoadFrom時也會有類似的過程。

* load, context, load context, load from, load from context ...: 是的,你沒看錯,就是這些概念。我想這也是動態加載程序集相關資料難以理解的原因之一,當遇到這幾個詞(或者相應的中文翻譯)時,你要非常小心的斷句,弄明白作者到底在說什么。歸納起來有如下幾點(這些說法有些不太嚴謹,但有利於大家理解相關概念):

  • 運行時會將加載進來的程序集放到四個可能的地方之一,這里的“地方”即為context,你可以把它們想像成四個鏈表;
  • 這四個地方分別叫做:load context, load from context, reflection only context和no context。【其中no context是我杜撰的,其它的資料中把第四種情況加載的程序集說成“程序集不加載到任何上下文中”——這其實有兩個層次的意思,一是程序集被加載了,二是程序集不在任何上下文中——如果看到這句話你已經迷糊了,請重新讀此段文本,同時忽略藍色的字】。這些名字是有些奇怪,但它們也僅僅是名字而已。 

前面提到的fuslogvw輸出中的“default加載上下文”其實指的是load context。


 

終於到了介紹Load, LoadFrom, LoadFile三個函數的地方了。其實搞清楚了上面的概念,這一部分就很簡單了。

Load(AssemblyName)、Load(string)

  • string和AssemblyName是程序集引用(fully specified reference或partially specified reference)的兩種指定方式。
  • 使用這兩個方法加載的程序集,會被放到load context中。
  • 使用這種方式加載的程序集的依賴項會被自動以相同的方式加載。
  • 當參數為fully specified reference或者partially specified reference時,具體步驟請參見前面關於兩種引用的敘述。
  • 由應用程序靜態引用的程序集,可以視為是通過Load方式加載的。

LoadFrom(string)

  • string是某個程序集文件的路徑。
  • CLR在加載此文件后會提取Assembly’s Identity,並以此重新執行Fully specified reference查找,如果查找到了某個程序集A,而且A對應的文件剛好是string指定的文件,則保留A,並把A放到load context中;否則,保留之前加載的程序集,並把它放到load from context中。但是,
  • 在將程序集放到load from context中之前,CLR會判斷load from context中是否已經存在與之Identity相同的程序集,如果沒有就保留它;否則丟棄它並返回已經存在的那個程序集。所以如果你在c:\library.dll和c:\lib\library.dll中放了兩個具有相同Identity的程序集,則:
    var a1 = Assembly.LoadFrom(@"c:\library.dll");
    var a2 = Assembly.LoadFrom(@"c:\lib\library.dll");

    這段代碼中的a1與a2將引用同一個對象。

  • 使用這種方式加載的程序集的依賴項會被自動以相同的方式加載,而且這此被依賴的程序集可以將在string所表示的目錄中(除去文件名)——這個目錄可能在ApplicationBase之外。
  • 為什么要有load from context和load context的區別?考慮這種情況:應用程序c:\apps\a.exe引用了Weak-Named私有程序集library.dll,其Identity為library, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null,這個文件被放在a.exe所在的目錄下;而a.exe又提供了以插件形式動態加載其它程序集的功能,用戶通過在菜單中指定要加載的程序集路徑來加載插件。假設有一個插件對應的程序集為c:\addins\library.dll,它的Identity與前者相同。但它們確實是完全不相干的程序集。那么CLR為了保證不論先加載哪一個a.exe都能正常運行,就采用了不同context的機制。更權威的說明,請參考《Customizing the Microsoft® .NET Framework Common Language Runtime》。

LoadFile(string)

  • Load和LoadFrom的行為那么復雜,而且加載的不一定就是我指定的程序集,如果我真的確定以及肯定就想加載某個程序集文件怎么辦呢?這就是為什么會有LoadFile的原因了。其實,在.NET Framework 1.0中並沒有LoadFile,因為有了前面提到的原因,才在.NET Framework 1.1中加入了LoadFile。
  • 使用此方法加載的程序集的依賴項不會被自動加載,可以通過AppDomain.AssemblyResolve事件來處理相關程序集的加載。
  • LoadFile把程序集加載到no context中,而且允許多個Identity相同但路徑不同的程序集同時存在。

好了,就這些了。歡迎大家指正!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM