天天看點

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

通過應用程式域AppDomain加載和解除安裝程式集

微 軟裝配車的大門似乎隻為貨物裝載敞開大門,卻将解除安裝勞工拒之門外。車門的鑰匙隻有一把,若要獲得還需要你費一些心思。我在學習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不相同了。

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

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

說明:

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;

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

public RemoteLoader SetRemoteLoaderObject(string dllName)

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

{

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    AppDomainSetup setup = new AppDomainSetup();            

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    setup.ShadowCopyFiles = "true";

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    domain = AppDomain.CreateDomain(dllName,null,setup);

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    domains.Add(dllName,domain);    

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    try

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

                rl = (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap(

                "AssemblyLoader.dll","AssemblyLoader.RemoteLoader"); 

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    }

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    catch

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        throw new Exception();

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

}

代碼中的變量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類的構造函數有參數,則方法應改為:

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

object[] parms = 

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

{dllName};

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

BindingFlags bindings = BindingFlags.CreateInstance |

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

BindingFlags.Instance | BindingFlags.Public;

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

rl = (AssemblyLoader.RemoteLoader)domain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.RemoteLoader",true,bindings,

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

null,parms,null,null,null);

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

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

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

public Assembly LoadAssembly(string dllName)

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        SetRemoteLoaderObject(dllName);

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        return rl.LoadAssembly(dllName);

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    catch (Exception)

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        throw new AssemblyLoadFailureException();

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

public void Unload(string dllName)

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    if (domains.ContainsKey(dllName))

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        AppDomain appDomain = (AppDomain)domains[dllName];

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        AppDomain.Unload(appDomain);

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        domains.Remove(dllName);

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    }            

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

當我們調用Unload方法時,則程式域domain加載的程式集也将随着而被解除安裝。LoadAssembly方法中的異常AssemblyLoadFailureException為自定義異常:

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    public class AssemblyLoadFailureException:Exception

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        public AssemblyLoadFailureException():base()

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

{            

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        }

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        public override string Message

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

            get

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

                return "Assembly Load Failure";

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

            }

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

既然在Loader類獲得的RemoteLoader類執行個體必須通過代理的方式,是以該類對象必須支援被序列化。是以我們可以令該類派生MarshalByRefObject。RemoteLoader類的代碼:

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

    public class RemoteLoader:MarshalByRefObject

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        public RemoteLoader(string dllName)

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

            if (assembly == null)

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

                assembly = Assembly.LoadFrom(dllName);

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        }        

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        private Assembly assembly = null;

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

        public Assembly LoadAssembly(string dllName)

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

            try

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

                assembly = Assembly.LoadFrom(dllName);                

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

                return assembly;

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

            catch (Exception)

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

                throw new AssemblyLoadFailureException();

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)
通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

通過上述的兩個類,我們就可以實作程式集的加載和解除安裝。另外,為了保證應用程式域的對象在記憶體中被清除,應該令這兩個類都實作IDisposable接口,和實作Dispose()方法。

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

不過通常我們在RemoteLoader類中,要實作的方法并非傳回一個Assembly對象,而是通過反射加載程式集後,建立該程式集的對象。由 于類對象都為object類型,此時序列化就不會出現問題。在我的項目中,因為要獲得程式集的版本号,比較版本号在确定是否需要更新,是以我在 RemoteLoader類中,隻需要在加載程式集後,傳回程式集的版本号字元串類型就可以了。字元串類型是絕對支援序列化的。

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

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

通過應用程式域AppDomain加載和解除安裝程式集(轉自張逸)

同時,大家也可以測試一下,直接加載和通過AppDomain加載,删除程式集檔案時會有什麼差別?

作者:KKcat