AppDomain和動態加載 轉


AppDomain和動態加載

下載 supergraphfiles.exe 示例文件。 <?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

應用程序體系結構 

在我專攻代碼之前,我想談談我嘗試做的事。您可能記得,SuperGraph 讓您從函數列表中進行選擇。我希望能夠在具體的目錄中放置外接程序程序集,讓 SuperGraph 檢測它們,加載它們,並找到它們中包含的所有函數。 

如果 SuperGraph 自己能完成此操作則不需要單獨的 AppDomain。Assembly.Load() 通常運行良好,但程序集無法獨立卸載(只有 AppDomain 可以卸載)。這意味着如果您正在編寫服務器,而且您希望用戶無需啓動和停止服務器即能更新他們的外接程序,那麼您將無法使用默認的 AppDomain 實現此任務。 

要實現此功能,我們將在一個獨立的 AppDomain 中加載所有外接程序程序集。當添加或修改文件時,我們將卸載 AppDomain,創建新的 AppDomain,然後將當前文件加載到其中。這樣,一切就都完美無缺了。 

創建 AppDomain 

第一項任務是創建 AppDomain。要以正確的方式創建 AppDomain,我們需要向 AppDomain 傳遞一個 AppDomainSetup 對象。一旦您理解了這一切的工作原理,關於這些的文檔就足夠使用了,但是如果您正在試圖理解其工作原理,那麼這些文檔的幫助並不大。當關於該主題的 Google 搜索將上個月的專欄作爲較高的匹配之一返回時,我懷疑我可能有點麻煩了。 

必須處理的基本問題是如何在運行時加載程序集。默認情況下,運行時將查看全局程序集緩存或當前應用程序目錄樹。而我們希望從完全不同的目錄中加載我們的外接程序。 

當您查看 AppDomainSetup 的文檔時,您將發現可以把 ApplicationBase 屬性設置爲要搜索程序集的目錄。然而,我們也需要參考原始的程序目錄,因爲那是 RemoteLoader 類存在的地方。 

AppDomain 的創作者們理解這一點,因此他們已經提供了額外的位置,用於從中搜索程序集。我們將使用 ApplicationBase 引用外接程序目錄,然後將 PrivateBinPath 設置爲指向主應用程序目錄。 

下面是來自 Loader 類的代碼,可實現此功能: 

AppDomainSetup setup = new AppDomainSetup();

setup.ApplicationBase = functionDirectory;

setup.PrivateBinPath = AppDomain.CurrentDomain.BaseDirectory;

setup.ApplicationName = "Graph";

appDomain = AppDomain.CreateDomain("Functions", null, setup);

 

remoteLoader = (RemoteLoader) 

    appDomain.CreateInstanceFromAndUnwrap("SuperGraph.exe",

        "SuperGraphInterface.RemoteLoader"); 

創建 AppDomain 之後,使用 CreateInstanceFromAndUnwrap() 函數在新的應用程序域中創建 RemoteLoader 類的實例。請注意,需要使用類所在的程序集的文件名以及類的全名。 

當執行此調用時,我們返回如同 RemoteLoader 一樣的實例。實際上,它是一個小型代理類,將所有調用轉發到其他 AppDomain 中的 RemoteLoader 實例中。這和 .NET Remoting 使用的是同一種結構。 

程序集綁定日誌查看器 

當您編寫代碼實現此功能時,您會產生錯誤。本文檔對如何調試應用程序並未提供什麼建議,但是如果您知道該向誰詢問,他們將告訴您有關程序集綁定日誌查看器(名爲“fuslogvw.exe”,因爲加載子系統稱爲“fusion”)的信息。運行查看器時,您可以要求它記錄故障,然後當您運行的應用程序出現加載程序集的問題時,您可以刷新查看器,獲得當前情況的詳細信息。 

例如,您會發現 Assembly.Load() 的文件名末尾不需要 .dll,這一點非常有用。您可以從日誌中獲知這一點,因爲它將告訴您它曾試圖加載 f.dll.dll。 

動態加載程序集 

因此,既然我們已經創建了應用程序域,下一步應該搞清楚如何加載組件並從中提取函數。這需要兩段相互獨立的代碼。第一段代碼在目錄中查找文件,然後加載找到的每個文件: 

