通過應用程序域AppDomain加載和卸載程序集

在程序中需要卸載之前加載的Assembly,不知道在.Net中如何實現,在網上看到了這篇文章,感覺寫的不錯,在此貼出來,謝謝原創作者。


微軟裝配車的大門似乎只爲貨物裝載敞開大門,卻將卸載工人拒之門外。車門的鑰匙只有一把,若要獲得還需要你費一些心思。我在學習Remoting的時候,就遇到一個擾人的問題,就是Remoting爲遠程對象僅提供Register的方法,如果你要註銷時,只有另闢蹊徑。細心的開發員,會發現Visual Studio.Net中的反射機制,同樣面臨這個問題。你可以找遍MSDN的所有文檔,在Assembly類中,你永遠只能看到Load方法,卻無法尋覓到Unload的蹤跡。難道我們裝載了程序集後,就不能再將它卸載下來嗎?

想一想這樣一個場景。你通過反射動態加載了一個dll文件,如今你需要在未關閉程序的情況下,刪除或覆蓋該文件,那麼結果會怎樣?很遺憾,系統會提示你無法訪問該文件。事實上該文件正處於被調用的狀態,此時要對該文件進行修改,就會出現爭用的情況。

顯然,爲程序集提供卸載功能是很有必要的,但爲什麼微軟在其產品中不提供該功能呢?CLR 產品單元經理(Unit Manager) Jason Zander 在文章 Why isn't there an Assembly.Unload method? 中解釋了沒有實現該功能的原因。Flier_Lu在其博客裏(Assembly.Unload)有詳細的中文介紹。文中介紹瞭解決卸載程序集的折中方法。Eric Gunnerson在文章《AppDomain 和動態加載》中也提到:Assembly.Load() 通常運行良好,但程序集無法獨立卸載(只有 AppDomain 可以卸載)。Enrico Sabbadin 在文章《Unload Assemblies From an Application Domain》也有相關VB.Net實現該功能的相關說明。

尤其是Flier_Lu的博客裏已經有了很詳細的代碼。不過,這些代碼沒有詳細地說明。我在我的項目中也需要這一項功能。這段代碼給了我很大的提示。但在實際的實現中,還是遇到一些具體的問題。所以我還是想再談談我的體會。

通過AppDomain來實現程序集的卸載,這個思路是非常清晰的。由於在程序設計中,非特殊的需要,我們都是運行在同一個應用程序域中。由於程序集的卸載存在上述的缺陷,我們必須要關閉應用程序域,方可卸載已經裝載的程序集。然而主程序域是不能關閉的,因此唯一的辦法就是在主程序域中建立一個子程序域,通過它來專門實現程序集的裝載。一旦要卸載這些程序集,就只需要卸載該子程序域就可以了,它並不影響主程序域的執行。

不過現在看來,最主要的問題不是子程序域如何創建,關鍵是我們必須實現一種機制,來達到兩個程序域之間完成通訊的功能。如果大家熟悉Remoting,就會想到這個問題不是和Remoting的機制有幾分相似之處嗎?那麼答案就可以呼之欲出了,對了,就是使用代理的方法!不過與Remoting不同的是兩個程序域之間的關係。因爲子程序域是在主程序域中建立的,因此對該域的控制顯然就與Remoting不相同了。

我想先用一副圖來表述實現的機制:

說明:
1、Loader類提供創建子程序域和卸載程序域的方法;
2、RemoteLoader類提供裝載程序集方法;
3、Loader類獲得RemoteLoader類的代理對象,並調用RemoteLoader類的方法;
4、RemoteLoader類的方法在子程序域中完成;
5、Loader類和RemoteLoader類均放在AssemblyLoader.dll程序集文件中;

我們再來看代碼:
Loader類:

SetRemoteLoaderObject()方法:

  private AppDomain domain = null;
  private Hashtable domains = new Hashtable();  
  private RemoteLoader rl = null;
