.NET 中部署的基本單元,也是所有類(lèi)型的容器。程序集包含已編譯的類(lèi)型及其中間語(yǔ)言 (IL) 代碼、運(yùn)行時(shí)資源和信息,以幫助進(jìn)行版本控制和引用其他程序集。程序集還定義類(lèi)型解析的邊界。在 .NET 中,程序集由擴(kuò)展名為的單個(gè)文件組成。
在 .NET 中生成可執(zhí)行應(yīng)用程序時(shí),最終會(huì)得到兩個(gè)文件:程序集 () 和適用于目標(biāo)平臺(tái)的可執(zhí)行啟動(dòng)器 ()。
這與 .NET Framework 中發(fā)生的情況不同,后者生成可 (PE) 程序集。PE 具有擴(kuò)展,并充當(dāng)程序集和應(yīng)用程序啟動(dòng)器。PE 可以同時(shí)面向 32 位和 64 位版本的 Windows。
本章中的大多數(shù)類(lèi)型都來(lái)自以下命名空間:
System.Reflection
System.Resources
System.Globalization
程序集包含四種內(nèi)容:
程序集清單
向 CLR 提供信息,例如程序集的名稱(chēng)、版本及其引用的其他程序集
應(yīng)用程序清單
向操作系統(tǒng)提供信息,例如應(yīng)如何部署程序集以及是否需要管理提升
已編譯的類(lèi)型
程序集中定義的類(lèi)型的已編譯 IL 代碼和元數(shù)據(jù)
資源
嵌入在程序集中的其他數(shù)據(jù),如圖像和可本地化的文本
其中,只有程序集清單是必需的,盡管程序集幾乎總是包含已編譯的類(lèi)型(除非它是資源)。參見(jiàn))。
程序集清單有兩個(gè)用途:
因此,程序集是。使用者可以發(fā)現(xiàn)程序集的所有數(shù)據(jù)、類(lèi)型和函數(shù),而無(wú)需其他文件。
程序集清單不是顯式添加到程序集的內(nèi)容,而是作為編譯的一部分自動(dòng)嵌入到程序集中。
以下是清單中存儲(chǔ)的功能重要數(shù)據(jù)的摘要:
清單還可以存儲(chǔ)以下信息數(shù)據(jù):
其中一些數(shù)據(jù)派生自提供給編譯器的參數(shù),例如引用的程序集列表或用于對(duì)程序集進(jìn)行簽名的公鑰。其余部分來(lái)自括號(hào)中指示的程序集屬性。
可以使用 .NET 工具 查看程序集清單的內(nèi)容。在第中,我們描述了如何使用反射以編程方式執(zhí)行相同的操作。
可以在 Visual Studio 中項(xiàng)目的“屬性”頁(yè)上的“包”選項(xiàng)卡上指定常用程序集屬性。該選項(xiàng)卡上的設(shè)置將添加到項(xiàng)目文件 () 中。
若要指定“包”選項(xiàng)卡不支持的屬性,或者如果不使用 文件,可以在源代碼中指定程序集屬性(這通常在名為 的文件中完成)。
專(zhuān)用屬性文件僅包含 using 語(yǔ)句和程序集屬性聲明。例如,若要向單元測(cè)試項(xiàng)目公開(kāi)內(nèi)部作用域的類(lèi)型,應(yīng)執(zhí)行以下操作:
using System.Runtime.CompilerServices;
[assembly:InternalsVisibleTo("MyUnitTestProject")]
應(yīng)用程序清單是一個(gè) XML 文件,用于將有關(guān)程序集的信息傳達(dá)給操作系統(tǒng)。在生成過(guò)程中,應(yīng)用程序清單作為 Win32 資源嵌入到啟動(dòng)可執(zhí)行文件中。如果存在,則在 CLR 加載程序集之前讀取和處理清單,并可能影響 Windows 啟動(dòng)應(yīng)用程序進(jìn)程的方式。
.NET 應(yīng)用程序清單在 XML 命名空間 urn:schemas-microsoft-com:asm.v1 中具有一個(gè)名為程序集的根元素:
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- contents of manifest -->
</assembly>
以下清單指示操作系統(tǒng)請(qǐng)求管理提升:
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
(UWP 應(yīng)用程序具有更詳細(xì)的清單,如 文件中所述。這包括程序功能的聲明,這些功能決定了操作系統(tǒng)授予的權(quán)限。編輯此文件的最簡(jiǎn)單方法是使用 Visual Studio,當(dāng)您雙擊清單文件時(shí),它會(huì)顯示一個(gè)對(duì)話(huà)框。
可以通過(guò)在“解決方案資源管理器”中右鍵單擊項(xiàng)目,依次選擇“添加”、“新建項(xiàng)”,然后選擇“應(yīng)用程序清單文件”,將應(yīng)用程序清單添加到 Visual Studio 中的 .NET 項(xiàng)目中。生成后,清單將嵌入到輸出程序集中。
.NET 工具 對(duì)嵌入式應(yīng)用程序清單的存在視而不見(jiàn)。但是,Visual Studio 指示如果在解決方案資源管理器中雙擊程序集,則嵌入的應(yīng)用程序清單是否存在。
程序集的內(nèi)容實(shí)際上打包在稱(chēng)為的中間容器中。模塊對(duì)應(yīng)于包含程序集內(nèi)容的文件。這個(gè)額外的容器層的原因是允許程序集跨越多個(gè)文件,這是.NET Framework中存在但在.NET 5和.NET Core中不存在的功能。 說(shuō)明了這種關(guān)系。
盡管 .NET 不支持多文件程序集,但有時(shí)需要注意模塊施加的額外容器交付級(jí)別。主要場(chǎng)景是反射(請(qǐng)參閱中的“”和)。
System.Reflection 中的程序集類(lèi)是在運(yùn)行時(shí)訪問(wèn)程序集元數(shù)據(jù)的網(wǎng)關(guān)。有多種方法可以獲取程序集對(duì)象:最簡(jiǎn)單的方法是通過(guò) Type 的程序集屬性:
Assembly a = typeof (Program).Assembly;
您還可以通過(guò)調(diào)用 Assembly 的靜態(tài)方法之一來(lái)獲取程序集對(duì)象:
GetExecutingAssembly
返回定義當(dāng)前正在執(zhí)行的函數(shù)的類(lèi)型的程序集
獲取呼叫程序集
與 GetExecutingAssembly 執(zhí)行相同的操作,但針對(duì)調(diào)用當(dāng)前執(zhí)行函數(shù)的函數(shù)
獲取條目程序集
返回定義應(yīng)用程序的原始輸入方法的程序集
擁有程序集對(duì)象后,可以使用其屬性和方法來(lái)查詢(xún)程序集的元數(shù)據(jù)并反映其類(lèi)型。 顯示了這些功能的摘要。
大會(huì)成員 | ||
功能 | 目的 | 請(qǐng)參閱該部分... |
全名 , 獲取名稱(chēng) | 返回完全限定名或程序集名稱(chēng)對(duì)象 | “程序集名稱(chēng)” |
代碼庫(kù) , 位置 | 程序集文件的位置 | “加載、解析和隔離程序集” |
加載 , 加載自 , | 手動(dòng)將程序集加載到內(nèi)存中 | “加載、解析和隔離程序集” |
獲取衛(wèi)星組裝 | 定位給定區(qū)域性的附屬程序集 | “資源和衛(wèi)星組件” |
GetType , GetTypes | 返回程序集中定義的一個(gè)或多個(gè)類(lèi)型。 | |
入口點(diǎn) | 返回應(yīng)用程序的輸入方法,作為 MethodInfo | |
GetModule , GetModules , ManifestModule | 返回程序集的所有模塊或主模塊 | 中的 |
GetCustomAttribute, GetCustomAttributes | 返回程序集的屬性 | 中的 |
在 .NET Framework 中,強(qiáng)命名程序集非常重要,原因有兩個(gè):
強(qiáng)命名在 .NET 5 和 .NET Core 中的重要性要低得多,因?yàn)檫@些運(yùn)行時(shí)沒(méi)有全局程序集緩存,也不會(huì)施加第二個(gè)限制。
程序集具有唯一的標(biāo)識(shí)。它的工作原理是向清單添加兩個(gè)元數(shù)據(jù)位:
這需要公鑰/私鑰對(duì)。提供唯一的標(biāo)識(shí)號(hào),便于簽名。
簽名與簽名不同。我們將在本章后面介紹 Authenticode。
公鑰在保證程序集引用的唯一性方面很有價(jià)值:強(qiáng)名稱(chēng)程序集將公鑰合并到其標(biāo)識(shí)中。
在 .NET Framework 中,私鑰可防止程序集被篡改,因?yàn)槿绻麤](méi)有私鑰,任何人都無(wú)法在不破壞簽名的情況下發(fā)布程序集的修改版本。實(shí)際上,當(dāng)將程序集加載到 .NET Framework 的全局程序集緩存中時(shí),這會(huì)很有用。在 .NET 5 和 .NET Core 中,簽名幾乎沒(méi)有用處,因?yàn)樗鼜奈幢贿x中。
向以前“弱”命名的程序集添加強(qiáng)名稱(chēng)會(huì)更改其標(biāo)識(shí)。出于這個(gè)原因,如果您認(rèn)為程序集將來(lái)可能需要強(qiáng)名稱(chēng),從一開(kāi)始就對(duì)程序集進(jìn)行強(qiáng)名稱(chēng)是值得的。
要為程序集指定一個(gè)強(qiáng)名稱(chēng),請(qǐng)首先使用 實(shí)用程序生成公鑰/私鑰對(duì):
sn.exe -k MyKeyPair.snk
Visual Studio 安裝一個(gè)名為 的快捷方式,該快捷方式啟動(dòng)一個(gè)命令提示符,其 PATH 包含 等開(kāi)發(fā)工具。
這將生成一個(gè)新的密鑰對(duì),并將其存儲(chǔ)到名為的文件中。如果隨后丟失此文件,將永久失去使用相同的標(biāo)識(shí)重新編譯程序集的能力。
可以通過(guò)更新項(xiàng)目文件來(lái)使用此文件對(duì)程序集進(jìn)行簽名。從 Visual Studio 轉(zhuǎn)到“項(xiàng)目屬性”窗口,然后在“簽名”選項(xiàng)卡上,選中“對(duì)程序集進(jìn)行”復(fù)選框并選擇 文件。
同一密鑰對(duì)可以對(duì)多個(gè)程序集進(jìn)行簽名 - 如果它們的簡(jiǎn)單名稱(chēng)不同,它們?nèi)詫⒕哂胁煌臉?biāo)識(shí)。
程序集的“標(biāo)識(shí)”包含其清單中的四個(gè)元數(shù)據(jù):
簡(jiǎn)單名稱(chēng)不是來(lái)自任何屬性,而是來(lái)自最初編譯到的文件的名稱(chēng)(減去任何擴(kuò)展名)。因此,程序集的簡(jiǎn)單名稱(chēng)是“System.Xml”。重命名文件不會(huì)更改程序集的簡(jiǎn)單名稱(chēng)。
版本號(hào)來(lái)自 AssemblyVersion 屬性。它是一個(gè)字符串,分為四個(gè)部分,如下所示:
major.minor.build.revision
您可以指定版本號(hào),如下所示:
[assembly: AssemblyVersion ("2.5.6.7")]
區(qū)域性來(lái)自 AssemblyCulture 屬性,適用于附屬程序集,稍后將在一節(jié)中介紹。
公鑰標(biāo)記來(lái)自編譯時(shí)提供的強(qiáng)名稱(chēng),如上一節(jié)所述。
完全限定的程序集名稱(chēng)是包含所有四個(gè)標(biāo)識(shí)組件的字符串,格式如下:
simple-name, Version=version, Culture=culture, PublicKeyToken=public-key
例如,System.Private.CoreLib.dll 的完全限定名稱(chēng)是 。
如果程序集沒(méi)有 AssemblyVersion 屬性,則版本顯示為 0.0.0.0 。如果未簽名,則其公鑰標(biāo)記顯示為 null 。
程序集對(duì)象的 FullName 屬性返回其完全限定名稱(chēng)。編譯器在清單中記錄程序集引用時(shí)始終使用完全限定的名稱(chēng)。
完全限定的程序集名稱(chēng)不包括目錄路徑,以幫助在磁盤(pán)上查找它。查找駐留在另一個(gè)目錄中的程序集是我們?cè)谥刑幚淼耐耆?dú)立的問(wèn)題。
AssemblyName 是一個(gè)類(lèi),對(duì)于完全限定程序集名稱(chēng)的四個(gè)組件中的每一個(gè)組件都有一個(gè)類(lèi)型化屬性。程序集名稱(chēng)有兩個(gè)用途:
可以通過(guò)以下任一方式獲取程序集名稱(chēng)對(duì)象:
還可以實(shí)例化不帶任何參數(shù)的 AssemblyName 對(duì)象,然后設(shè)置其每個(gè)屬性以生成完全限定名。以這種方式構(gòu)造時(shí),程序集名稱(chēng)是可變的。
以下是它的基本屬性和方法:
string FullName { get; } // Fully qualified name
string Name { get; set; } // Simple name
Version Version { get; set; } // Assembly version
CultureInfo CultureInfo { get; set; } // For satellite assemblies
string CodeBase { get; set; } // Location
byte[] GetPublicKey(); // 160 bytes
void SetPublicKey (byte[] key);
byte[] GetPublicKeyToken(); // 8-byte version
void SetPublicKeyToken (byte[] publicKeyToken);
版本本身是一種強(qiáng)類(lèi)型表示形式,具有“主要”、“次要”、“內(nèi)部版本”和“修訂號(hào)”的屬性。GetPublicKey 返回完整的加密公鑰;GetPublicKeyToken 返回用于建立標(biāo)識(shí)的最后八個(gè)字節(jié)。
使用程序集名稱(chēng)獲取程序集的簡(jiǎn)單名稱(chēng):
Console.WriteLine (typeof (string).Assembly.GetName().Name);
// System.Private.CoreLib
獲取程序集版本:
string v = myAssembly.GetName().Version.ToString();
另外兩個(gè)程序集屬性可用于表示與版本相關(guān)的信息。與AssemblyVersion不同,以下兩個(gè)屬性不會(huì)影響程序集的標(biāo)識(shí),因此對(duì)編譯時(shí)或運(yùn)行時(shí)發(fā)生的情況沒(méi)有影響:
匯編信息版本
向最終用戶(hù)顯示的版本。這在“Windows 文件屬性”對(duì)話(huà)框中顯示為“產(chǎn)品版本”。任何字符串都可以轉(zhuǎn)到此處,例如“5.1 Beta 2”。通常,將為應(yīng)用程序中的所有程序集分配相同的信息版本號(hào)。
匯編文件版本
這旨在引用該程序集的內(nèi)部版本號(hào)。這在“Windows 文件屬性”對(duì)話(huà)框中顯示為“文件版本”。與AssemblyVersion一樣,它必須包含一個(gè)由最多四個(gè)數(shù)字組成的字符串,這些數(shù)字由句點(diǎn)分隔。
是一種代碼簽名系統(tǒng),其目的是證明發(fā)布者的身份。驗(yàn)證碼和簽名是獨(dú)立的:您可以使用其中一個(gè)或兩個(gè)系統(tǒng)對(duì)程序集進(jìn)行簽名。
盡管強(qiáng)名稱(chēng)簽名可以證明程序集 A、B 和 C 來(lái)自同一參與方(假設(shè)私鑰未泄露),但它無(wú)法告訴您該參與方是誰(shuí)。要知道派對(duì)是喬·阿爾巴哈里(Joe Albahari)或Microsoft公司(Joe Albahari),你需要Authenticode。
從 Internet 下載程序時(shí),驗(yàn)證碼很有用,因?yàn)樗梢员WC程序來(lái)自證書(shū)頒發(fā)機(jī)構(gòu)指定的任何人,并且在傳輸過(guò)程中未被修改。它還可以防止首次運(yùn)行下載的應(yīng)用程序時(shí)出現(xiàn)“未知發(fā)布者”警告。將應(yīng)用提交到 Windows 應(yīng)用商店時(shí),驗(yàn)證碼簽名也是一項(xiàng)要求。
驗(yàn)證碼不僅適用于 .NET 程序集,還適用于非托管可執(zhí)行文件和二進(jìn)制文件(如部署文件)。當(dāng)然,Authenticode 并不能保證程序沒(méi)有惡意軟件,盡管它確實(shí)降低了它的可能性。個(gè)人或?qū)嶓w愿意將其名稱(chēng)(由護(hù)照或公司文件支持)放在可執(zhí)行文件或庫(kù)后面。
CLR 不會(huì)將驗(yàn)證碼簽名視為程序集標(biāo)識(shí)的一部分。但是,它可以按需讀取和驗(yàn)證驗(yàn)證碼簽名,您很快就會(huì)看到。
使用 Authenticode 簽名要求您聯(lián)系 (CA),并提供您的個(gè)人身份或公司身份(公司章程等)的證據(jù)。CA 檢查您的文檔后,它將頒發(fā)一個(gè) X.509 代碼簽名證書(shū),該證書(shū)的有效期通常為一到五年。這使您能夠使用 實(shí)用程序?qū)Τ绦蚣M(jìn)行簽名。您也可以使用 實(shí)用程序自己制作證書(shū);但是,只有在顯式安裝了證書(shū)的計(jì)算機(jī)上才能識(shí)別它。
(非自簽名)證書(shū)可以在任何計(jì)算機(jī)上工作的事實(shí)依賴(lài)于基礎(chǔ)結(jié)構(gòu)。實(shí)質(zhì)上,您的證書(shū)是使用屬于 CA 的另一個(gè)證書(shū)簽名的。CA 是受信任的,因?yàn)樗?CA 都加載到操作系統(tǒng)中。 (轉(zhuǎn)到 Windows 控制面板,然后在搜索框中鍵入。在“管理工具”部分中,單擊“管理計(jì)算機(jī)證書(shū)”。這將啟動(dòng)證書(shū)管理器。打開(kāi)“受信任的根證書(shū)頒發(fā)機(jī)構(gòu)”節(jié)點(diǎn),然后單擊“證書(shū)”。如果泄露,CA 可以吊銷(xiāo)發(fā)布者的證書(shū),因此驗(yàn)證驗(yàn)證碼簽名需要定期向 CA 詢(xún)問(wèn)證書(shū)吊銷(xiāo)的最新列表。
由于驗(yàn)證碼使用加密簽名,因此如果隨后有人篡改文件,驗(yàn)證碼簽名無(wú)效。我們將在第 中討論加密、散列和簽名。
第一步是從 CA 獲取代碼簽名證書(shū)(請(qǐng)參閱下面的側(cè)欄)。然后,可以將證書(shū)作為受密碼保護(hù)的文件使用,也可以將證書(shū)加載到計(jì)算機(jī)的證書(shū)存儲(chǔ)中。執(zhí)行后者的好處是,您無(wú)需指定密碼即可簽名。這是有利的,因?yàn)樗梢苑乐姑艽a在自動(dòng)生成腳本或批處理文件中可見(jiàn)。
只有少數(shù)代碼簽名 CA 作為根證書(shū)頒發(fā)機(jī)構(gòu)預(yù)加載到 Windows 中。其中包括Comodo,Go Daddy,GlobalSign,DigiCert,Thawte和Symantec。
還有一些經(jīng)銷(xiāo)商,如K Software,提供上述當(dāng)局的折扣代碼簽名證書(shū)。
由K Software,Comodo,Go Daddy和GlobalSign頒發(fā)的Authenticode證書(shū)被宣傳為限制較少,因?yàn)樗鼈円矊⒑炇鸱荕icrosoft程序。除此之外,所有供應(yīng)商的產(chǎn)品在功能上都是等效的。
請(qǐng)注意,SSL 證書(shū)通常不能用于驗(yàn)證碼簽名(盡管使用相同的 X.509 基礎(chǔ)結(jié)構(gòu))。這部分是因?yàn)镾SL證書(shū)是關(guān)于證明域所有權(quán)的;Authenticode是關(guān)于證明你是誰(shuí)。
若要將證書(shū)加載到計(jì)算機(jī)的證書(shū)存儲(chǔ)中,請(qǐng)按照前面所述打開(kāi)證書(shū)管理器。打開(kāi)“個(gè)人”文件夾,右鍵單擊其“證書(shū)”文件夾,然后選擇“所有任務(wù)/導(dǎo)入”。導(dǎo)入向?qū)⒅笇?dǎo)您完成此過(guò)程。導(dǎo)入完成后,單擊證書(shū)上的“查看”按鈕,轉(zhuǎn)到“詳細(xì)信息”選項(xiàng)卡,然后復(fù)制證書(shū)的。這是隨后在簽名時(shí)需要標(biāo)識(shí)證書(shū)的 SHA-256 哈希。
如果還希望對(duì)程序集進(jìn)行強(qiáng)名稱(chēng)簽名,則必須在驗(yàn)證碼簽名執(zhí)行此操作。這是因?yàn)?CLR 知道驗(yàn)證碼簽名,但反之則不然。因此,如果在對(duì)程序集進(jìn)行驗(yàn)證碼簽名程序集進(jìn)行強(qiáng)名稱(chēng)簽名,則后者會(huì)將添加 CLR 的強(qiáng)名稱(chēng)視為未經(jīng)授權(quán)的修改,并認(rèn)為程序集。
您可以使用 Visual Studio 附帶的 實(shí)用工具對(duì)程序進(jìn)行驗(yàn)證簽名(查看”下的 文件夾)。下面使用安全的 SHA256 哈希算法對(duì)名為 的文件進(jìn)行簽名.exe該文件的證書(shū)位于計(jì)算機(jī)的“中,名為“Joseph Albahari”:
signtool sign /n "Joseph Albahari" /fd sha256 LINQPad.exe
您還可以使用 /d 和 /du 指定描述和產(chǎn)品 URL:
... /d LINQPad /du http://www.linqpad.net
在大多數(shù)情況下,您還需要指定。
證書(shū)過(guò)期后,您將無(wú)法再對(duì)程序進(jìn)行簽名。但是,如果在簽名時(shí)使用 /tr 開(kāi)關(guān)指定了,則在過(guò)期簽名的程序仍將有效。CA將為此目的為您提供一個(gè)URI:以下內(nèi)容適用于Comodo(或K軟件):
... /tr http://timestamp.comodoca.com/authenticode /td SHA256
查看文件驗(yàn)證碼簽名的最簡(jiǎn)單方法是在 Windows 資源管理器中查看文件的屬性(在“數(shù)字簽名”選項(xiàng)卡中查看)。實(shí)用程序也為此提供了一個(gè)選項(xiàng)。
應(yīng)用程序通常不僅包含可執(zhí)行代碼,還包含文本、圖像或 XML 文件等內(nèi)容。此類(lèi)內(nèi)容可以通過(guò)在程序集中表示。資源有兩個(gè)重疊的用例:
程序集資源最終是具有名稱(chēng)的字節(jié)流。您可以將程序集視為包含按字符串鍵控的字節(jié)數(shù)組字典。如果在 ildasm 中反匯編包含名為 的資源的程序集.jpg 和名為 的資源,則可以在 中看到這一點(diǎn):
.mresource public banner.jpg
{
// Offset: 0x00000F58 Length: 0x000004F6
}
.mresource public data.xml
{
// Offset: 0x00001458 Length: 0x0000027E
}
在本例中,和數(shù)據(jù)直接包含在程序集中 — 每個(gè)都作為其自己的嵌入資源。這是最簡(jiǎn)單的工作方式。
.NET 還允許您通過(guò)中間 容器添加內(nèi)容。這些旨在保存可能需要翻譯成不同語(yǔ)言的內(nèi)容。本地化的 可以打包為單獨(dú)的附屬程序集,這些附屬程序集在運(yùn)行時(shí)根據(jù)用戶(hù)的操作系統(tǒng)語(yǔ)言自動(dòng)選取。
演示了一個(gè)程序集,其中包含兩個(gè)直接嵌入的資源,以及一個(gè)名為 容器,我們?yōu)槠鋭?chuàng)建了兩個(gè)本地化的附屬服務(wù)器。
在 Window 應(yīng)用商店應(yīng)用中不支持將資源嵌入到程序集中。相反,請(qǐng)將任何額外的文件添加到部署包中,并通過(guò)從應(yīng)用程序 StorageFolder ( Package.Current.InstalledLocation ) 讀取來(lái)訪問(wèn)它們。
要使用 Visual Studio 直接嵌入資源,請(qǐng)執(zhí)行以下操作:
Visual Studio 始終在資源名稱(chēng)前面加上項(xiàng)目的默認(rèn)命名空間,以及包含該文件的任何子文件夾的名稱(chēng)。因此,如果項(xiàng)目的默認(rèn)命名空間是 Westwind.Reports 并且您的文件稱(chēng)為 .jpg則在文件夾中,資源名稱(chēng)將為 。
資源名稱(chēng)區(qū)分大小寫(xiě)。這使得 Visual Studio 中包含資源的項(xiàng)目子文件夾名稱(chēng)有效地區(qū)分大小寫(xiě)。
若要檢索資源,請(qǐng)?jiān)诎撡Y源的程序集上調(diào)用 GetManifestResourceStream。這將返回一個(gè)流,然后您可以像任何其他流一樣讀取該流:
Assembly a = Assembly.GetEntryAssembly();
using (Stream s = a.GetManifestResourceStream ("TestProject.data.xml"))
using (XmlReader r = XmlReader.Create (s))
...
System.Drawing.Image image;
using (Stream s = a.GetManifestResourceStream ("TestProject.banner.jpg"))
image = System.Drawing.Image.FromStream (s);
返回的流是可搜索的,因此您也可以執(zhí)行以下操作:
byte[] data;
using (Stream s = a.GetManifestResourceStream ("TestProject.banner.jpg"))
data = new BinaryReader (s).ReadBytes ((int) s.Length);
如果使用 Visual Studio 嵌入資源,則必須記住包含基于命名空間的前綴。為了幫助避免錯(cuò)誤,可以使用在單獨(dú)的參數(shù)中指定前綴。類(lèi)型的命名空間用作前綴:
using (Stream s = a.GetManifestResourceStream (typeof (X), "data.xml"))
X 可以是具有所需資源命名空間的任何類(lèi)型(通常是同一項(xiàng)目文件夾中的類(lèi)型)。
在 Visual Studio 中將項(xiàng)目項(xiàng)的生成操作設(shè)置為 Windows Presentation Foundation (WPF) 應(yīng)用程序中的資源與將其生成操作設(shè)置為“嵌入的資源”不同。前者實(shí)際上將該項(xiàng)添加到名為 的 文件中,該文件的內(nèi)容通過(guò) WPF 的應(yīng)用程序類(lèi)訪問(wèn),使用 URI 作為鍵。
為了增加混淆,WPF 進(jìn)一步重載了術(shù)語(yǔ)“資源”。資源和動(dòng)態(tài)資源都與程序集無(wú)關(guān)!
GetManifestResourceNames 返回程序集中所有資源的名稱(chēng)。
文件是潛在可本地化內(nèi)容的容器。 文件最終會(huì)成為程序集中的嵌入資源,就像任何其他類(lèi)型的文件一樣。不同之處在于您必須執(zhí)行以下操作:
文件以二進(jìn)制形式構(gòu)建,因此不可人工編輯;因此,您必須依靠 .NET 和 Visual Studio 提供的工具來(lái)處理它們。字符串或簡(jiǎn)單數(shù)據(jù)類(lèi)型的標(biāo)準(zhǔn)方法是使用 .resx 格式,可以通過(guò) Visual Studio 或 工具將其轉(zhuǎn)換為 文件。 格式也適用于用于 Windows 窗體或 ASP.NET 應(yīng)用程序的圖像。
在 WPF 應(yīng)用程序中,必須對(duì)需要由 URI 引用的圖像或類(lèi)似內(nèi)容使用 Visual Studio 的“資源”生成操作。無(wú)論是否需要本地化,這都適用。
我們將在以下各節(jié)中介紹如何執(zhí)行其中的每一個(gè)操作。
文件是用于生成 文件的設(shè)計(jì)時(shí)格式。 文件使用 XML,其結(jié)構(gòu)為名稱(chēng)/值對(duì),如下所示:
<root>
<data name="Greeting">
<value>hello</value>
</data>
<data name="DefaultFontSize" type="System.Int32, mscorlib">
<value>10</value>
</data>
</root>
若要在 Visual Studio 中創(chuàng)建 文件,請(qǐng)?zhí)砑宇?lèi)型為“資源文件”的項(xiàng)目項(xiàng)。其余工作將自動(dòng)完成:
資源設(shè)計(jì)器將圖像添加為類(lèi)型化圖像對(duì)象 (),而不是字節(jié)數(shù)組,這使得它們不適合 WPF 應(yīng)用程序。
如果在 Visual Studio 中創(chuàng)建 文件,則會(huì)自動(dòng)生成一個(gè)同名的類(lèi),其中包含用于檢索其每個(gè)項(xiàng)的屬性。
類(lèi)讀取程序集中嵌入的 文件:
ResourceManager r = new ResourceManager ("welcome",
Assembly.GetExecutingAssembly());
(如果資源是在 Visual Studio 中編譯的,則第一個(gè)參數(shù)必須以命名空間為前綴。
然后,您可以通過(guò)使用強(qiáng)制轉(zhuǎn)換調(diào)用 GetString 或 GetObject 來(lái)訪問(wèn)內(nèi)部?jī)?nèi)容:
string greeting = r.GetString ("Greeting");
int fontSize = (int) r.GetObject ("DefaultFontSize");
Image image = (Image) r.GetObject ("flag.png");
枚舉 文件的內(nèi)容:
ResourceManager r = new ResourceManager (...);
ResourceSet set = r.GetResourceSet (CultureInfo.CurrentUICulture,
true, true);
foreach (System.Collections.DictionaryEntry entry in set)
Console.WriteLine (entry.Key);
在 WPF 應(yīng)用程序中,XAML 文件需要能夠通過(guò) URI 訪問(wèn)資源。例如:
<Button>
<Image Height="50" Source="flag.png"/>
</Button>
或者,如果資源位于另一個(gè)程序集中:
<Button>
<Image Height="50" Source="UtilsAssembly;Component/flag.png"/>
</Button>
(組件是一個(gè)文字關(guān)鍵字。
若要?jiǎng)?chuàng)建可以這種方式加載的資源,不能使用 文件。相反,必須將文件添加到項(xiàng)目中,并將其生成操作設(shè)置為“資源”(而不是“嵌入的資源”)。然后,Visual Studio 將它們編譯為名為 的 . 文件,該文件也是編譯的 XAML () 文件的主頁(yè)。
若要以編程方式加載 URI 鍵資源,請(qǐng)調(diào)用 Application.GetResourceStream:
Uri u = new Uri ("flag.png", UriKind.Relative);
using (Stream s = Application.GetResourceStream (u).Stream)
請(qǐng)注意,我們使用了相對(duì) URI。您還可以使用完全以下格式的絕對(duì) URI(三個(gè)逗號(hào)不是拼寫(xiě)錯(cuò)誤):
Uri u = new Uri ("pack://application:,,,/flag.png");
如果您希望指定程序集對(duì)象,則可以使用資源管理器來(lái)檢索內(nèi)容:
Assembly a = Assembly.GetExecutingAssembly();
ResourceManager r = new ResourceManager (a.GetName().Name + ".g", a);
using (Stream s = r.GetStream ("flag.png"))
...
資源管理器還允許您枚舉給定程序集中 . 容器的內(nèi)容。
中的數(shù)據(jù)是可本地化的。
當(dāng)應(yīng)用程序在構(gòu)建為以不同語(yǔ)言顯示所有內(nèi)容的 Windows 版本上運(yùn)行時(shí),資源本地化是相關(guān)的。為了保持一致性,應(yīng)用程序也應(yīng)使用相同的語(yǔ)言。
典型的設(shè)置如下:
當(dāng)應(yīng)用程序運(yùn)行時(shí),.NET 會(huì)檢查當(dāng)前操作系統(tǒng)的語(yǔ)言(來(lái)自 CultureInfo.CurrentUICulture )。每當(dāng)使用 資源管理器 請(qǐng)求資源時(shí),運(yùn)行時(shí)都會(huì)查找本地化的附屬程序集。如果可用(并且它包含您請(qǐng)求的資源密鑰),則使用該密鑰代替主程序集的版本。
這意味著您只需添加新的附屬組件即可增強(qiáng)語(yǔ)言支持,而無(wú)需更改主程序集。
附屬程序集不能包含可執(zhí)行代碼,只能包含資源。
附屬程序集部署在程序集文件夾的子目錄中
programBaseFolder\MyProgram.exe
\MyLibrary.exe
\XX\MyProgram.resources.dll
\XX\MyLibrary.resources.dll
XX指兩個(gè)字母的語(yǔ)言代碼(例如“de”表示德語(yǔ))或語(yǔ)言和地區(qū)代碼(例如“en-GB”表示英國(guó)的英語(yǔ))。此命名系統(tǒng)允許 CLR 自動(dòng)查找并加載正確的附屬程序集。
回想一下我們之前的 示例,其中包括以下內(nèi)容:
<root>
...
<data name="Greeting"
<value>hello</value>
</data>
</root>
然后,我們?cè)谶\(yùn)行時(shí)檢索問(wèn)候語(yǔ),如下所示:
ResourceManager r = new ResourceManager ("welcome",
Assembly.GetExecutingAssembly());
Console.Write (r.GetString ("Greeting"));
假設(shè)我們希望它改為在德語(yǔ)版本的Windows上運(yùn)行時(shí)寫(xiě)“hallo”。第一步是添加另一個(gè)名為文件,該文件用代替:
<root>
<data name="Greeting">
<value>hallo<value>
</data>
</root>
在 Visual Studio 中,只需執(zhí)行以下操作 — 重新生成時(shí),將在名為 的子目錄中自動(dòng)創(chuàng)建名為 的附屬程序集。
若要模擬在具有不同語(yǔ)言的操作系統(tǒng)上運(yùn)行,必須使用 Thread 類(lèi)更改 CurrentUICulture:
System.Threading.Thread.CurrentThread.CurrentUICulture
= new System.Globalization.CultureInfo ("de");
CultureInfo.CurrentUICulture 是同一屬性的只讀版本。
一個(gè)有用的測(cè)試策略是將 l?¢αl??? 轉(zhuǎn)換為仍然可以讀作英語(yǔ)的單詞,但不要使用標(biāo)準(zhǔn)的羅馬 Unicode 字符。
Visual Studio 中的設(shè)計(jì)器為本地化組件和可視元素提供了擴(kuò)展支持。WPF 設(shè)計(jì)器具有自己的本地化工作流;其他基于組件的設(shè)計(jì)器使用僅設(shè)計(jì)時(shí)屬性來(lái)使組件或 Windows 窗體控件看起來(lái)具有語(yǔ)言屬性。若要針對(duì)其他語(yǔ)言進(jìn)行自定義,只需更改 Language 屬性,然后開(kāi)始修改組件。屬性為可本地化的控件的所有屬性都將保存到該語(yǔ)言的 文件中。只需更改 Language 屬性,即可隨時(shí)在語(yǔ)言之間切換。
文化分為文化和亞文化。文化代表一種特定的語(yǔ)言;亞文化代表該語(yǔ)言的區(qū)域變體。.NET 運(yùn)行時(shí)遵循 RFC1766 標(biāo)準(zhǔn),該標(biāo)準(zhǔn)使用兩個(gè)字母的代碼表示區(qū)域性和子區(qū)域性。以下是英語(yǔ)和德語(yǔ)區(qū)域性的代碼:
En
de
以下是澳大利亞英語(yǔ)和奧地利德語(yǔ)亞文化的代碼:
en-AU
de-AT
區(qū)域性在 .NET 中使用 System.Globalization.CultureInfo 類(lèi)表示。可以檢查應(yīng)用程序的當(dāng)前區(qū)域性,如下所示:
Console.WriteLine (System.Threading.Thread.CurrentThread.CurrentCulture);
Console.WriteLine (System.Threading.Thread.CurrentThread.CurrentUICulture);
在針對(duì)澳大利亞本地化的計(jì)算機(jī)上運(yùn)行此函數(shù)說(shuō)明了兩者之間的區(qū)別:
en-AU
en-US
CurrentCulture 反映 Windows 控制面板的區(qū)域設(shè)置,而 CurrentUICulture 反映操作系統(tǒng)的語(yǔ)言。
區(qū)域設(shè)置包括時(shí)區(qū)以及貨幣和日期的格式等。CurrentCulture 確定諸如 DateTime.Parse 之類(lèi)的函數(shù)的默認(rèn)行為。可以自定義區(qū)域設(shè)置,使其不再類(lèi)似于任何特定區(qū)域性。
CurrentUICulture 確定計(jì)算機(jī)與用戶(hù)通信的語(yǔ)言。澳大利亞不需要單獨(dú)的英語(yǔ)版本,所以它只使用美國(guó)的英語(yǔ)版本。如果我在奧地利工作了幾個(gè)月,我會(huì)轉(zhuǎn)到控制面板并將我的 CurrentCulture 更改為奧地利德語(yǔ)。但是,鑒于我不會(huì)說(shuō)德語(yǔ),我的 CurrentUICulture 將仍然是美國(guó)英語(yǔ)。
默認(rèn)情況下,資源管理器使用當(dāng)前線程的 CurrentUICulture 屬性來(lái)確定要加載的正確附屬程序集。資源管理器在加載資源時(shí)使用回退機(jī)制。如果定義了亞文化程序集,則使用該組合體;否則,它將回退到通用區(qū)域性。如果泛型區(qū)域性不存在,它將回退到主程序集中的默認(rèn)區(qū)域性。
從已知位置加載程序集是一個(gè)相對(duì)簡(jiǎn)單的過(guò)程。我們將其稱(chēng)為。
但是,更常見(jiàn)的是,您(或 CLR)需要加載僅知道其完整(或簡(jiǎn)單)名稱(chēng)的程序集。這稱(chēng)為。程序集分辨率與加載的不同之處在于,必須首先找到程序集。
在兩種情況下觸發(fā)程序集解析:
為了說(shuō)明第一種方案,請(qǐng)考慮一個(gè)包含主程序集和一組靜態(tài)引用的庫(kù)程序集(依賴(lài)項(xiàng))的應(yīng)用程序,如以下示例所示:
AdventureGame.dll // Main assembly
Terrain.dll // Referenced assembly
UIEngine.dll // Referenced assembly
通過(guò)“靜態(tài)引用”,我們的意思是.dll是引用和編譯的。編譯器本身不需要執(zhí)行程序集解析,因?yàn)樗桓嬷@式或由 MSBuild)在哪里查找 .dll 和 。在編譯過(guò)程中,它將 Terrain 和 UIEngine 程序集的寫(xiě)入 的元數(shù)據(jù)中,但沒(méi)有關(guān)于在哪里可以找到它們的信息。因此,在運(yùn)行時(shí),必須地形和 UIEngine 程序集。
程序集加載和解析由 (ALC) 處理;具體來(lái)說(shuō),System.Runtime.Loader 中的 AssemblyLoadContext 類(lèi)的一個(gè)實(shí)例。由于 是應(yīng)用程序的主程序集,因此 CLR 使用 ( AssemblyLoadContext.Default ) 來(lái)解析其依賴(lài)關(guān)系。默認(rèn) ALC 首先通過(guò)查找并檢查名為 的文件(該文件描述了在何處查找依賴(lài)項(xiàng))來(lái)解決依賴(lài)項(xiàng),或者如果不存在,它會(huì)在應(yīng)用程序基文件夾中查找,在該文件夾中可以找到 和 。(默認(rèn) ALC 還會(huì)解析 .NET 運(yùn)行時(shí)程序集。
作為開(kāi)發(fā)人員,您可以在程序執(zhí)行期間動(dòng)態(tài)加載其他程序集。例如,您可能希望將可選功能打包到僅在購(gòu)買(mǎi)這些功能后部署的程序集中。在這種情況下,您可以通過(guò)調(diào)用 Assembly 來(lái)加載額外的程序集(如果存在)。加載(程序集名稱(chēng)) .
一個(gè)更復(fù)雜的示例是實(shí)現(xiàn)插件系統(tǒng),用戶(hù)可以在其中提供應(yīng)用程序在運(yùn)行時(shí)檢測(cè)和加載的第三方程序集,以擴(kuò)展應(yīng)用程序的功能。之所以出現(xiàn)復(fù)雜性,是因?yàn)槊總€(gè)插件程序集可能都有自己的依賴(lài)項(xiàng),這些依賴(lài)項(xiàng)也必須解決。
通過(guò)子類(lèi)化 AssemblyLoadContext 并重寫(xiě)其程序集解析方法 ( Load ),可以控制插件查找其依賴(lài)項(xiàng)的方式。例如,您可能決定每個(gè)插件都應(yīng)駐留在其自己的文件夾中,并且其依賴(lài)項(xiàng)也應(yīng)駐留在該文件夾中。
ALC 還有另一個(gè)用途:通過(guò)為每個(gè) ALC 實(shí)例化一個(gè)單獨(dú)的 AssemblyLoadContext(插件 + 依賴(lài)項(xiàng)),您可以保持每個(gè) ALC 的隔離,確保它們的依賴(lài)項(xiàng)并行加載并且不會(huì)相互干擾(或主機(jī)應(yīng)用程序)。例如,每個(gè)都可以有自己的 JSON.NET 版本。因此,除了和之外,ALC還提供了機(jī)制。在某些條件下,甚至可以ALC,從而釋放其內(nèi)存。
在本節(jié)中,我們將詳細(xì)闡述這些原則中的每一個(gè),并描述以下內(nèi)容:
然后,我們將理論付諸實(shí)踐,并演示如何編寫(xiě)具有ALC隔離的插件系統(tǒng)。
AssemblyLoadContext 類(lèi)是 .NET 5 和 .NET Core 的新增功能。在 .NET Framework 中,ALC 存在,但受到限制和隱藏:創(chuàng)建和與它們交互的唯一方法是間接通過(guò)程序集類(lèi)上的 LoadFile(string)、LoadFrom(string) 和 Load(byte[]) 靜態(tài)方法。與 ALC API 相比,這些方法不靈活,它們的使用可能會(huì)導(dǎo)致意外(尤其是在處理依賴(lài)項(xiàng)時(shí))。因此,最好支持在 .NET 5 和 .NET Core 中顯式使用 AssemblyLoadContext API。
正如我們剛才所討論的,AssemblyLoadContext 類(lèi)負(fù)責(zé)加載和解析程序集,并提供隔離機(jī)制。
每個(gè) .NET 程序集對(duì)象只屬于一個(gè) AssemblyLoadContext 。您可以獲取程序集的 ALC,如下所示:
Assembly assem = Assembly.GetExecutingAssembly();
AssemblyLoadContext context = AssemblyLoadContext.GetLoadContext (assem);
Console.WriteLine (context.Name);
相反,您可以將 ALC 視為“包含”或“擁有”程序集,可以通過(guò)其 Assemblies 屬性獲取這些程序集。繼上一個(gè)之后:
foreach (Assembly a in context.Assemblies)
Console.WriteLine (a.FullName);
類(lèi)還具有枚舉所有 ALC 的靜態(tài) All 屬性。
你可以通過(guò)實(shí)例化AssemblyLoadContext并提供一個(gè)名稱(chēng)來(lái)創(chuàng)建新的ALC(該名稱(chēng)在調(diào)試時(shí)很有幫助),盡管更常見(jiàn)的是,你首先要子類(lèi)AssemblyLoadContext,以便你可以實(shí)現(xiàn)邏輯來(lái)依賴(lài)關(guān)系;換句話(huà)說(shuō),從程序集加載程序集。
AssemblyLoadContext 提供了以下方法來(lái)將程序集顯式加載到其上下文中:
public Assembly LoadFromAssemblyPath (string assemblyPath);
public Assembly LoadFromStream (Stream assembly, Stream assemblySymbols);
第一種方法從文件路徑加載程序集,而第二種方法從 Stream(可以直接來(lái)自?xún)?nèi)存)加載程序集。第二個(gè)參數(shù)是可選的,對(duì)應(yīng)于項(xiàng)目 debug () 文件的內(nèi)容,該文件允許堆棧跟蹤在代碼執(zhí)行時(shí)包含源代碼信息(在異常報(bào)告中很有用)。
使用這兩種方法時(shí),不會(huì)進(jìn)行。
下面將程序集 加載到其自己的 ALC 中:
var alc = new AssemblyLoadContext ("Test");
Assembly assem = alc.LoadFromAssemblyPath (@"c:\temp\foo.dll");
如果程序集有效,則加載將始終成功,但要遵守一條重要規(guī)則:程序集的在其 ALC 中必須是唯一的。這意味著不能將同名程序集的多個(gè)版本加載到單個(gè) ALC 中;為此,您必須創(chuàng)建其他 ALC。我們可以加載另一個(gè) 的副本
var alc2 = new AssemblyLoadContext ("Test 2");
Assembly assem2 = alc2.LoadFromAssemblyPath (@"c:\temp\foo.dll");
請(qǐng)注意,源自不同程序集對(duì)象的類(lèi)型是不兼容的,即使這些程序集在其他方面是相同的。在我們的示例中,assem 中的類(lèi)型與 assem2 中的類(lèi)型不兼容。
加載組件后,除非卸載其 ALC,否則無(wú)法卸載該組件(請(qǐng)參見(jiàn))。CLR 在加載文件的持續(xù)時(shí)間內(nèi)保持文件的鎖定。
您可以通過(guò)字節(jié)數(shù)組加載程序集來(lái)避免鎖定文件:
bytes[] bytes = File.ReadAllBytes (@"c:\temp\foo.dll");
var ms = new MemoryStream (bytes);
var assem = alc.LoadFromStream (ms);
這有兩個(gè)缺點(diǎn):
AssemblyLoadContext 還提供了以下方法,該方法按加載程序集:
public Assembly LoadFromAssemblyName (AssemblyName assemblyName);
與剛才討論的兩種方法不同,您不會(huì)傳入任何信息來(lái)指示程序集所在的位置;相反,您正在指示 ALC 程序集。
上述方法觸發(fā)。CLR 還會(huì)在加載依賴(lài)項(xiàng)時(shí)觸發(fā)程序集解析。例如,假設(shè)程序集 A 靜態(tài)引用程序集 B。為了解析引用 B,CLR 會(huì)在加載 程序集上觸發(fā)程序集解析。
CLR 通過(guò)觸發(fā)程序集解析(無(wú)論觸發(fā)程序集是默認(rèn)程序集還是自定義 ALC)來(lái)解析依賴(lài)項(xiàng)。不同之處在于,使用默認(rèn) ALC,解析規(guī)則是硬編碼的,而使用自定義 ALC,您可以自己編寫(xiě)規(guī)則。
然后會(huì)發(fā)生什么:
由此,我們可以看到有兩種方法可以在自定義 ALC 中實(shí)現(xiàn)程序集解析:
如果將多個(gè)事件處理程序附加到 Resolve 事件,則第一個(gè)返回非 null 值的事件處理程序優(yōu)先。
為了說(shuō)明這一點(diǎn),假設(shè)我們要加載一個(gè)主應(yīng)用程序在編譯時(shí)一無(wú)所知的程序集,稱(chēng)為 ,位于 (與我們的應(yīng)用程序文件夾不同)。我們還假設(shè) 對(duì) 有私有依賴(lài)。我們希望確保當(dāng)我們加載 并執(zhí)行其代碼時(shí), 可以正確解析。我們還希望確保foo及其私有依賴(lài)項(xiàng)bar不會(huì)干擾主應(yīng)用程序。
讓我們首先編寫(xiě)一個(gè)覆蓋 Load 的自定義 ALC:
using System.IO;
using System.Runtime.Loader;
class FolderBasedALC : AssemblyLoadContext
{
readonly string _folder;
public FolderBasedALC (string folder) => _folder = folder;
protected override Assembly Load (AssemblyName assemblyName)
{
// Attempt to find the assembly:
string targetPath = Path.Combine (_folder, assemblyName.Name + ".dll");
if (File.Exists (targetPath))
return LoadFromAssemblyPath (targetPath); // Load the assembly
return null; // We can’t find it: it could be a .NET runtime assembly
}
}
請(qǐng)注意,在 Load 方法中,如果程序集文件不存在,則返回 null。此檢查很重要,因?yàn)?也將依賴(lài)于 .NET BCL 程序集;因此,Load 方法將在諸如 System.Runtime 之類(lèi)的程序集上調(diào)用。通過(guò)返回 null,我們?cè)试S CLR 回退到默認(rèn) ALC,這將正確解析這些程序集。
請(qǐng)注意,我們沒(méi)有嘗試將 .NET 運(yùn)行時(shí) BCL 程序集加載到我們自己的 ALC 中。這些系統(tǒng)程序集不是設(shè)計(jì)為在默認(rèn) ALC 之外運(yùn)行,嘗試將它們加載到您自己的 ALC 中可能會(huì)導(dǎo)致不正確下降和意外的類(lèi)型不兼容。
以下是我們?nèi)绾问褂米远x ALC 在 中加載 程序集:
var alc = new FolderBasedALC (@"c:\temp");
Assembly foo = alc.LoadFromAssemblyPath (@"c:\temp\foo.dll");
...
當(dāng)我們隨后開(kāi)始在 foo 程序集中調(diào)用代碼時(shí),CLR 在某些時(shí)候?qū)⑿枰鉀Q對(duì) 的依賴(lài)關(guān)系。此時(shí),自定義 ALC 的 Load 方法將觸發(fā)并在 中成功找到 程序集。
在這種情況下,我們的 Load 方法也能夠解析 ,因此我們可以將代碼簡(jiǎn)化為:
var alc = new FolderBasedALC (@"c:\temp");
Assembly foo = alc.LoadFromAssemblyName (new AssemblyName ("foo"));
...
現(xiàn)在,讓我們考慮一個(gè)替代解決方案:我們可以實(shí)例化一個(gè)普通的 AssemblyLoadContext 并處理其解析事件,而不是子類(lèi)化 AssemblyLoadContext 并覆蓋 Load:
var alc = new AssemblyLoadContext ("test");
alc.Resolving += (loadContext, assemblyName) =>
{
string targetPath = Path.Combine (@"c:\temp", assemblyName.Name + ".dll");
return alc.LoadFromAssemblyPath (targetPath); // Load the assembly
};
Assembly foo = alc.LoadFromAssemblyName (new AssemblyName ("foo"));
現(xiàn)在請(qǐng)注意,我們不需要檢查程序集是否存在。由于 Resolve 事件在默認(rèn) ALC 有機(jī)會(huì)解析程序集觸發(fā)(并且僅在它失敗時(shí)觸發(fā)),因此我們的處理程序不會(huì)為 .NET BCL 程序集觸發(fā)。這使得此解決方案更簡(jiǎn)單,盡管存在缺點(diǎn)。請(qǐng)記住,在我們的場(chǎng)景中,主應(yīng)用程序在編譯時(shí)對(duì) 或 一無(wú)所知。這意味著主應(yīng)用程序本身可能依賴(lài)于稱(chēng)為 或 的程序集。如果發(fā)生這種情況,解析事件將永遠(yuǎn)不會(huì)觸發(fā),而是加載應(yīng)用程序的 foo 和 bar 程序集。換言之,我們將無(wú)法實(shí)現(xiàn)。
我們的 FolderBasedALC 類(lèi)非常適合說(shuō)明程序集解析的概念,但它在現(xiàn)實(shí)生活中的用處較少,因?yàn)樗鼰o(wú)法處理特定于平臺(tái)和(對(duì)于庫(kù)項(xiàng)目)開(kāi)發(fā)時(shí) NuGet 依賴(lài)項(xiàng)。在”中,我們描述了這個(gè)問(wèn)題的解決方案,在中,我們給出了一個(gè)詳細(xì)的例子。
當(dāng)應(yīng)用程序啟動(dòng)時(shí),CLR 會(huì)為靜態(tài) AssemblyLoadContext 分配一個(gè)特殊的 ALC。默認(rèn)屬性。默認(rèn) ALC 是加載啟動(dòng)程序集及其靜態(tài)引用依賴(lài)項(xiàng)和 .NET 運(yùn)行時(shí) BCL 程序集的位置。
默認(rèn) ALC 首先在默認(rèn)路徑中查找以自動(dòng)解析程序集(請(qǐng)參閱);這通常等同于應(yīng)用程序的 . 和 . 文件中指示的位置。
如果 ALC 在其默認(rèn)探測(cè)路徑中找不到程序集,則會(huì)觸發(fā)其解析事件。通過(guò)處理此事件,您可以從其他位置加載程序集,這意味著您可以將應(yīng)用程序的依賴(lài)項(xiàng)部署到其他位置,例如子文件夾、共享文件夾,甚至作為宿主程序集內(nèi)的二進(jìn)制資源:
AssemblyLoadContext.Default.Resolving += (loadContext, assemblyName) =>
{
// Try to locate assemblyName, returning an Assembly object or null.
// Typically you’d call LoadFromAssemblyPath after finding the file.
// ...
};
當(dāng)自定義 ALC 無(wú)法解析時(shí)(換句話(huà)說(shuō),當(dāng)其 Load 方法返回 null 時(shí)),默認(rèn) ALC 中的 Resolve 事件也會(huì)觸發(fā),并且默認(rèn) ALC 無(wú)法解析程序集。
還可以從解析事件外部將程序集加載到默認(rèn) ALC 中。但是,在繼續(xù)之前,您應(yīng)該首先確定是否可以通過(guò)使用單獨(dú)的 ALC 或使用我們?cè)谙乱还?jié)中描述的方法(使用和 ALC)來(lái)更好地解決問(wèn)題。硬編碼為默認(rèn) ALC 會(huì)使代碼變得脆弱,因?yàn)樗荒茏鳛橐粋€(gè)整體進(jìn)行隔離(例如,通過(guò)單元測(cè)試框架或 LINQPad)。
如果仍要繼續(xù),最好調(diào)用(即 LoadFromAssemblyName)而不是(例如 LoadFromAssemblyPath),尤其是在程序集被靜態(tài)引用的情況下。這是因?yàn)槌绦蚣赡芤呀?jīng)加載,在這種情況下,LoadFromAssemblyName 將返回已加載的程序集,而 LoadFromAssemblyPath 將引發(fā)異常。
(使用 LoadFromAssemblyPath ,您還可以冒著從與 ALC 的默認(rèn)解析機(jī)制不一致的位置加載程序集的風(fēng)險(xiǎn)。
如果程序集位于 ALC 不會(huì)自動(dòng)找到它的位置,您仍然可以按照此過(guò)程進(jìn)行操作,并另外處理 ALC 的解析事件。
請(qǐng)注意,調(diào)用 LoadFromAssemblyName 時(shí),不需要提供全名;簡(jiǎn)單名稱(chēng)就可以了(即使程序集是強(qiáng)名稱(chēng)的,也是有效的):
AssemblyLoadContext.Default.LoadFromAssemblyName ("System.Xml");
但是,如果在名稱(chēng)中包含公鑰標(biāo)記,則它必須與加載的內(nèi)容匹配。
默認(rèn)探測(cè)路徑通常包括以下內(nèi)容:
MSBuild 自動(dòng)生成一個(gè)名為 的文件,該文件描述了在何處查找其所有依賴(lài)項(xiàng)。其中包括放置在應(yīng)用程序基文件夾中的與平臺(tái)無(wú)關(guān)的程序集,以及放置在運(yùn)行時(shí)子目錄下(如 或 )下的特定于平臺(tái)的程序集。
生成的 . 文件中指定的路徑相對(duì)于應(yīng)用程序基文件夾,或您在 AppName.runtimeconfig.json 和/或 配置文件的 additionalProbingPath 部分中指定的任何其他文件夾(后者僅適用于開(kāi)發(fā)環(huán)境)。
在上一節(jié)中,我們警告不要將程序集顯式加載到默認(rèn) ALC 中。相反,您通常想要的是加載/解析到“當(dāng)前”ALC 中。
在大多數(shù)情況下,“當(dāng)前”ALC 是包含當(dāng)前正在執(zhí)行的程序集的 ALC:
var executingAssem = Assembly.GetExecutingAssembly();
var alc = AssemblyLoadContext.GetLoadContext (executingAssem);
Assembly assem = alc.LoadFromAssemblyName (...); // to resolve by name
// OR: = alc.LoadFromAssemblyPath (...); // to load by path
以下是獲取 ALC 的更靈活、更明確的方法:
var myAssem = typeof (SomeTypeInMyAssembly).Assembly;
var alc = AssemblyLoadContext.GetLoadContext (myAssem);
...
有時(shí),無(wú)法推斷“當(dāng)前”ALC。例如,假設(shè)您負(fù)責(zé)編寫(xiě) .NET 二進(jìn)制序列化程序(我們將 的聯(lián)機(jī)補(bǔ)充中介紹序列化)。像這樣的序列化程序?qū)懭胨蛄谢念?lèi)型的全名(包括其程序集名稱(chēng)),必須在反序列化期間這些名稱(chēng)。問(wèn)題是,您應(yīng)該使用哪種 ALC?依賴(lài)正在執(zhí)行的程序集的問(wèn)題在于,它將返回包含反序列化程序的任何程序集,而不是反序列化程序的程序集。
最好的解決方案不是猜測(cè),而是問(wèn):
public object Deserialize (Stream stream, AssemblyLoadContext alc)
{
...
}
明確可以最大限度地提高靈活性并最大限度地減少犯錯(cuò)的機(jī)會(huì)。調(diào)用方現(xiàn)在可以決定什么應(yīng)該算作“當(dāng)前”ALC:
var assem = typeof (SomeTypeThatIWillBeDeserializing).Assembly;
var alc = AssemblyLoadContext.GetLoadContext (assem);
var object = Deserialize (someStream, alc);
幫助處理將程序集加載到當(dāng)前執(zhí)行的 ALC 中的常見(jiàn)情況;那是
var executingAssem = Assembly.GetExecutingAssembly();
var alc = AssemblyLoadContext.GetLoadContext (executingAssem);
Assembly assem = alc.LoadFromAssemblyName (...);
Microsoft已在程序集類(lèi)中定義了以下方法
public static Assembly Load (string assemblyString);
以及接受 AssemblyName 對(duì)象的功能相同的版本:
public static Assembly Load (AssemblyName assemblyRef);
(不要將這些方法與傳統(tǒng)的 Load(byte[]) 方法混淆,后者的行為方式完全不同 — 請(qǐng)參閱
與 LoadFromAssemblyName 一樣,您可以選擇指定程序集的簡(jiǎn)單名稱(chēng)、部分名稱(chēng)或全名:
Assembly a = Assembly.Load ("System.Private.Xml");
這會(huì)將 System.Private.Xml 程序集加載到的任何 ALC 中。
在本例中,我們指定了一個(gè)簡(jiǎn)單的名稱(chēng)。以下字符串也是有效的,并且在 .NET 中所有字符串的結(jié)果都相同:
"System.Private.Xml, PublicKeyToken=cc7b13ffcd2ddd51"
"System.Private.Xml, Version=4.0.1.0"
"System.Private.Xml, Version=4.0.1.0, PublicKeyToken=cc7b13ffcd2ddd51"
如果選擇指定公鑰標(biāo)記,則它必須與加載的內(nèi)容匹配。
Microsoft開(kāi)發(fā)人員網(wǎng)絡(luò) (MSDN) 警告不要從部分名稱(chēng)加載程序集,建議您指定確切的版本和公鑰標(biāo)記。它們的基本原理基于與 .NET Framework 相關(guān)的因素,例如全局程序集緩存和代碼訪問(wèn)安全性的影響。在 .NET 5 和 .NET Core 中,不存在這些因素,從簡(jiǎn)單名稱(chēng)或部分名稱(chēng)加載通常是安全的。
這兩種方法都嚴(yán)格用于,因此無(wú)法指定文件路徑。(如果在 AssemblyName 對(duì)象中填充 CodeBase 屬性,則將忽略該屬性。
不要落入使用 Assembly.Load 加載靜態(tài)引用程序集的陷阱。在這種情況下,您需要做的就是引用程序集中的一個(gè)類(lèi)型,并從中獲取程序集:
Assembly a = typeof (System.Xml.Formatting).Assembly;
或者,您甚至可以這樣做:
Assembly a = System.Xml.Formatting.Indented.GetType().Assembly;
這可以防止對(duì)程序集名稱(chēng)進(jìn)行硬編碼(將來(lái)可能會(huì)更改),同時(shí)在 ALC 上觸發(fā)程序集解析(就像 一樣)。
如果你要寫(xiě)大會(huì).自己加載方法,它(幾乎)看起來(lái)像這樣:
[MethodImpl(MethodImplOptions.NoInlining)]
Assembly Load (string name)
{
Assembly callingAssembly = Assembly.GetCallingAssembly();
var callingAlc = AssemblyLoadContext.GetLoadContext (callingAssembly);
return callingAlc.LoadFromAssemblyName (new AssemblyName (name));
}
集會(huì)。加載 使用調(diào)用程序集的 ALC 上下文的策略在程序集 失敗時(shí)失敗。負(fù)載通過(guò)中介(如反序列化程序或單元測(cè)試運(yùn)行程序)調(diào)用。如果中介在不同的程序集中定義,則使用中介的加載上下文,而不是調(diào)用方的加載上下文。
我們之前在討論如何編寫(xiě)反序列化程序時(shí)描述了這種情況。在這種情況下,理想的解決方案是強(qiáng)制調(diào)用方指定 ALC,而不是使用 Assembly.Load(string) 推斷它。
但是,由于 .NET 5 和 .NET Core 是從 .NET Framework 演變而來(lái)的(在 .NET Framework 中,隔離是通過(guò)應(yīng)用程序域而不是 ALC 完成的),因此理想的解決方案并不普遍,并且在無(wú)法可靠地推斷 ALC 的情況下,有時(shí)會(huì)不恰當(dāng)?shù)厥褂?Assembly.Load(string)。一個(gè)例子是 .NET 二進(jìn)制序列化程序。
要允許程序集 。加載在這種情況下仍然有效,Microsoft向AssemblyLoadContext添加了一個(gè)名為EnterContextualReflection的方法。這會(huì)將 ALC 分配給 AssemblyLoadContext。當(dāng)前上下文反射上下文 .盡管這是一個(gè)靜態(tài)屬性,但其值存儲(chǔ)在 AsyncLocal 變量中,因此它可以在不同的線程上保存單獨(dú)的值(但仍會(huì)在整個(gè)異步操作中保留)。
如果此屬性為非空,則程序集 .Load 自動(dòng)使用它,而不是調(diào)用 ALC:
Method1();
var myALC = new AssemblyLoadContext ("test");
using (myALC.EnterContextualReflection())
{
Console.WriteLine (
AssemblyLoadContext.CurrentContextualReflectionContext.Name); // test
Method2();
}
// Once disposed, EnterContextualReflection() no longer has an effect.
Method3();
void Method1() => Assembly.Load ("..."); // Will use calling ALC
void Method2() => Assembly.Load ("..."); // Will use myALC
void Method3() => Assembly.Load ("..."); // Will use calling ALC
我們之前演示了如何編寫(xiě)功能類(lèi)似于 匯編 的方法。負(fù)荷。下面是一個(gè)更準(zhǔn)確的版本,它考慮了上下文反射上下文:
[MethodImpl(MethodImplOptions.NoInlining)]
Assembly Load (string name)
{
var alc = AssemblyLoadContext.CurrentContextualReflectionContext
?? AssemblyLoadContext.GetLoadContext (Assembly.GetCallingAssembly());
return alc.LoadFromAssemblyName (new AssemblyName (name));
}
盡管上下文反射上下文在允許舊代碼運(yùn)行方面很有用,但更可靠的解決方案(如前所述)是修改調(diào)用 Assembly.Load 的代碼,使其改為在調(diào)用方傳入的 ALC 上調(diào)用 LoadFromAssemblyName。
.NET Framework 沒(méi)有等效的 EnterContextualReflection,也不需要它,盡管具有相同的程序集。加載方法。這是因?yàn)槭褂?.NET Framework,隔離主要通過(guò)而不是 ALC 實(shí)現(xiàn)。應(yīng)用程序域提供了更強(qiáng)的隔離模型,其中每個(gè)應(yīng)用程序域都有自己的默認(rèn)加載上下文,因此即使僅使用默認(rèn)加載上下文,隔離仍然可以工作。
ALC 還可以加載和解析本機(jī)庫(kù)。調(diào)用標(biāo)有 [DllImport] 屬性的外部方法時(shí),將觸發(fā)本機(jī)解析:
[DllImport ("SomeNativeLibrary.dll")]
static extern int SomeNativeMethod (string text);
由于我們沒(méi)有在 [DllImport] 屬性中指定完整路徑,因此調(diào)用 SomeNativeMethod 會(huì)在包含定義 SomeNativeMethod 的程序集的任何 ALC 中觸發(fā)解析。
ALC 中的虛擬方法稱(chēng)為 LoadUnmanagedDll ,方法稱(chēng)為 LoadUnmanagedDllFromPath:
protected override IntPtr LoadUnmanagedDll (string unmanagedDllName)
{
// Locate the full path of unmanagedDllName...
string fullPath = ...
return LoadUnmanagedDllFromPath (fullPath); // Load the DLL
}
如果找不到該文件,可以返回 IntPtr.Zero 。然后,CLR 將觸發(fā) ALC 的 ResolvevingUnmanagedDll 事件。
有趣的是,LoadUnmanagedDllFromPath 方法受到保護(hù),因此通常無(wú)法從 ResolvevingUnmanagedDll 事件處理程序調(diào)用它。但是,您可以通過(guò)調(diào)用靜態(tài) NativeLibrary.Load 來(lái)獲得相同的結(jié)果:
someALC.ResolvingUnmanagedDll += (requestingAssembly, unmanagedDllName) =>
{
return NativeLibrary.Load ("(full path to unmanaged DLL)");
};
盡管本機(jī)庫(kù)通常由 ALC 解析和加載,但它們并不“屬于”ALC。加載后,本機(jī)庫(kù)將獨(dú)立存在,并負(fù)責(zé)解析它可能具有的任何傳遞依賴(lài)項(xiàng)。此外,本機(jī)庫(kù)是進(jìn)程的全局庫(kù),因此如果它們具有相同的文件名,則無(wú)法加載兩個(gè)不同版本的本機(jī)庫(kù)。
在中,我們說(shuō)過(guò)默認(rèn) ALC 會(huì)讀取 .deps.json 和 . 文件(如果存在),以確定在何處查找以解析特定于平臺(tái)和開(kāi)發(fā)時(shí) NuGet 依賴(lài)項(xiàng)。
如果要將程序集加載到具有特定于平臺(tái)或 NuGet 依賴(lài)項(xiàng)的自定義 ALC 中,則需要以某種方式重現(xiàn)此邏輯。可以通過(guò)分析配置文件并仔細(xì)遵循特定于平臺(tái)的名字對(duì)象上的規(guī)則來(lái)實(shí)現(xiàn)此目的,但這樣做不僅困難,而且如果規(guī)則在更高版本的 .NET 中發(fā)生更改,則編寫(xiě)的代碼將中斷。
程序集依賴(lài)解析器類(lèi)解決了這個(gè)問(wèn)題。若要使用它,請(qǐng)使用要探測(cè)其依賴(lài)項(xiàng)的程序集的路徑實(shí)例化它:
var resolver = new AssemblyDependencyResolver (@"c:\temp\foo.dll");
然后,若要查找依賴(lài)項(xiàng)的路徑,請(qǐng)調(diào)用 ResolveAssemblyToPath 方法:
string path = resolver.ResolveAssemblyToPath (new AssemblyName ("bar"));
在沒(méi)有 . 文件的情況下(或者如果 . 不包含任何與 bar.dll) 相關(guān)的內(nèi)容,這將計(jì)算為 。
同樣,可以通過(guò)調(diào)用 ResolveUnmanagedDllToPath 來(lái)解析非托管依賴(lài)項(xiàng)。
說(shuō)明更復(fù)雜的方案的一個(gè)好方法是創(chuàng)建一個(gè)名為 ClientApp 的新控制臺(tái)項(xiàng)目,然后添加對(duì) 的 NuGet 引用。添加以下類(lèi):
using Microsoft.Data.SqlClient;
namespace ClientApp
{
public class Program
{
public static SqlConnection GetConnection() => new SqlConnection();
static void Main() => GetConnection(); // Test that it resolves
}
}
現(xiàn)在構(gòu)建應(yīng)用程序并查看輸出文件夾:您將看到一個(gè)名為 的文件。但是,此文件在運(yùn)行時(shí)加載,嘗試顯式加載它會(huì)引發(fā)異常。實(shí)際加載的程序集位于(或)子文件夾中;默認(rèn) ALC 知道加載它,因?yàn)樗馕?文件。
如果要嘗試從另一個(gè)應(yīng)用程序加載 .dll 程序集,則需要編寫(xiě)一個(gè)可以解析其依賴(lài)項(xiàng)的 ALC,。這樣做時(shí),僅查看 所在的文件夾是不夠的(就像中所做的那樣)。相反,您需要使用 AssemblyDependencyResolver 來(lái)確定該文件對(duì)于正在使用的平臺(tái)的位置:
string path = @"C:\source\ClientApp\bin\Debug\netcoreapp3.0\ClientApp.dll";
var resolver = new AssemblyDependencyResolver (path);
var sqlClient = new AssemblyName ("Microsoft.Data.SqlClient");
Console.WriteLine (resolver.ResolveAssemblyToPath (sqlClient));
在 Windows 計(jì)算機(jī)上,這將輸出以下內(nèi)容:
C:\source\ClientApp\bin\Debug\netcoreapp3.0\runtimes\win\lib\netcoreapp2.1
\Microsoft.Data.SqlClient.dll
我們?cè)谥薪o出了一個(gè)完整的示例。
在簡(jiǎn)單的情況下,可以卸載非默認(rèn)的 AssemblyLoadContext ,釋放內(nèi)存并釋放它加載的程序集上的文件鎖。為此,ALC 必須已使用 isCollectible 參數(shù) true 進(jìn)行實(shí)例化:
var alc = new AssemblyLoadContext ("test", isCollectible:true);
然后,可以在 ALC 上調(diào)用 Unload 方法來(lái)啟動(dòng)卸載過(guò)程。
卸載模型是合作的,而不是搶占的。如果 ALC 的任何程序集中的任何方法正在執(zhí)行,則卸載將延遲到這些方法完成。
實(shí)際卸載發(fā)生在垃圾收集期間;如果來(lái)自 ALC 外部的任何內(nèi)容對(duì) ALC 內(nèi)部的任何內(nèi)容(包括對(duì)象、類(lèi)型和程序集)有任何(非弱)引用,則不會(huì)發(fā)生這種情況。API(包括 .NET BCL 中的 API)在靜態(tài)字段或字典中緩存對(duì)象或訂閱事件的情況并不少見(jiàn),這使得創(chuàng)建防止卸載的引用變得容易,尤其是在 ALC 中的代碼以非平凡的方式使用其 ALC 外部的 API 時(shí)。確定卸載失敗的原因很困難,需要使用 WinDbg 等工具。
如果您仍在使用 .NET Framework(或編寫(xiě)面向 .NET Standard 的庫(kù),并希望支持 .NET Framework),您將無(wú)法使用 AssemblyLoadContext 類(lèi)。加載是通過(guò)使用以下方法完成的:
public static Assembly LoadFrom (string assemblyFile);
public static Assembly LoadFile (string path);
public static Assembly Load (byte[] rawAssembly);
LoadFile 和 Load(byte[]) 提供隔離,而 LoadFrom 不提供。
解析是通過(guò)處理應(yīng)用程序域的 AssemblyResolve 事件來(lái)實(shí)現(xiàn)的,該事件的工作方式類(lèi)似于默認(rèn) ALC 的解析事件。
Assembly.Load(string) 方法也可用于觸發(fā)解析,并以類(lèi)似的方式工作。
LoadFrom 將程序集從給定路徑加載到默認(rèn) ALC 中。這有點(diǎn)像調(diào)用AssemblyLoadContext.Default.LoadFromAssemblyPath,除了:
.NET Framework 有一個(gè) (GAC)。如果程序集存在于 GAC 中,則 CLR 將始終從那里加載。這適用于所有三種加載方法。
LoadFrom 自動(dòng)解析傳遞相同文件夾依賴(lài)項(xiàng)的能力可能很方便 - 直到它加載不應(yīng)該加載的程序集。由于此類(lèi)方案可能難以調(diào)試,因此最好使用 Load(string) 或 LoadFile 并通過(guò)處理應(yīng)用程序域的 AssemblyResolve 事件來(lái)解析傳遞依賴(lài)項(xiàng)。這使您能夠決定如何解析每個(gè)程序集,并允許調(diào)試(通過(guò)在事件處理程序中創(chuàng)建斷點(diǎn))。
LoadFile 和 Load(byte[]) 將程序集從給定的文件路徑或字節(jié)數(shù)組加載到新的 ALC 中。與 LoadFrom 不同,這些方法提供隔離,并允許您加載同一程序集的多個(gè)版本。但是,有兩個(gè)注意事項(xiàng):
使用 LoadFile 和 Load(byte[]) ,您最終會(huì)得到每個(gè)程序集的單獨(dú) ALC(請(qǐng)注意)。這樣可以實(shí)現(xiàn)隔離,盡管它可能會(huì)使管理更加尷尬。
要解析依賴(lài)關(guān)系,請(qǐng)?zhí)幚?AppDomain 的解析事件,該事件在所有 ALC 上觸發(fā):
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
string fullAssemblyName = args.Name;
// return an Assembly object or null
...
};
args 變量還包括一個(gè)名為 請(qǐng)求程序集 ,它告訴您哪個(gè)程序集觸發(fā)了解析。
找到程序集后,可以調(diào)用程序集 。加載文件以加載它。
可以使用 AppDomain.CurrentDomain.GetAssemblies() 枚舉已加載到當(dāng)前應(yīng)用程序域中的所有程序集。這也適用于 .NET 5,它等效于以下內(nèi)容:
AssemblyLoadContext.All.SelectMany (a => a.Assemblies)
為了充分演示本節(jié)中介紹的概念,讓我們編寫(xiě)一個(gè)插件系統(tǒng),該系統(tǒng)使用可卸載的 ALC 來(lái)隔離每個(gè)插件。
我們的演示系統(tǒng)最初將包含三個(gè) .NET 項(xiàng)目:
插件.通用 (庫(kù))
定義插件將實(shí)現(xiàn)的接口
資本化器(圖書(shū)館)
將文本大寫(xiě)的插件
Plugin.Host (控制臺(tái)應(yīng)用程序)
查找和調(diào)用插件
假設(shè)項(xiàng)目駐留在以下目錄中:
c:\source\PluginDemo\Plugin.Common
c:\source\PluginDemo\Capitalizer
c:\source\PluginDemo\Plugin.Host
所有項(xiàng)目都將引用 Plugin.Common 庫(kù),并且不會(huì)有其他項(xiàng)目間引用。
如果 Plugin.Host 引用 Capitalizer,我們就不會(huì)編寫(xiě)插件系統(tǒng);中心思想是插件是在 Plugin.Host 和 Plugin.Common 發(fā)布后由第三方編寫(xiě)的。
如果您使用的是 Visual Studio,為了本演示,將所有三個(gè)項(xiàng)目放入一個(gè)解決方案中會(huì)很方便。如果這樣做,請(qǐng)右鍵單擊 Plugin.Host 項(xiàng)目,選擇“構(gòu)建依賴(lài)項(xiàng)”>“項(xiàng)目依賴(lài)項(xiàng)”,然后勾選 Capitalizer 項(xiàng)目。這會(huì)強(qiáng)制 Capitalizer 在運(yùn)行 Plugin.Host 項(xiàng)目時(shí)構(gòu)建,而不添加引用。
讓我們從Plugin.Common開(kāi)始。我們的插件將執(zhí)行一個(gè)非常簡(jiǎn)單的任務(wù),即轉(zhuǎn)換字符串。以下是我們?nèi)绾味x接口:
namespace Plugin.Common
{
public interface ITextPlugin
{
string TransformText (string input);
}
}
這就是Plugin.Common的全部?jī)?nèi)容。
我們的 Capitalizer 插件將引用 Plugin.Common 并包含一個(gè)類(lèi)。現(xiàn)在,我們將保持邏輯簡(jiǎn)單,以便插件沒(méi)有額外的依賴(lài)項(xiàng):
public class CapitalizerPlugin : Plugin.Common.ITextPlugin
{
public string TransformText (string input) => input.ToUpper();
}
如果同時(shí)生成這兩個(gè)項(xiàng)目并查看 Capitalizer 的輸出文件夾,您將看到以下兩個(gè)程序集:
Capitalizer.dll // Our plug-in assembly
Plugin.Common.dll // Referenced assembly
Plugin.Host 是一個(gè)具有兩個(gè)類(lèi)的控制臺(tái)應(yīng)用程序。第一個(gè)類(lèi)是用于加載插件的自定義 ALC:
class PluginLoadContext : AssemblyLoadContext
{
AssemblyDependencyResolver _resolver;
public PluginLoadContext (string pluginPath, bool collectible)
// Give it a friendly name to help with debugging:
: base (name: Path.GetFileName (pluginPath), collectible)
{
// Create a resolver to help us find dependencies.
_resolver = new AssemblyDependencyResolver (pluginPath);
}
protected override Assembly Load (AssemblyName assemblyName)
{
// See below
if (assemblyName.Name == typeof (ITextPlugin).Assembly.GetName().Name)
return null;
string target = _resolver.ResolveAssemblyToPath (assemblyName);
if (target != null)
return LoadFromAssemblyPath (target);
// Could be a BCL assembly. Allow the default context to resolve.
return null;
}
protected override IntPtr LoadUnmanagedDll (string unmanagedDllName)
{
string path = _resolver.ResolveUnmanagedDllToPath (unmanagedDllName);
return path == null
? IntPtr.Zero
: LoadUnmanagedDllFromPath (path);
}
}
在構(gòu)造函數(shù)中,我們傳入主插件程序集的路徑以及一個(gè)標(biāo)志,以指示我們是否希望 ALC 是可收集的(以便可以卸載它)。
Load 方法是我們處理依賴(lài)項(xiàng)解析的地方。所有插件都必須引用 Plugin.Common,以便它們可以實(shí)現(xiàn) ITextPlugin 。這意味著 Load 方法將在某個(gè)時(shí)候觸發(fā)以解析 Plugin.Common。我們需要小心,因?yàn)椴寮妮敵鑫募A可能不僅包含 .dll,還包含它自己的 副本。如果我們要把 的副本加載到 PluginLoadContext 中,我們最終會(huì)得到程序集的兩個(gè)副本:一個(gè)在主機(jī)的默認(rèn)上下文中,另一個(gè)在插件的 PluginLoadContext 中。程序集將不兼容,主機(jī)會(huì)抱怨插件沒(méi)有實(shí)現(xiàn) ITextPlugin!
為了解決這個(gè)問(wèn)題,我們顯式檢查此條件:
if (assemblyName.Name == typeof (ITextPlugin).Assembly.GetName().Name)
return null;
返回 null 允許主機(jī)的默認(rèn) ALC 改為解析程序集。
我們可以返回typeof(ITextPlugin),而不是返回null。組裝 ,它也可以正常工作。我們?nèi)绾未_定 ITextPlugin 將在主機(jī)的 ALC 上解析,而不是在我們的 PluginLoadContext 上解析?請(qǐng)記住,我們的 PluginLoadContext 類(lèi)是在 Plugin.Host 程序集中定義的。因此,從此類(lèi)靜態(tài)引用的任何類(lèi)型都將在 Plugin.Host 的 ALC 上觸發(fā)程序集解析。
檢查公共程序集后,我們使用 AssemblyDependencyResolver 來(lái)查找插件可能具有的任何私有依賴(lài)項(xiàng)。(現(xiàn)在,不會(huì)有。
請(qǐng)注意,我們還重寫(xiě)了 LoadUnamangedDll 方法。這可確保如果插件具有任何非托管依賴(lài)項(xiàng),這些依賴(lài)項(xiàng)也將正確加載。
在 Plugin.Host 中編寫(xiě)的第二個(gè)類(lèi)是主程序本身。為簡(jiǎn)單起見(jiàn),讓我們對(duì) Capitalizer 插件的路徑進(jìn)行硬編碼(在現(xiàn)實(shí)生活中,您可能會(huì)通過(guò)在已知位置查找 DLL 或從配置文件中讀取來(lái)發(fā)現(xiàn)插件的路徑):
class Program
{
const bool UseCollectibleContexts = true;
static void Main()
{
const string captializer = @"C:\source\PluginDemo\"
+ @"Capitalizer\bin\Debug\netcoreapp3.0\Capitalizer.dll";
Console.WriteLine (TransformText ("big apple", captializer));
}
static string TransformText (string text, string pluginPath)
{
var alc = new PluginLoadContext (pluginPath, UseCollectibleContexts);
try
{
Assembly assem = alc.LoadFromAssemblyPath (pluginPath);
// Locate the type in the assembly that implements ITextPlugin:
Type pluginType = assem.ExportedTypes.Single (t =>
typeof (ITextPlugin).IsAssignableFrom (t));
// Instantiate the ITextPlugin implementation:
var plugin = (ITextPlugin)Activator.CreateInstance (pluginType);
// Call the TransformText method
return plugin.TransformText (text);
}
finally
{
if (UseCollectibleContexts) alc.Unload(); // unload the ALC
}
}
}
讓我們看一下 TransformText 方法。我們首先為插件實(shí)例化一個(gè)新的 ALC,然后要求它加載主插件程序集。接下來(lái),我們使用 Reflection 來(lái)定位實(shí)現(xiàn) ITextPlugin 的類(lèi)型(我們將在第 中詳細(xì)介紹)。然后,我們實(shí)例化插件,調(diào)用 TransformText 方法,并卸載 ALC。
如果需要重復(fù)調(diào)用 TransformText 方法,更好的方法是緩存 ALC,而不是在每次調(diào)用后卸載它。
下面是輸出:
BIG APPLE
我們的代碼完全能夠解析和隔離依賴(lài)項(xiàng)。為了說(shuō)明這一點(diǎn),讓我們首先添加對(duì) 的 NuGet 引用。您可以通過(guò)Visual Studio UI或?qū)⒁韵略靥砑拥轿募?lái)執(zhí)行此操作:
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.6.2" />
</ItemGroup>
現(xiàn)在,修改資本插件,如下所示:
using Humanizer;
namespace Capitalizer
{
public class CapitalizerPlugin : Plugin.Common.ITextPlugin
{
public string TransformText (string input) => input.Pascalize();
}
}
如果重新運(yùn)行該程序,輸出現(xiàn)在將是這樣的:
BigApple
接下來(lái),我們創(chuàng)建另一個(gè)名為 Pluralizer 的插件。創(chuàng)建一個(gè)新的 .NET 庫(kù)項(xiàng)目,并添加對(duì) 的 NuGet 引用:
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.7.9" />
</ItemGroup>
現(xiàn)在,添加一個(gè)名為 復(fù)數(shù)器插件 。這將類(lèi)似于 資本化插件 ,但我們調(diào)用 Pluralize 方法,改為:
using Humanizer;
namespace Pluralizer
{
public class PluralizerPlugin : Plugin.Common.ITextPlugin
{
public string TransformText (string input) => input.Pluralize();
}
}
最后,我們需要在 Plugin.Host 的 Main 方法中添加代碼來(lái)加載和運(yùn)行 Pluralizer 插件:
static void Main()
{
const string captializer = @"C:\source\PluginDemo\"
+ @"Capitalizer\bin\Debug\netcoreapp3.0\Capitalizer.dll";
Console.WriteLine (TransformText ("big apple", captializer));
const string pluralizer = @"C:\source\PluginDemo\"
+ @"Pluralizer\bin\Debug\netcoreapp3.0\Pluralizer.dll";
Console.WriteLine (TransformText ("big apple", pluralizer));
}
輸出現(xiàn)在將如下所示:
BigApple
big apples
若要完全了解發(fā)生了什么,請(qǐng)將 UseCollectibleContexts 常量更改為 false,并將以下代碼添加到 Main 方法以枚舉 ALC 及其程序集:
foreach (var context in AssemblyLoadContext.All)
{
Console.WriteLine ($"Context: {context.GetType().Name} {context.Name}");
foreach (var assembly in context.Assemblies)
Console.WriteLine ($" Assembly: {assembly.FullName}");
}
在輸出中,您可以看到兩個(gè)不同版本的 Humanizer,每個(gè)版本都加載到自己的 ALC 中:
Context: PluginLoadContext Capitalizer.dll
Assembly: Capitalizer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=...
Assembly: Humanizer, Version=2.6.0.0, Culture=neutral, PublicKeyToken=...
Context: PluginLoadContext Pluralizer.dll
Assembly: Pluralizer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=...
Assembly: Humanizer, Version=2.7.0.0, Culture=neutral, PublicKeyToken=...
Context: DefaultAssemblyLoadContext Default
Assembly: System.Private.CoreLib, Version=4.0.0.0, Culture=neutral,...
Assembly: Host, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
...
即使兩個(gè)插件都使用相同的 Humanizer 版本,隔離單獨(dú)的程序集仍然是有益的,因?yàn)槊總€(gè)程序集都有自己的靜態(tài)變量。
本文分享內(nèi)容來(lái)自圖書(shū)《學(xué)習(xí)OpenCV 4:基于Python的算法實(shí)戰(zhàn)》,該書(shū)內(nèi)容如下:
第1章 OpenCV快速入門(mén);
第2章 圖像讀寫(xiě)模塊imgcodecs;
第3章 核心庫(kù)模塊core;
第4章 圖像處理模塊imgproc(一);
第5章 圖像處理模塊imgproc(二);
第6章 可視化模塊highgui;
第7章 視頻處理模塊videoio;
第8章 視頻分析模塊video;
第9章 照片處理模塊photo;
第10章 2D特征模塊features2d;
第11章 相機(jī)標(biāo)定與三維重建模塊calib3d;
第12章 傳統(tǒng)目標(biāo)檢測(cè)模塊objdetect;
第13章 機(jī)器學(xué)習(xí)模塊ml;
第14章 深度神經(jīng)網(wǎng)絡(luò)模塊dnn
歡迎關(guān)注圖書(shū)《深度學(xué)習(xí)計(jì)算機(jī)視覺(jué)實(shí)戰(zhàn)》與《學(xué)習(xí)OpenCV4:基于Python的算法實(shí)戰(zhàn)》.
圖像格式即圖像文件存放在存儲(chǔ)卡等介質(zhì)上的格式,常用的圖像格式有BMP、JPEG、TIFF、RAW等,受到存儲(chǔ)容量的限制,圖像文件通常都會(huì)經(jīng)過(guò)壓縮再存儲(chǔ),表2.1列舉了OpenCV中圖像讀寫(xiě)支持的格式,本節(jié)對(duì)一些常用格式做簡(jiǎn)單介紹。
(1)BMP格式
Windows Bitmaps格式圖像文件又稱(chēng)位圖,后綴為.bmp或.dib,是Windows系統(tǒng)中最常見(jiàn)的圖像格式,也是Windows環(huán)境中圖像數(shù)據(jù)處理的一種標(biāo)準(zhǔn)格式,因此Windows環(huán)境中的圖像處理軟件都支持BMP格式。
BMP格式采用位映射存儲(chǔ)格式,除了圖像深度可以設(shè)置之外(圖像深度可設(shè)置為1bit、4bit、8bit及24bit),不采用其他的壓縮方式,因此存儲(chǔ)BMP文件所需占用的空間會(huì)很大。BMP文件進(jìn)行數(shù)據(jù)存儲(chǔ)時(shí),采用從左到右、從下到上的圖像掃描方式。
(2)JPEG格式
JPEG(Joint Photographic Experts Group,聯(lián)合圖像專(zhuān)家小組)是面向連續(xù)色調(diào)靜止圖像的一種壓縮標(biāo)準(zhǔn),該標(biāo)準(zhǔn)由國(guó)際標(biāo)準(zhǔn)化組織(ISO)制訂。JPEG格式圖像文件采用JPEG標(biāo)準(zhǔn),是目前最常用的圖像文件格式,后綴名為.jpg或.jpeg。
JPEG格式是一種先進(jìn)的壓縮格式,可以去除圖像中的冗余數(shù)據(jù),該格式壓縮比通常在10:1到40:1,用JPEG格式存儲(chǔ)的文件大小是其他類(lèi)型文件的1/10~1/20,它能夠?qū)D像壓縮在很小的存儲(chǔ)空間內(nèi)。JPEG格式屬于有損壓縮格式,壓縮比越大,圖像的品質(zhì)就越低,所以如果要求高品質(zhì)圖像,壓縮比則不宜設(shè)置過(guò)高。
JPEG格式可分為標(biāo)準(zhǔn)JPEG、漸進(jìn)式JPEG及JPEG2000三種格式。
標(biāo)準(zhǔn)JPEG格式在網(wǎng)頁(yè)中加載時(shí),需要圖像文件全部加載完畢才能展示圖像。
漸進(jìn)式JPEG在網(wǎng)頁(yè)中加載時(shí)會(huì)先呈現(xiàn)圖像的粗略外觀,然后逐漸呈現(xiàn)圖像細(xì)節(jié),因而稱(chēng)為漸進(jìn)式JPEG。漸進(jìn)式JPEG格式圖像文件比標(biāo)準(zhǔn)JPEG格式文件小,所以網(wǎng)頁(yè)端圖像的展示建議使用這種格式。
JPEG2000是新一代圖像壓縮方法,壓縮品質(zhì)更高,JPEG2000格式文件后綴為.jp2。在無(wú)線傳輸圖像時(shí),經(jīng)常會(huì)遇到信號(hào)不穩(wěn)造成的馬賽克現(xiàn)象,或者圖像位置錯(cuò)亂的問(wèn)題,JPEG2000可以改善這種情況下的圖像傳輸品質(zhì)。JPEG2000的壓縮率比標(biāo)準(zhǔn)JPEG高約30%,支持有損壓縮和無(wú)損壓縮兩種方式,支持漸進(jìn)式傳輸,支持設(shè)定感興趣區(qū)域(即指定圖片上感興趣區(qū)域的壓縮質(zhì)量),而且還可以選擇圖像中的某一部分先行解壓。
(3)PNG格式
PNG(Portable Network Graphics,便攜式網(wǎng)絡(luò)圖形)格式是一種采用無(wú)損壓縮數(shù)據(jù)算法的位圖格式,后綴為.png,該格式當(dāng)初的設(shè)計(jì)意圖是替代有專(zhuān)利的GIF和TIFF文件格式,是目前比較常用的一種圖像格式。
(4)WebP格式
WebP由Google發(fā)布,文件后綴為.webp,提供了有損壓縮與無(wú)損壓縮兩種方式,該格式派生于VP8,支持的最大像素?cái)?shù)量是16383x16383。相較于JPEG格式,WebP格式采用有損壓縮在保持與JPEG格式相同的圖片質(zhì)量的情況下,文件大小會(huì)比JPEG格式文件小,WebP無(wú)損壓縮的圖像比PNG圖像少了45%的存儲(chǔ)大小,因此能夠有效的減少圖片在網(wǎng)絡(luò)上的傳輸時(shí)間。
(5)TIFF 格式
TIFF(Tag Image File Format,標(biāo)簽圖像文件格式)是一種靈活的位圖格式,文件后綴為.tiff或.tif。TIFF格式采用3級(jí)體系結(jié)構(gòu),內(nèi)部結(jié)構(gòu)分為三個(gè)部分:文件頭信息區(qū)、標(biāo)識(shí)信息區(qū)和圖像數(shù)據(jù)區(qū)。文件頭信息區(qū)存儲(chǔ)TIFF文件解析所必需的信息,標(biāo)識(shí)信息區(qū)包含了有關(guān)于圖像的所有信息,圖像數(shù)據(jù)區(qū)存儲(chǔ)圖像信息。
TIFF格式應(yīng)用廣泛,擁有多種壓縮方案,可以描述多種類(lèi)型的圖像,不依賴(lài)于硬件,具有可移植性。
OpenCV進(jìn)行圖像編解碼時(shí),在Windows和MacOSX操作系統(tǒng)上,默認(rèn)情況下使用OpenCV自帶的編解碼器(libjpeg、libpng、libtiff和libjasper),因此在這兩種系統(tǒng)上OpenCV總是可以讀取JPEG、PNG和TIFF格式的圖像文件。在MacOSX上,還可以選擇使用MacOSX本機(jī)的圖像讀取器。
在Linux、BSD或其他類(lèi)Unix的操作系統(tǒng)上,OpenCV尋找與操作系統(tǒng)鏡像一起提供的編解碼器,安裝相關(guān)軟件包時(shí)不要忘記安裝開(kāi)發(fā)文件,例如Debian和Ubuntu中的“l(fā)ibjpeg dev”。
友情鏈接: 餐飲加盟
地址:北京市海淀區(qū) 電話(huà):010- 郵箱:@126.com
備案號(hào):冀ICP備2024067069號(hào)-3 北京科技有限公司版權(quán)所有