void LoadUserAssemblies()

{

    availableFunctions = new FunctionList();

    LoadBuiltInFunctions();

 

    DirectoryInfo d = new DirectoryInfo(functionAssemblyDirectory);

    foreach (FileInfo file in d.GetFiles("*.dll"))

    {  

        string filename = file.Name.Replace(file.Extension, "");

        FunctionList functionList = loader.LoadAssembly(filename);

 

        availableFunctions.Merge(functionList);

    }

} 

Graph 類中的函數在外接程序目錄中查找所有的 dll 文件,刪除它們的擴展名,然後告訴加載程序加載它們。返回的函數列表將併入當前的函數列表。 

第二段代碼在 RemoteLoader 類中,它實際加載程序集並查找函數: 

public FunctionList LoadAssembly(string filename)

{

    FunctionList functionList = new FunctionList();

    Assembly assembly = AppDomain.CurrentDomain.Load(filename);

 

    foreach (Type t in assembly.GetTypes())

    {

        functionList.AddAllFromType(t);

    }   

    return functionList;

} 

這段代碼只是對傳入的文件名(實際是程序集名稱)調用 Assembly.Load(),然後將所有有用的函數加載到 FunctionList 實例中返回給調用程序。 

此時,應用程序可以啓動,加載外接程序程序集,然後用戶就可以引用它們。 

重新加載程序集 

下一項任務是能夠按照需要重新加載這些程序集。最終,我們希望能夠自動實現該任務,但是出於測試目的,我將 Reload 按鈕添加到窗體中,以使程序集能夠重新加載。該按鈕的處理程序僅調用 Graph.Reload(),它需要執行以下操作: 

1.       卸載 AppDomain。

2.       創建新的 AppDomain。

3.       在新的 AppDomain 中重新加載程序集。

4.       將圖形線條掛鉤到新創建的 AppDomain。

 

步驟 4 是必需的,因爲 GraphLine 對象包含來自原 AppDomain 的 Function 對象。卸載 AppDomain 後,函數對象無法再被使用。 

爲解決此問題,HookupFunctions() 修改了 GraphLine 對象,使它們從當前應用程序域指向正確的函數。 

代碼如下: 

loader.Unload();

loader = new Loader(functionAssemblyDirectory);

LoadUserAssemblies();

HookupFunctions();

reloadCount++;

 

if (this.ReloadCountChanged != null)

    ReloadCountChanged(this, new ReloadEventArgs(reloadCount)); 

只要執行重新加載操作,最後兩行將引發一個事件。其作用是更新窗體上的重新加載計數器。 

檢測新的程序集 

下一步是能夠檢測在外接程序目錄中顯示的新的或修改過的程序集。該框架提供 FileSystemWatcher 類來實現此功能。下面是我添加到 Graph 類構造函數中的代碼:

watcher = new FileSystemWatcher(functionAssemblyDirectory, "*.dll");

watcher.EnableRaisingEvents = true;

watcher.Changed += new FileSystemEventHandler(FunctionFileChanged);

watcher.Created += new FileSystemEventHandler(FunctionFileChanged);

watcher.Deleted += new FileSystemEventHandler(FunctionFileChanged);

當創建 FileSystemWatcher 類時,我們告訴它要在什麼目錄中查找,要跟蹤哪些文件。EnableRaisingEvents 屬性表示當它檢測到更改時,我們是否需要它發送事件。最後 3 行將事件掛鉤到類中的某個函數。該函數僅僅調用 Reload() 以重新加載程序集。 

這種方法有一些累贅的地方。在更新程序集時,我們必須卸載程序集才能夠加載新的版本,但是添加或刪除文件時不需要卸載程序集。在這種情況下,對所有更改執行此操作的成本並不是很高,而且它使代碼更簡單。 

在構造此代碼之後,我們運行該應用程序,然後嘗試把新的程序集複製到外接程序目錄中。正如我們所希望的那樣,我們獲得了文件更改事件,當重新加載完畢時,新的函數就可供使用。 