public RemoteLoader SetRemoteLoaderObject(string dllName)
{
    AppDomainSetup setup 
= new AppDomainSetup();            
    setup.ShadowCopyFiles 
= "true";
    domain 
= AppDomain.CreateDomain(dllName,null,setup);
            
    domains.Add(dllName,domain);    
    
try
    
{
                rl = (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap(
                "AssemblyLoader.dll","AssemblyLoader.RemoteLoader");         
    }

    
catch
    
{
        
throw new Exception();
    }

}


代碼中的變量rl爲RemoteLoader類對象,在Loader類中是其私有成員。SetRemoteLoaderObject()方法實際上提供了兩個功能,一是創建了子程序域,第二則是獲得了RemoteLoader類對象。

請大家一定要注意語句:
rl = (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.RemoteLoader");

這條語句就是實現兩個程序域之間通訊的關鍵。因爲Loader類是在主程序域中,RemoteLoader類則是在子程序域中。如果我們在Loader類即主程序域中顯示實例化RemoteLoader類對象rl,此時調用rl的方法,實際上是在主程序域中調用的。因此,我們必須使用代理的方式,來獲得rl對象,這就是CreateInstanceFromAndUnwrap方法的目的。其中參數一爲要創建類對象的程序集文件名,參數二則是該類的類型名。

CreateCreateInstanceFromAndUnwrap方法有多個重載。代碼中的調用方式是當RemoteLoader類爲默認構造函數時的其中一種重載。如果RemoteLoader類的構造函數有參數,則方法應改爲:

object[] parms = {dllName};
BindingFlags bindings 
= BindingFlags.CreateInstance |
BindingFlags.Instance 
| BindingFlags.Public;
rl 
= (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.RemoteLoader",true,bindings,
null,parms,null,null,null);

詳細的調用方式可以參考MSDN。

以下Loader類的Unload方法和LoadAssembly方法():

public Assembly LoadAssembly(string dllName)
{
    
try
    
{
        SetRemoteLoaderObject(dllName);
        
return rl.LoadAssembly(dllName);
    }

    
catch (Exception)
    
{
        
throw new AssemblyLoadFailureException();
    }

}
public void Unload(string dllName)
{
    
if (domains.ContainsKey(dllName))
    
{
        AppDomain appDomain 
= (AppDomain)domains[dllName];
        AppDomain.Unload(appDomain);
        domains.Remove(dllName);
    }
            
}

當我們調用Unload方法時,則程序域domain加載的程序集也將隨着而被卸載。LoadAssembly方法中的異常AssemblyLoadFailureException爲自定義異常:

    public class AssemblyLoadFailureException:Exception
    
{
        
public AssemblyLoadFailureException():base()
        
{            
        }


        
public override string Message
        
{
            
get
            
{
                
return "Assembly Load Failure";
            }

        }


    }


既然在Loader類獲得的RemoteLoader類實例必須通過代理的方式,因此該類對象必須支持被序列化。所以我們可以令該類派生MarshalByRefObject。RemoteLoader類的代碼:

    public class RemoteLoader:MarshalByRefObject
    
{
        
public RemoteLoader(string dllName)
        
{
            
if (assembly == null)
            
{
                assembly 
= Assembly.LoadFrom(dllName);
            }

        }
        

        
private Assembly assembly = null;

        
public Assembly LoadAssembly(string dllName)
        
{
            
try
            
{
                assembly 
= Assembly.LoadFrom(dllName);                
                
return assembly;
            }

            
catch (Exception)
            
{
                
throw new AssemblyLoadFailureException();
            }

        }

    }


通過上述的兩個類,我們就可以實現程序集的加載和卸載。另外,爲了保證應用程序域的對象在內存中被清除,應該令這兩個類都實現IDisposable接口,和實現Dispose()方法。

然而在實際的操作過程中,我發現在RemoteLoader類的LoadAssembly方法,是存在遺患的。在我的LoadAssembly方法中,會返回一個Assembly對象。令我百思不得其解的是,雖然都是Assembly對象,但在加載某些程序集並返回Assembly時,在Loader類中會拋出SerializationException異常,並報告反序列化的對象狀態不足。這個異常是在序列化獲反序列化過程中發生的。我反覆比較了兩個程序集,一個可以正常加載並序列化,一個會拋出如上異常。會拋出異常的程序集並沒有什麼特殊之處,且我在程序中的其他地方也沒有重複加載該程序集。這是一個疑問!!

不過通常我們在RemoteLoader類中,要實現的方法並非返回一個Assembly對象,而是通過反射加載程序集後,創建該程序集的對象。由於類對象都爲object類型,此時序列化就不會出現問題。在我的項目中,因爲要獲得程序集的版本號,比較版本號在確定是否需要更新,因此我在RemoteLoader類中,只需要在加載程序集後,返回程序集的版本號字符串類型就可以了。字符串類型是絕對支持序列化的。

AssemlbyLoader.Dll的源代碼可以點擊這裏獲得。在應用程序中,顯示添加對該程序集的引用,然後實例化Loader類對象,來調用該方法即可。我還做了一個簡單的測試程序,用的是LoadAssembly方法。大家可以測試一下,是否如我所說,對於某些程序集,可能會拋出序列化的異常!?

測試的代碼請點擊這裏獲得,測試界面如下:

同時,大家也可以測試一下,直接加載和通過AppDomain加載,刪除程序集文件時會有什麼區別?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章