C#單個程式集程式碼熱更新
有的時候我們想更新我們正在執行中的程式碼,而不想軟體重啟。
微軟提供的標準方法是通過應用程式域來實現程式碼熱更新,意思就是說,把自己想要進行熱更新的程式碼放到另外一個應用程式域中,在檢測到程式碼需要變更的時候,解除安裝掉那個程式域然後重新載入來實現程式碼熱更新。按照微軟的說法,一個應用程式域是無法實現程式碼熱更新的。
但是,一下方法確實是可以在單個應用程式域中實現程式碼熱更新的,本人嘗試過,在公司的上班中,軟體的頻繁重啟讓人很煩,又不能在另外一個程式域中執行,所以嘗試了一下辦法,在一個應用程式域中改變程式碼是可行的。
方法如下:假設程式集A中有類a,程式集B中有類b,b要去呼叫a的方法,這個情況下可以對a的部分程式碼(僅對方法和屬性有效)進行熱更新。第一步是針對類a 進行程式碼重寫,生成一個假的A.dll,但是具有和包含真A中類,欄位,屬性,和方法簽名,把真A的Dll放到另外一個資料夾。當B去呼叫A中方法的時候,假的A會通過位元組陣列的形式將真A儲存在自己的一個Assembly(最好放在一個其他類的靜態變數中,我是那麼做的)變數中,並且檢測真A的檔案改動,當真A發生變化的時候對Assembly變數通過位元組陣列的形式重新賦值。這個樣子就可以就可以實現部分程式碼熱更新。
假A對真A程式碼的重寫原則:假a類中至少要有三個變數,一個object變數,用來存對真a物件的引用。一個Type變數,用來儲存真a的型別資訊,還有真A的Assembly變數。假a在初始化的時候,要初始化剛才的那三個變數,對假a方法的呼叫通過反射轉換為對真a物件(object變數,用來存對真a物件的引用)的呼叫。並且把真a方法的返回結果返回回去在真A變化之後,假A重新載入真A,這個時間點之後,在被建立的假a執行的程式碼已經是程式碼變化之後真A的程式碼,就可以實現程式碼特更新。
把真A程式碼重構並且生成編譯出假A是一個很費勁容易出錯的過程,如下是我生成假A的程式碼邏輯,應該是存在某些問題的(嘿嘿,主要是說明方法)
using Microsoft.CSharp; using System; using System.CodeDom; using System.CodeDom.Compiler; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace DllLoad { class Core2 { List<string> dlls = new List<string>(); string outpath = string.Empty; public EventHandler CompileCompleted; public Core2() { } public void SetInfo(List<string> dlls, string outpath) { this.dlls = dlls; this.outpath = outpath; } public void StartCompile() { try { dlls.ForEach(o => { Assembly assem = Assembly.LoadFrom(o); Compile(assem, outpath); }); } catch (Exception e) { KeyValuePair<bool, string> error = new KeyValuePair<bool, string>(false, e.Message); CompileCompleted(error, null); return; } KeyValuePair<bool, string> correct = new KeyValuePair<bool, string>(true, null); CompileCompleted(correct, null); } public void Compile(Assembly assembly, string dllOutPath) { CodeCompileUnit compunit = new CodeCompileUnit(); Dictionary<string, CodeNamespace> nameDic = new Dictionary<string, CodeNamespace>(); FileInfo assemblyInfo = new FileInfo(assembly.Location); string fileName = assemblyInfo.Name; foreach (var typeinfo in assembly.DefinedTypes) { string namespa = typeinfo.Namespace; var space = new CodeNamespace(namespa); if (!nameDic.ContainsKey(namespa)) { nameDic.Add(namespa, space); compunit.Namespaces.Add(space); space.Imports.AddRange((new string[] {"System","System.Linq","System.Reflection","System.Text","Free" }).ToList().Select(o => new CodeNamespaceImport(o)).ToArray()); } #region 新增類中的變數 CodeTypeDeclaration clas = new CodeTypeDeclaration(typeinfo.Name); CodeMemberField obj = new CodeMemberField(typeof(object), "obj"); obj.InitExpression = new CodePrimitiveExpression(null); CodeMemberField self = new CodeMemberField(typeof(Type), "self"); self.InitExpression = new CodePrimitiveExpression(null); CodeMemberField ass = new CodeMemberField(typeof(Assembly), "ass"); ass.InitExpression = new CodePrimitiveExpression(null); clas.Members.Add(obj); clas.Members.Add(self); clas.Members.Add(ass); #endregion //新增建構函式 CodeConstructor constructor = new CodeConstructor() { Attributes = MemberAttributes.Public }; constructor.Statements.Add(new CodeSnippetExpression("Inite()")); clas.Members.Add(constructor); //新增Inite() 函式 CodeMemberMethod Inite = new CodeMemberMethod() { Name = "Inite", ReturnType = new CodeTypeReference(typeof(void)) }; Inite.Attributes = MemberAttributes.Private; Inite.Statements.Add(new CodeSnippetStatement(@" ass = FastLoadAssembly.GetAssembly(); self = ass.GetType(""{0}""); obj = Activator.CreateInstance(self); ".Replace("{0}", typeinfo.AsType().FullName))); clas.Members.Add(Inite); clas.BaseTypes.AddRange(new List<CodeTypeReference> { new CodeTypeReference(typeinfo.BaseType) }.Union(typeinfo.GetInterfaces().Select(o => new CodeTypeReference(o))).Distinct().ToArray()); space.Imports.AddRange(new List<string> { typeinfo.Namespace }.Union(typeinfo.GetInterfaces().Select(o => o.Namespace)).Select(o => new CodeNamespaceImport(o)).Distinct().ToArray()); foreach (var met in typeinfo.GetMembers()) { switch (met.MemberType) { case MemberTypes.Method: { MethodInfo info = (MethodInfo)met; if (!info.IsPublic) continue; CodeMemberMethod metho = SolveMethod(met,typeinfo); clas.Members.Add(metho); } break; case MemberTypes.Property: { } break; } } space.Types.Add(clas); } compunit.Namespaces.Add(GetCommonClass(dllOutPath + "\\" + fileName)); CSharpCodeProvider cprovider = new CSharpCodeProvider(); ICodeGenerator gen = cprovider.CreateGenerator(); StringBuilder fileContent = new StringBuilder(); using (StringWriter sw = new StringWriter(fileContent)) { gen.GenerateCodeFromCompileUnit(compunit, sw, new CodeGeneratorOptions());//想把生成的程式碼儲存為cs檔案 } string reslut = fileContent.ToString(); ICodeCompiler compiler = cprovider.CreateCompiler(); //編譯引數 CompilerParameters cp = new CompilerParameters(); // 把當前應用引用的程式集全部新增進來 cp.ReferencedAssemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies().Select(o => o.Location).Distinct().ToArray()); cp.OutputAssembly = dllOutPath + "\\" + fileName; cp.GenerateInMemory = false; //是否只在記憶體中生成 cp.IncludeDebugInformation = true;//包含除錯符號 pdb檔案 cp.GenerateExecutable = false;//生成dll,不是exe cp.WarningLevel = 4; cp.TreatWarningsAsErrors = false; string filePath = dllOutPath + "\\" + fileName + ".cs"; File.WriteAllText(filePath, fileContent.ToString()); CompilerResults cr = compiler.CompileAssemblyFromFile(cp, filePath); //儲存檔案再進行編譯 待會兒除錯就比較方便了 ,可以直接斷點到剛才生成的檔案裡面 // CompilerResults cr = compiler.CompileAssemblyFromDom(cp, compunit); //這樣的生成 不用寫檔案 ,就是除錯麻煩 String outputMessage = ""; foreach (var item in cr.Output) { outputMessage += item + Environment.NewLine;//除錯的最終輸出資訊 } if (cr.Errors.HasErrors)//有編譯錯誤就丟擲異常 { throw new Exception("error:" + Environment.NewLine + outputMessage); } } private CodeMemberMethod SolveMethod(MemberInfo info,TypeInfo type) { if (info.MemberType != MemberTypes.Method) return null; MethodInfo method = (MethodInfo)info; CodeMemberMethod newMethod = new CodeMemberMethod() { Name = method.Name, ReturnType = new CodeTypeReference(method.ReturnType) }; newMethod.Parameters.AddRange( method.GetParameters().Select(o => { var res = new CodeParameterDeclarationExpression { Type = new CodeTypeReference(o.ParameterType), Name = o.Name }; return res; }).ToArray() ); switch (method.Attributes) { case MethodAttributes.Public | MethodAttributes.HideBySig: { if(type.DeclaredMembers.Contains(info) ) newMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final; else newMethod.Attributes = MemberAttributes.Public | MemberAttributes.New | MemberAttributes.Final; } break; case MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual: newMethod.Attributes = MemberAttributes.Public | MemberAttributes.Override; break; case MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.NewSlot: newMethod.Attributes = MemberAttributes.Public; break; } string statement = @" MethodInfo info= self.GetMethod(""{0}"");" + "\r\nObject result = info.Invoke(obj, "; string param = string.Join(",", method.GetParameters().Select(o => o.Name).ToList()); string expression = statement.Replace("{0}", info.Name); if (!string.IsNullOrWhiteSpace(param)) newMethod.Statements.Add(new CodeSnippetStatement(expression + "new object[] { " + param + " });")); else newMethod.Statements.Add(new CodeSnippetStatement(expression + "null );")); if (method.ReturnType != typeof(void)) { string returnstr = @"return ({0})result;"; newMethod.Statements.Add(new CodeSnippetStatement(string.Format(returnstr, method.ReturnType.ToString()))); } return newMethod; } private CodeNamespace GetCommonClass(string dllpath) { CodeNamespace space = new CodeNamespace("Free"); CodeTypeDeclaration common = new CodeTypeDeclaration("FastLoadAssembly"); space.Types.Add(common); space.Imports.AddRange((new string[] {"System.Reflection","System.IO" }).ToList().Select(o => new CodeNamespaceImport(o)).ToArray()); common.Attributes = MemberAttributes.Assembly; CodeMemberField assembly = new CodeMemberField(typeof(Assembly), "assembly"); assembly.Attributes = MemberAttributes.Private | MemberAttributes.Static; common.Members.Add(assembly); CodeMemberField path = new CodeMemberField(typeof(string), "path"); path.InitExpression = new CodePrimitiveExpression(dllpath); path.Attributes = MemberAttributes.Private | MemberAttributes.Const; common.Members.Add(path); CodeMemberField watch = new CodeMemberField(typeof(FileSystemWatcher), "watcher"); watch.Attributes = MemberAttributes.Private | MemberAttributes.Static; common.Members.Add(watch); CodeMemberMethod Load = new CodeMemberMethod() { Name = "Load", ReturnType = new CodeTypeReference(typeof(void)) }; Load.Statements.Add(new CodeSnippetStatement(@" byte[] buffer; FileStream stream = File.OpenRead(path); buffer = new byte[stream.Length]; stream.Read(buffer,0,(int)stream.Length); stream.Close(); assembly = Assembly.Load(buffer); ")); Load.Attributes = MemberAttributes.Private | MemberAttributes.Static; CodeMemberMethod GetAssembly = new CodeMemberMethod() { Name = "GetAssembly", ReturnType = new CodeTypeReference(typeof(Assembly)) }; GetAssembly.Statements.Add(new CodeSnippetStatement(@" IniteWatcher(); if (assembly == null) Load(); return assembly; ")); GetAssembly.Attributes = MemberAttributes.Public | MemberAttributes.Static; CodeMemberMethod IniteWatcher = new CodeMemberMethod() { Name = "IniteWatcher", ReturnType = new CodeTypeReference(typeof(void)) }; IniteWatcher.Statements.Add(new CodeSnippetStatement(@" if (watcher==null) { FileInfo info = new FileInfo(path); watcher = new FileSystemWatcher(); watcher.Filter = info.Name; watcher.Path = info.DirectoryName; watcher.EnableRaisingEvents = true; watcher.Created += (s, o) => { Load(); }; } ")); IniteWatcher.Attributes = MemberAttributes.Private | MemberAttributes.Static; common.Members.Add(IniteWatcher); common.Members.Add(GetAssembly); common.Members.Add(Load); return space; } } }