然而,當我們試圖更新現有的程序集時,我們遇到了一個問題。運行時已經鎖定該文件,這意味着我們無法將新的程序集複製到外接程序目錄中,我們收到一個錯誤。 

AppDomain 類的設計人員意識到這是一個問題,因此他們提供一種不錯的解決方法。當 ShadowCopyFiles 屬性設置爲 true(字符串 true,不是布爾常數 true。不要問我爲什麼……)時,運行時將把程序集複製到緩存目錄中,然後打開該程序集。這樣,原文件就不會被鎖定,我們也就能更新正在使用的程序集。ASP.NET 使用了這種機制。 

爲了啓用此功能,我在 Loader 類的構造函數中添加了以下行: 

setup.ShadowCopyFiles = "true"; 

然後我重新生成了該應用程序,並得到相同的錯誤。我查看了 ShadowCopyDirectories 屬性的文檔,該文檔明確指出 PrivateBinPath 指定的所有目錄(包括 ApplicationBase 指定的目錄)是陰影複製的(如果未設置此屬性)。記得我是如何說該文檔在這個方面不是很好的嗎? 

有關此屬性的文檔肯定是錯了。我沒有驗證確切的表現方式,但是我可以告訴您 ApplicationBase 目錄中的文件在默認情況下並不是陰影複製的。明確指定目錄可以解決此問題: 

setup.ShadowCopyDirectories = functionDirectory; 

搞明白這一點至少花了我半個小時。 

現在我們可以更新現有文件並將其正確地加載進去。可我剛把這個理順,又遇到了另外一個小的問題。當我們從窗體的按鈕上運行重新加載函數時,重新加載總是和繪製發生在同一個線程中,這意味着在重新加載過程中我們不可能嘗試繪製直線。 

既然我們已經切換到文件更改事件,那麼在卸載 AppDomain 之後和加載新的 AppDomain 之前,有可能會進行繪製。如果發生這種情況,我們會得到一個異常。 

這是傳統的多線程編程問題,使用 C# lock 語句很容易處理。我在繪圖函數和重新加載函數中添加了 lock 語句,這就確保了它們不會同時發生。這就解決了該問題,添加程序集的更新版本將使程序自動切換到函數的新版本。這相當不錯。 

還有一個奇怪的現象。原來用於檢測文件更改的 Win32® 函數發送的更改數量很大,因此對文件做一次更新將發送五個更改事件,程序集也將被重新加載五次。解決方法是編寫更智能的、可以將這些操作組合在一起的 FileSystemWatcher,但是此版本中沒有提供這種解決方法。 

拖放 

將文件複製到目錄中不是很方便,因此我決定在該應用程序中添加拖放功能。實現該任務的第一步是把窗體的 AllowDrop 屬性設置爲 true,這將打開拖放功能。下一步,我將一個例程掛鉤到 DragEnter 事件。當光標在對象上移動進行拖放操作以確定當前對象是否接受拖放時,將調用該事件。 

private void Form1_DragEnter(

    object sender, System.Windows.Forms.DragEventArgs e)

{

    object o = e.Data.GetData(DataFormats.FileDrop);

    if (o != null)

    {

        e.Effect = DragDropEffects.Copy;

    }

    string[] formats = e.Data.GetFormats();

} 

在此處理程序中,我查看是否有可用的 FileDrop 數據(也就是說,文件被拖放到窗口中)。如果有,我把效果設置爲“複製”,這將相應地設置光標,並且如果用戶釋放鼠標按鈕,將發送 DragDrop 事件。該函數中的最後一行完全是出於調試目的,用於查看操作中有哪些可用信息。 

下一項任務是爲 DragDrop 事件編寫處理程序:

private void Form1_DragDrop(

    object sender, System.Windows.Forms.DragEventArgs e)

{

    string[] filenames = (string[]) e.Data.GetData(DataFormats.FileDrop);

    graph.CopyFiles(filenames);

} 

此例程獲得與此操作關聯的數據(文件名數組),將其傳遞到圖形函數,然後圖形函數把文件複製到外接程序目錄中,觸發文件更改事件以便重新加載它們。 

狀態

 

此時,您可以運行該應用程序,把新的程序集拖到程序上,程序將很快加載它們並保持運行。這相當不錯。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章