當一個程序集被加載使用的時候,出於數據的完整性和安全性考慮,程序集文件(在99.9998%的情況下是.dll文件)會被鎖定,如果此時你想更新程序集(實際上是替換dll文件),是不可以操作的,這時你得把應用程序退出,替換文件后再啟動程序。
多數情況下這樣做是可行的,只是有時候,比如ASP.NET或一些需要一直運行的服務進程,重啟程序來更新好像不太好。
要是想對程序集進行熱更新,即在程序運行的同時替換文件,有一個大家很熟悉的方案——影像復制,如果你不熟悉.net,你肯定沒聽說過的。當然了,這個叫法也挺難聽的,沒辦法,只好這樣翻譯,原詞是 Shadow Copy ,Shadow是影子,陰影,影像的意思,那也只好這么翻譯了。不過,你不用擔心它很抽象很高端,其實,只要用心學,沒什么東西是攻不克的。
我用一句話來概括一下影子復制(也可以叫拷貝,但我不喜歡拷貝這個詞,很黃很暴力的感覺)——應用程序域在加載程序集時,會把程序集文件復制到另一個地方,再進行加載。這樣一來,當程序集文件被使用時,它鎖定的是復制后的文件,即原始文件我們可以放心地去替換了,等到合適的時間,把應用程序重新啟動一下,再次運行時,就會自動把最新的程序集復制到緩存的目錄下,然后執行最新版本的代碼。最好把這些代碼的調用放到一個新的應用程序域中執行,因為這樣的好處是不用重新啟動應用程序,而只要把某個應用程序域卸載掉再重新創建一個新的,就會自動加載最新的程序集了。而且,通常你都應該這么做的,創建一個應用程序域,在里面執行代碼,執行完了就把應用程序域卸掉,可以節約資源。
應用程序在運行的時候,默認會創建一個應用程序域的,說白了,一個進程中至少會有一個應用程序域,如果你把某段代碼放到一個新的應用程序域中執行,並且你希望執行完后,可以把結果傳回給主應用程序域,那就用老周以前寫過的方法,記得老周前面寫過的,想按引用傳遞對象,就從MarshalByRefObject類派生,想讓對象按值傳遞,就讓它支持序列化。
在創建新的應用程序域時,可以同時傳遞一個SetupInfo對象,這個對象有一個 ShadowCopyFiles 屬性,雖然它定義的類型是 string,但你千萬不要理解錯,不要把一個文件的路徑賦給它。老周以前就見到一位朋友理解錯了,它誤以為這個屬性是用來設置復制程序集文件的緩存路徑,結果代碼寫了老是不行。唉,這就是不看MSDN的下場。
不要亂來,設置復制程序集的緩存目錄是 CachePath 屬性,不是 ShadowCopyFiles 屬性。ShadowCopyFiles 屬性只能用兩個字符串的值,如果要啟用影像復制,就設置為 true,如果想禁用,就設置 false 或者干脆保持默認的null值。也就說,它是一個用字符串表示的 bool 值。
下面,我們用一個例子來表演一下,很精彩的。
首先,弄一個類庫項目,然后在里面寫一個全宇宙最簡單的類。
namespace TestLib { public class Demo { public string Call() { return "Ver - 3"; } } }
而主啟動項目是一個控制台應用,這里,老周希望設置新應用程序域的 PrivateBinPath ,這個屬性可以設置一堆目錄,可以是相對路徑,其實應該是用相對路徑的,因為這個目錄不能亂設的,它必須是應用程序目錄的子目錄。如果是多個目錄,可以用英文的分號(;)來分隔。
ApplicationBase路徑指定的是應用程序,即.exe啟動的目錄,不管你創建多個新的應用程序域,這個目錄都必須指定為當前exe的啟動目錄。否則你試試看,不能運行的,因為應用程序域之間是隔離的,所以在新創建的應用程序域中也必須加載當前exe所在的程序集,這個程序集是必須的,因為它是主入口點。
而 PrivateBinPath 屬性所指定的路徑必須為應用程序目錄的子目錄,比如,我們的項目在Debug模式下,通常是把exe生成到 bin \ Debug目錄下的,所以,你可以在Debug目錄下創建一個子目錄,我這里創了一個,叫ExtDlls,隨后我會把要用到的dll文件放在這個目錄中,並設置 PrivateBinPath = "ExtDlls" ,這樣一來,就算項目不引用這個類庫項目,在運行階段它都會自動到這個 ExtDlls 目錄下去找,找到了就加載,要是找不到就會“呵呵”。
我這個類庫項目名叫 TestLib,為了讓它生成后能夠自動把最新的版本復制到 ExtDlls 目錄中,可以打開類庫項目的項目屬性窗口,切換到【生成事件】頁,並在“后期生成命令行”中輸入以下命令:
copy "$(TargetPath)" "$(SolutionDir)MyApp\bin\Debug\ExtDlls\"
這么一搞,每次我重新生成類庫項目后,就會自動把dll文件復制過去。
好,下面的重點放在主項目上,在代碼中,可以創建一個新的應用程序域,然后調用類庫中的代碼。
AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase; setup.ApplicationName = "ExtFuncs"; setup.PrivateBinPath = "ExtDlls"; setup.ShadowCopyFiles = "true"; AppDomain newDom = AppDomain.CreateDomain("hello", null, setup); newDom.DoCallBack(() => { Type t = Type.GetType("TestLib.Demo, TestLib"); // 獲取公共無參構造函數 ConstructorInfo costr = t.GetConstructor(new Type[] { }); // 調用構造函數,創建類型實例 object instance = costr.Invoke(new object[] { }); // 找到要調用的方法 MethodInfo m = t.GetMethod("Call", BindingFlags.Public | BindingFlags.Instance); // 調用方法,得到返回值 object retval = m.Invoke(instance, new object[] { }); Console.WriteLine($"調用輸出:{retval}"); Console.WriteLine("\n==================================="); // 輸出引用程序集的路徑 var refAsses = AppDomain.CurrentDomain.GetAssemblies(); foreach (var ass in refAsses) { Console.WriteLine("名稱:"+ ass.GetName().Name); Console.WriteLine("路徑:" + ass.Location); Console.WriteLine(); } }); AppDomain.Unload(newDom); //卸載應用程序域
實驗表明,ApplicationName 屬性的值可以隨便寫,但 ApplicationBase 屬性必須是當前應用程序所在目錄。
這里我用的是反射的方法來調用的,DoCallBack 方法允許在另一個應用程序域中執行代碼,代碼內容通過一個委托來關聯。
在反射調用完測試類庫后,我還用這段代碼來輸出新的應用程序域所引用的所有程序集的路徑。
var refAsses = AppDomain.CurrentDomain.GetAssemblies(); foreach (var ass in refAsses) { Console.WriteLine("名稱:"+ ass.GetName().Name); Console.WriteLine("路徑:" + ass.Location); Console.WriteLine(); }
由於這段代碼是在新的應用程序域中執行的,所以 CurrentDomain 屬性所指的是新創建的應用程序域,而不是進程運行時創建的默認域。
之所以要在反射之后輸出路徑是因為,應用程序域是動態加載程序集,即當你用到類庫中的類型時才會加載,如果不訪問類庫中的任何東西,是不會加載這個程序集的。
我為啥要輸出路徑呢,就是讓大伙能夠清楚地看到,TestLib 類庫已經被復制到另一個目錄中執行了。請看:
從這個圖你就看到,默認的緩存程序集的路徑是在你的用戶配置目錄下的 AppData \ Local \ assembly 下面。
可能你覺得這個默認的緩存路徑不好,能不能自定義啊?能,前面老周提了一下 CachePath 屬性,對,你給這個屬性分配一個路徑,緩存的程序集就會放到這個自定義路徑中了。比如,我在Debug目錄下新建一個 TempAss 目錄,用來存放臨時復制的程序集。
setup.CachePath = CACHE_PATH;
然后你再看它的路徑。
看,是不是變了?
現在,我們來驗證一下,是不是可以熱更新。
先運行exe,輸出Ver - 1 ,如圖。
好,保持exe運行着,不要關,然后修改一下類庫項目的代碼。
public class Demo { public string Call() { return "Ver - 2"; } }
把 1 改為 2。
重新生成一下類庫項目,它會自動復制到 ExtDlls 目錄。
現在在控制台窗口按除 Esc 以外的任意鍵,就會重新建一個應用程序域,並加載執行類庫代碼,因為我弄了個循環,只有遇到Esc鍵才會退出。
這時候,你看到,輸出的內容變了。
不用退出應用程序,就能實現程序集文件的替換了,這對於服務應用特別好使。
為了寫代碼有智能提示,如果我不想用反射呢,而是直接在VS中引用類庫項目呢,試試,引用之后,把所TestLib屬性中的“復制本地”改為false,因為 ExtDlls 目錄下已經有文件了,不必再復制了,在新的應用程序域中執行時,會自動搜索。
然后把DoCallBack 方法中的代碼改一下:
newDom.DoCallBack(() => { TestLib.Demo dm = new TestLib.Demo(); Console.WriteLine($"輸出:{dm.Call()}"); });
現在代碼就變得簡單多了,是吧,才兩行就完事了。
那能不能運行呢,當然能了。看。
怎么樣,牛逼烘烘吧。
好了,老周的芹菜炒魚蛋飯做好了,肚子餓了,開飯了。