重温.NET下Assembly的加载过程
最近在工作中牽涉到了.NET下的一個古老的問題:Assembly的加載過程。雖然網(wǎng)上有很多文章介紹這部分內(nèi)容,很多文章也是很久以前就已經(jīng)出現(xiàn)了,但閱讀之后發(fā)現(xiàn),并沒能解決我的問題,有些點(diǎn)寫的不是特別詳細(xì),讓人看完之后感覺還是云里霧里。最后,我決定重新復(fù)習(xí)一下這個經(jīng)典而古老的問題,并將所得總結(jié)于此,然后會有一個實(shí)例對這個問題進(jìn)行演示,希望能夠幫助到大家。
.NET下Assembly的加載過程
.NET下Assembly的加載,最主要的一步就是確定Assembly的版本。在.NET下,托管的DLL和EXE都稱之為Assembly,Assembly由AssemblyName來唯一標(biāo)識,AssemblyName也就是大家所熟悉的Assembly.FullName,它是由五部分:名稱、版本、語言、公鑰Token、處理器架構(gòu)組成的,這一點(diǎn)相信大家都知道。有關(guān)Assembly Name的詳細(xì)描述,請參考:https://docs.microsoft.com/en-us/dotnet/framework/app-domains/assembly-names。那么版本,就是AssemblyName中的一個重要組成部分。其它四部分相同,版本如果不同的話,就不能算作是同一個Assembly。設(shè)計這樣一個Assembly的版本策略,微軟本身就是為了解決最開始的DLL Hell的問題,在維基百科上著關(guān)于這段黑歷史的詳細(xì)描述,地址是:https://en.wikipedia.org/wiki/DLL_Hell,在此也就不多啰嗦了。
Assembly版本的重定向和最終確定
.NET下Assembly的加載過程,其實(shí)也是Assembly版本的確定和Assembly文件的定位過程,步驟如下:
在一個Assembly被編譯的時候,它所引用的Assembly的全名(FullName)就會被編譯器強(qiáng)行寫入Assembly的Metadata,這個值是死的,從ILSpy可以看到,每個Reference都有它的全名信息:
例如上圖,System.Data依賴System.Xml,它所需要的版本是4.0.0.0,那么當(dāng)CLR加載System.Data的時候,就可以暫且認(rèn)為接下來需要加載的System.Xml版本是4.0.0.0。這里強(qiáng)調(diào)“暫且認(rèn)為”,是因?yàn)檫@只是確定Assembly版本的第一步,那么最終System.Xml到底是不是使用4.0.0.0的版本呢?就需要看接下來這步的處理結(jié)果,也就是Assembly版本的重定向
首先,檢查應(yīng)用程序的配置文件,看是否存在Assembly版本重定向的設(shè)定。我們暫時先討論應(yīng)用程序配置文件就在AppDomain內(nèi)的情況(如果在AppDomain之外,則需要首先下載配置文件,再繼續(xù),這里先不深入討論)。應(yīng)用程序配置文件常見的有.exe.config和web.config兩種。在配置文件中,可以在runtime節(jié)點(diǎn)下的assemblyBinding中進(jìn)行配置。例如:
在這個例子中,asm6 Assembly的版本號被重定向到2.0.0.0。那么假設(shè)這就是asm6的最終版本號,那么接下來當(dāng)CLR開始加載asm6的時候,如果2.0.0.0的版本沒有找到,則直接拋出FileLoadException(即使3.0.0.0的版本是存在的),整個Assembly加載過程結(jié)束。FileLoadException的詳細(xì)信息類似于:Could not load file or assembly 'asm6, Version=3.0.0.0, Culture=neutral, PublicKeyToken=c0305c36380ba429' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference
如果在配置文件中找到了對應(yīng)的版本重定向設(shè)定,那么,再接著查看Publisher Policy文件。Publisher Policy文件是一個僅包含配置文件的.NET Assembly,被安裝到GAC里。它的Assembly版本重定向配置內(nèi)容跟上面的應(yīng)用程序配置文件的配置內(nèi)容相同,不同的是,它的作用域是所有使用了該Assembly的應(yīng)用程序。這種做法對于開發(fā)系統(tǒng)級通用框架的Assembly升級非常有用,比如.NET Framework。下面就是安裝在GAC里的Publisher Policy文件的樣本,需要注意:Publisher Policy會override應(yīng)用程序配置信息中的版本重定向配置,而不是相反。換言之,假如asm6在上面這一步被確定為2.0.0.0,而所對應(yīng)的Publisher Policy文件又將其確定為2.5.0.0,那么,暫且認(rèn)為,CLR應(yīng)該要加載2.5.0.0的版本。同理,“暫且認(rèn)為”這個詞表示,版本確定的過程還未結(jié)束
接下來,查找machine.config文件。同理,如果machine.config文件中存在版本重定向的設(shè)定,那么就會使用machine.config文件中的這個值,作為CLR應(yīng)該去加載的Assembly的版本
至此,Assembly的最終版本已被確定,接下來就是搜索Assembly文件并進(jìn)行加載的過程了。
Assembly文件的搜索和加載過程
現(xiàn)在,CLR已經(jīng)開始加載確定版本的Assembly了,接下來就是搜索Assembly文件的過程。這個過程也叫作Assembly Probing。CLR會做以下事情:
首先,查看所需的Assembly是否已經(jīng)加載過,如果已經(jīng)加載了,那就直接使用那個已經(jīng)加載的Assembly的版本與當(dāng)前所需的版本進(jìn)行比對,如果匹配,則使用那個已經(jīng)加載的Assembly,如果不匹配,則拋出FileLoadException,執(zhí)行結(jié)束
然后,看Assembly是否已被強(qiáng)簽名(Strongly Named),如果是,則去GAC里查找Assembly。如果找到,則直接加載,整個Assembly加載過程結(jié)束。如果沒有找到,那么就進(jìn)行下一步,繼續(xù)搜索Assembly文件。當(dāng)然,如果Assembly沒有進(jìn)行強(qiáng)簽名,那么就跳過這一步,直接繼續(xù)
接著,CLR開始搜索(Probing)可能的Assembly位置,這又要分多種情況:
首先,查看文件中是否有指定<codeBase>,codeBase配置允許應(yīng)用程序針對Assembly的不同版本指定裝載地址,遵循如下規(guī)律:
如果所指定的Assembly文件位于當(dāng)前應(yīng)用程序域的啟動目錄(或其子目錄)下,則使用相對路徑指定href的值
如果所指定的Assembly文件位于其它目錄,或任何其它地方,則href必須給出全路徑,并且Assembly必須強(qiáng)簽名的
然后,CLR對應(yīng)用程序域的根目錄以及相關(guān)的子目錄進(jìn)行探索:
假設(shè)Assembly的名字是abc.dll,那么CLR會探索以下目錄:
[appdomain_base]\abc.dll
[appdomain_base]\abc\abc.dll
假設(shè)abc.dll還有語言設(shè)置(culture不是neutral),那么CLR會探索以下目錄:
[appdomain_base]\[culture]\abc.dll
[appdomain_base]\[culture]\abc\abc.dll
如果找到符合版本的Assembly,則加載,否則進(jìn)入下一步
最后,CLR會查看應(yīng)用程序配置文件中是否有<probling>節(jié)點(diǎn),如果有,則按probling節(jié)點(diǎn)所指定的privatePath值進(jìn)行逐一探索。這個過程也會考慮culture的因素,類似于上面這步這樣,對相應(yīng)的子目錄進(jìn)行搜索。如果找到對應(yīng)的Assembly,則加載,否則拋出FileLoadException,整個加載過程結(jié)束。注意,這里“逐一探索”的過程,不是遍歷并找最佳匹配的過程。CLR僅根據(jù)Assembly的名字(不帶版本號的名字)在privatePath下查找Assembly的文件,找到第一個名字匹配但是版本不匹配的話,就拋異常并終止加載了,它不會繼續(xù)搜索privatePath中余下的其它路徑
在加載Assembly文件失敗的時候,AppDomain會觸發(fā)AssemblyResolve的事件,在這個事件的訂閱函數(shù)中,允許客戶程序自定義對加載失敗的Assembly的處理方式,比如,可以通過Assembly.LoadFrom或者Assembly.LoadFile調(diào)用“手動地”將Assembly加載到AppDomain。
fuslogvw Assembly綁定日志查看器
在.NET SDK中帶了一個fuslogvw.exe的應(yīng)用程序,通過它可以查看詳細(xì)的Assembly加載過程。使用方法非常簡單,使用管理員身份啟動Visual Studio 2017 Developer Command Prompt,然后在命令行輸入fuslogvw.exe,即可啟動日志查看器。啟動之后,點(diǎn)擊Settings按鈕,以啟用日志記錄功能:
日志啟動之后,點(diǎn)擊Refresh按鈕,然后啟動你的.NET應(yīng)用程序,就可以看到當(dāng)前應(yīng)用程序所依賴的Assembly的加載過程日志了:
接下來,我會做一個例子程序,然后使用這個工具來分析Assembly的加載過程。
插件系統(tǒng)的實(shí)現(xiàn)與Assembly加載過程的分析
理論結(jié)合實(shí)際,看看如何通過實(shí)際代碼來詮釋以上所述Assembly的加載過程。一個比較好的例子就是設(shè)計一個簡單的插件系統(tǒng),并通過觀察系統(tǒng)加載插件的過程,來了解Assembly加載的來龍去脈。為了簡單直觀,我把這個插件系統(tǒng)稱為PluginDemo。這個插件很簡單,主體程序是一個控制臺應(yīng)用程序,然后我們實(shí)現(xiàn)兩個插件:Earth和Mars,在不同的插件的Initialize方法中,會輸出不同的字符串。
整個應(yīng)用程序的項(xiàng)目結(jié)構(gòu)如下:
該插件系統(tǒng)包含4個C#的項(xiàng)目:
PluginDemo.Common:它定義了AddIn抽象類,所有的插件實(shí)現(xiàn)都需要繼承于這個抽象類。此外,AddInDefinition類是一個用來保存插件Metadata的類。為了演示,插件的Metadata僅僅包含插件類型的Assembly Qualified Name
PluginDemo.App:插件系統(tǒng)的應(yīng)用程序。這個程序執(zhí)行的時候,會掃描程序目錄下Modules目錄中的DLL,并根據(jù)module.xml的Metadata信息,加載相應(yīng)的插件對象,并執(zhí)行Initialize方法
PluginDemo.Plugins.Earth:其中的一個插件實(shí)現(xiàn)
PluginDemo.Plugins.Mars:另一個插件實(shí)現(xiàn)
注意:除了PluginDemo.Common之外的其它三個項(xiàng)目,都對PluginDemo.Common有引用關(guān)系。而PluginDemo.App項(xiàng)目僅僅在項(xiàng)目本身依賴于PluginDemo.Plugins.Earth和PluginDemo.Plugins.Mars,它不會去引用這兩個項(xiàng)目。目的就是為了當(dāng)PluginDemo.App被編譯時,其余兩個插件項(xiàng)目也會同時被編譯并輸出到指定位置。
在Earth插件的CustomAddIn類中,我們實(shí)現(xiàn)了Initialize方法,并在此輸出一個字符串:
| public?class?CustomAddIn : AddIn{????public?override?string?Name => "Earth AddIn";????public?override?void?Initialize()????{????????Console.WriteLine("Earth Plugin initialized.");????}} |
在Mars插件的CustomAddIn類中,我們也實(shí)現(xiàn)了Initialize方法,并在此輸出一個字符串:
| public?class?CustomAddIn : AddIn{????public?override?string?Name => "Mars AddIn";????public?override?void?Initialize()????{????????Console.WriteLine("Mars AddIn initialized.");????}} |
那么,在插件系統(tǒng)主程序中,就會掃描Modules子目錄下的module.xml文件,然后解析每個module.xml文件獲得每個插件類的Assembly Qualified Name,然后通過Type.GetType方法獲得插件類,進(jìn)而創(chuàng)建實(shí)例、調(diào)用Initialize方法。代碼如下:
| static?void?Main(){????var?directory = new?DirectoryInfo("Modules");????foreach(var?file in?directory.EnumerateFiles("module.xml", SearchOption.AllDirectories))????{????????var?addinDefinition = AddInDefinition.ReadFromFile(file.FullName);????????var?addInType = Type.GetType(addinDefinition.FullName);????????var?addIn = (AddIn)Activator.CreateInstance(addInType);????????Console.WriteLine($"{addIn.Id} - {addIn.Name}");????????addIn.Initialize();????}} |
接下來,修改App.config文件,修改為:
| <?xml?version="1.0" encoding="utf-8" ?><configuration>??<runtime>????<assemblyBinding?xmlns="urn:schemas-microsoft-com:asm.v1">??????<probing?privatePath="Modules\Earth;Modules\Mars;" />????</assemblyBinding>??</runtime></configuration> |
此時,運(yùn)行程序,可以得到:
目前沒有什么問題。接下來,對兩個AddIn分別做一些修改。讓這兩個AddIn依賴于不同版本的Newtonsoft.Json,比如,Earth依賴于7.0.0.0的版本,Mars依賴于6.0.0.0的版本,然后分別修改兩個CustomAddIn的Initialize方法,在方法中各自調(diào)用一次JsonConvert.SerializeObject方法,以觸發(fā)Newtonsoft.Json這個Assembly的加載。此時再次運(yùn)行程序,你將看到下面的異常:
現(xiàn)在,刷新fuslogvw.exe,找到Newtonsoft.Json的日志:
雙擊打開日志,可以看到如下信息:
從整個過程可以看出:
PluginDemo.App.exe正在試圖加載PluginDemo.Plugins.Mars Assembly
PluginDemo.Plugins.Mars開始調(diào)用Newtonsoft.Json
掃描應(yīng)用程序配置文件、Host配置文件以及machine.config文件,均無找到Newtonsoft.Json的重定向信息,此時,Newtonsoft.Json版本確定為6.0.0.0
GAC掃描失敗,繼續(xù)查找文件
首先查找應(yīng)用程序當(dāng)前目錄下有沒有Newtonsoft.Json,以及Newtonsoft.Json子目錄下有沒有Newtonsoft.Json.dll,發(fā)現(xiàn)都沒有,繼續(xù)
然后,通過App.config中的probing的privatePath設(shè)定,首先查找Modules\Earth目錄(因?yàn)檫@個目錄放在privatePath的第一個),找到了一個叫做Newtonsoft.Json.dll的Assembly,于是,判斷版本是否相同。結(jié)果,找到的是7.0.0.0,而它需要的卻是6.0.0.0,版本不匹配,于是就拋出異常,退出程序
那么接下來,改一改App.config文件,將privatePath下的兩個值換個位置呢?
再試試:
此時,Earth AddIn又出錯了。那么,我們加上版本重定向的配置,指定當(dāng)程序需要加載7.0.0.0版本的Newtonsoft.Json時,讓它重定向到6.0.0.0的版本:
再次執(zhí)行,成功了:
看看日志:
版本已經(jīng)被重定向到6.0.0.0,并且在Mars目錄下找到了6.0.0.0的Newtonsoft.Json,加載成功了。
這個案例的源代碼可以點(diǎn)擊此處下載。
總結(jié)
本文詳細(xì)介紹了.NET下Assembly的版本確定和加載過程,最后給出了一個實(shí)例,對這個過程進(jìn)行了演示。
原文:https://www.cnblogs.com/daxnet/p/8525249.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結(jié)
以上是生活随笔為你收集整理的重温.NET下Assembly的加载过程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 枚举特性 FlagAttribut
- 下一篇: 创建基于MailKit和MimeKit的