一. 面臨的問題
開發插件系統的主要優勢是擴展性,我們不需要為系統模塊的集成再多費腦筋,但這也帶來了額外的問題。通常,系統需要在每次啟動時搜索固定目錄下的符合要求的插件。但是,當系統變得越來越龐大,所引用的dll文件越來越多時,就會出現很嚴重的問題:開啟時間慢,性能差,用戶體驗降低,尤其是在調試程序時,會浪費大量寶貴的時間。
我確確實實的面臨了這樣的問題,有興趣的讀者可以看看我的插件系列文章的前幾篇,這兩天痛定思痛,決心提升系統搜索插件的性能。
我們先看一段普通的搜索插件的代碼:
1 public void GetAllPluginInPath(string Path, string InterFaceName) 2 { 3 var DllFileName = from file in Directory.GetFileSystemEntries(Path) 4 where file.Contains(".dll") 5 select file; 6 7 8 9 //string[] DllFileName = Directory.GetFileSystemEntries(Path); 10 Type[] types; 11 foreach (string file in DllFileName) 12 { 13 14 if (System.IO.Path.GetExtension(file) == ".dll") 15 { 16 Assembly assembly; 17 18 try 19 { 20 assembly = Assembly.LoadFrom(file); 21 } 22 catch 23 { 24 continue; 25 } 26 27 try 28 { 29 types = assembly.GetTypes(); 30 } 31 catch (Exception ex) 32 { 33 continue; 34 } 35 36 foreach (Type type in types) 37 { 38 if (type.GetInterface(InterFaceName) != null && !type.IsAbstract) 39 { 40 object thisObject = Activator.CreateInstance(type); 41 42 43 IXPlugin rc1 = thisObject as IXPlugin; 44 45 46 //如果要在啟動時被加載 47 if (rc1 != null && rc1.isStartLoaded) 48 { 49 AddPlugin(rc1); 50 } 51 } 52 } 53 } 54 } 55 }
造成啟動慢的主要原因有:
1. 目錄下包含大量dll文件(這是因為項目引用了大量第三方庫),它們並不包含我們開發的組件,卻白白浪費大量搜索時間。有些dll文件不是托管dll,在獲取程序集時還會拋出異常,直接捕獲后,也會造成時間損失。
2. 上述代碼僅搜索了滿足一種接口規范的插件, (見函數的參數InterFaceName)。如果不止一種插件類型,那么可能要做很多次這樣的查找,對性能的影響更大。
3. 為了獲取插件的一些信息(比如是否要在啟動時加載),不得不實例化其對象獲取字段,這種性能損失也是不能承受的。
二. 解決途徑
找到問題,我們對症下葯:
1.成熟的軟件系統采用了插件樹的機制,將插件存儲為樹結構,包含父子關系,這樣能盡可能的提升搜索和加載性能,同時方便管理,比如Ecilpse。 但是,這種復雜的插件管理機制可能不適用於我們開發的輕量級系統,因此我們僅僅考慮扁平化的插件結構。
2. 雖然插件的數量是經常變化的,但通常加載的dll文件種類很少變化。我們可以考慮把實際包含所需插件的dll文件名列表存儲起來,從而在搜索時僅搜索這些固定的dll文件,提升性能。
3. 插件的種類可能多種多樣,所以我們希望能一次性獲得全部類型的插件。
4. 采用.NET4.0的並行庫機制實現插件的並行搜索。
三. 插件結構表述
該部分已經在我的插件系列文章的.NET插件系統之二——不實例化獲取插件信息和可視化方法 中進行了描述,主要是標記接口名稱的InterfaceAttribute 和 標記實現接口的插件的XFrmWorkAttribute,你需要在插件接口和插件實現的類上添加這兩類標識,此處不再贅述。
我們定義兩個數據結構存儲插件名稱和插件信息:
/// <summary> /// 為了便於序列化而簡化的插件接口數據類型,是簡化的InterfaceAttribute /// </summary> [Serializable] public class PluginNameLite { public string myName { get; set; } public SearchStrategy mySearchStrategy { get; set; } public string detailInfo { get; set; } public PluginNameLite() { } public PluginNameLite(InterfaceAttribute attr) { myName = attr.myName; mySearchStrategy = attr.mySearchStrategy; detailInfo = attr.DetailInfo; } } /// <summary> /// 插件集合 /// </summary> public class PluginCollection : ObservableCollection<XFrmWorkAttribute> { public PluginCollection() : base() { } /// <summary> /// 可以被序列化的簡化插件字典,僅包含插件接口名稱和搜索策略 /// </summary> static List<PluginNameLite> myPluginNameList = new List<PluginNameLite>(); /// <summary> /// 插件字典 /// </summary> static Dictionary<Type, PluginCollection> mPluginDictionary = new Dictionary<Type, PluginCollection>();
四. 插件搜索的方法
我們將插件搜索的步驟分為兩步:
1. 搜索所有接口契約(即搜索所有的接口)
/// <summary> /// 獲取所有的插件接口契約名稱 /// </summary> /// <param name="Path"></param> /// <param name="InterFaceName"></param> public static void GetAllPluginName(string folderLocation, bool isRecursiveDirectory) { List<PluginNameLite> mPluginNameList = new List<PluginNameLite>(); //緩存所有插件名稱 if (!isRecursiveDirectory) { try //如果不執行遞歸搜索,則查看在目錄下是否有保存了插件名稱的文件,若有,直接反序列化之,不執行插件名稱搜索 { mPluginNameList = CustomSerializer.Deserialize<List<PluginNameLite>>(folderLocation + "\\PluginLog.xml"); myPluginNameList.AddRange(mPluginNameList); return; } catch (Exception ex) { } } var DllFile = from file in Directory.GetFileSystemEntries(folderLocation) //若無緩存文件,獲取目錄下全部的dll文件執行搜索 where file.Contains(".dll") select file; Parallel.ForEach(DllFile, //並行化處理 file => { Type[] types; Assembly assembly; try { assembly = Assembly.LoadFrom(file); } catch { return; } try { types = assembly.GetTypes(); } catch { return; } foreach (Type type in types) { if (type.IsInterface == false) continue; // Iterate through all the Attributes for each method. foreach (Attribute attr in type.GetCustomAttributes(typeof(InterfaceAttribute), false)) { mPluginNameList.Add(new PluginNameLite(attr as InterfaceAttribute)); } } }); if (isRecursiveDirectory) ////執行遞歸搜索 { foreach (var dir in Directory.GetDirectories(folderLocation)) { GetAllPluginName(dir, isRecursiveDirectory); } } else //保存當前目錄下的插件名稱 { CustomSerializer.Serialize(mPluginNameList, folderLocation + "\\PluginLog.xml"); myPluginNameList.AddRange(mPluginNameList); } }
流程圖如下:
2. 搜索所有實現接口契約的插件
直接用代碼說話
/// <summary> /// 獲取所有插件 /// </summary> /// <param name="folderLocation"></param> /// <param name="isRecursiveDirectory">是否進行目錄遞歸搜索</param> public static void GetAllPlugins(string folderLocation, bool isRecursiveDirectory) { bool isSaved = false; //是否已經保存了包含插件的dll文件列表 List<string> mPluginFileList = new List<string>(); //包含插件的dll文件列表 List<string> allDllFileList = new List<string>(); //所有dll文件列表 if (!isRecursiveDirectory) { try { mPluginFileList = CustomSerializer.Deserialize<List<string>>(folderLocation + "\\PluginFileLog.xml"); isSaved = true; } catch (Exception ex) { allDllFileList = (from file in Directory.GetFileSystemEntries(folderLocation) where file.Contains(".dll") select file).ToList(); } } else { allDllFileList = (from file in Directory.GetFileSystemEntries(folderLocation) where file.Contains(".dll") select file).ToList(); ; } Type[] types; IEnumerable<string> dllPluginFils; //最終要進行處理的的dll文件 if (mPluginFileList.Count == 0) //如果不存在插件文件目錄,則獲取該目錄下所有dll文件 dllPluginFils = allDllFileList; else dllPluginFils = from file in mPluginFileList select folderLocation + file; //否則將插件文件名稱拼接為完整的文件路徑 Parallel.ForEach(dllPluginFils, file => { Assembly assembly; try { assembly = Assembly.LoadFrom(file); } catch { return; } try { types = assembly.GetTypes(); } catch { return; } foreach (Type type in types) { if (type.IsInterface == true) continue; Type interfaceType = null; string interfaceName = null; foreach (var interfacename in myPluginNameList) //對該Type,依次查看是否實現了插件名稱列表中的接口 { interfaceType = type.GetInterface(interfacename.myName); if (interfaceType != null) { interfaceName = interfacename.myName; // Iterate through all the Attributes for each method. foreach (Attribute attr in type.GetCustomAttributes(typeof(XFrmWorkAttribute), false)) //獲取該插件的XFrmWorkAttribute標識 { XFrmWorkAttribute attr2 = attr as XFrmWorkAttribute; attr2.myType = type; //將其類型賦值給XFrmWorkAttribute if (attr2.MainKind != interfaceName) { continue; } PluginCollection pluginInfo = null; //保存到插件字典當中 if (mPluginDictionary.TryGetValue(interfaceType, out pluginInfo)) { pluginInfo.Add(attr2); //若插件字典中已包含了該interfaceType的鍵,則直接添加 } else { var collection = new PluginCollection(); collection.Add(attr2); mPluginDictionary.Add(interfaceType, collection); //否則新建一項並添加之 } file = file.Replace(folderLocation, ""); //獲取文件在該文件夾下的真實名稱 if (!mPluginFileList.Contains(file)) //若插件文件列表中不包含此文件則添加到文件目錄中 mPluginFileList.Add(file); goto FINISH; } } } FINISH: ; } }); if (isRecursiveDirectory) //執行遞歸搜索 { foreach (var dir in Directory.GetDirectories(folderLocation)) { GetAllPlugins(dir, isRecursiveDirectory); } } else { if (!isSaved) //若沒有保存插件文件目錄,則反序列化保存之。 CustomSerializer.Serialize(mPluginFileList, folderLocation + "\\PluginFileLog.xml"); } }
由於篇幅有限,搜索插件的流程與搜索插件名稱的流程基本相同,因此省略流程圖。
3. 並行化優化
讀者可以看到,在搜索不同dll文件的插件時 ,使用了 Parallel.ForEach ,網上介紹該方法的文章很多,此處不再贅述。 同時,系統直接將所有插件一次性的搜索完成。大幅度的提升了搜索速度。
五. 總結和問題
總結:
1. 系統盡可能的減少了對插件本身的限制,通過添加attribute的方式即可標記插件,減少了對原生代碼的修改。
2. 實測表明,文件目錄下存在60個左右的dll文件,其中只有6個是作者完成的包含插件的文件, 在I7 2600K的電腦上:(Debug版本)
原始版本的插件搜索和實例化需要將近5s的啟動時間
通過緩存文件目錄和插件目錄,時間減少2.7s
通過並行化搜索dll文件下的插件,時間進一步減少1s
最終,啟動時間僅僅為1.3s左右,同時還避免了多次搜索插件
存在的問題:
1. 插件系統可以自動檢測出同一dll文件中插件的變化,但在緩存了dll文件名之后,是無法自動檢測出dll文件的變化的。這種情況下,需要首先刪除記錄插件名稱和文件的緩存xml文件才能檢測出來。
2. 依然有一定的性能提升的余地。
以下是我的插件搜索器的完整代碼,歡迎有問題隨時交流:

using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace XFrmWork.Data { /// <summary> /// 插件集合 /// </summary> public class PluginCollection : ObservableCollection<XFrmWorkAttribute> { public PluginCollection() : base() { } } /// <summary> /// 執行搜索策略 /// </summary> public enum SearchStrategy { /// <summary> /// 目錄內搜索 /// </summary> FolderSearch, /// <summary> /// 目錄內遞歸搜索 /// </summary> RecursiveFolderSearch, } /// <summary> /// 該類定義了插件系統的接口契約記錄 /// </summary> public class InterfaceAttribute : Attribute { /// <summary> /// 該插件接口的名稱 /// </summary> public string myName { get; set; } /// <summary> /// 搜索策略 /// </summary> public SearchStrategy mySearchStrategy { get; set; } /// <summary> /// 相關信息 /// </summary> public string DetailInfo { get; set; } public InterfaceAttribute(string thisName, string thisDetailInfo, SearchStrategy thisSearchStrategy) // 定位參數 { this.myName = thisName; this.DetailInfo = thisDetailInfo; this.mySearchStrategy = thisSearchStrategy; } } /// <summary> /// 為了便於序列化而簡化的插件接口數據類型,是簡化的InterfaceAttribute /// </summary> [Serializable] public class PluginNameLite { public string myName { get; set; } public SearchStrategy mySearchStrategy { get; set; } public string detailInfo { get; set; } public PluginNameLite() { } public PluginNameLite(InterfaceAttribute attr) { myName = attr.myName; mySearchStrategy = attr.mySearchStrategy; detailInfo = attr.DetailInfo; } } /// <summary> /// 單例模式提供的插件搜索器 /// </summary> public class PluginProvider { PluginProvider() { } /// <summary> /// 可以被序列化的簡化插件字典,僅包含插件接口名稱和搜索策略 /// </summary> static List<PluginNameLite> myPluginNameList = new List<PluginNameLite>(); /// <summary> /// 插件字典 /// </summary> static Dictionary<Type, PluginCollection> mPluginDictionary = new Dictionary<Type, PluginCollection>(); /// <summary> /// 獲取某插件在插件目錄中的索引號 /// </summary> /// <param name="interfaceName">接口名稱</param> /// <param name="className">類名</param> /// <returns></returns> public static int GetObjectIndex(Type interfaceName, Type className) { foreach (var rc in GetPluginCollection(interfaceName)) { if (rc.myType == className) return GetPluginCollection(interfaceName).IndexOf(rc); } return 100; } /// <summary> /// 獲取所有的插件接口契約名稱 /// </summary> /// <param name="Path"></param> /// <param name="InterFaceName"></param> public static void GetAllPluginName(string folderLocation, bool isRecursiveDirectory) { List<PluginNameLite> mPluginNameList = new List<PluginNameLite>(); //緩存所有插件名稱 if (!isRecursiveDirectory) { try //如果不執行遞歸搜索,則查看在目錄下是否有保存了插件名稱的文件,若有,直接反序列化之,不執行插件名稱搜索 { mPluginNameList = CustomSerializer.Deserialize<List<PluginNameLite>>(folderLocation + "\\PluginLog.xml"); myPluginNameList.AddRange(mPluginNameList); return; } catch (Exception ex) { } } var DllFile = from file in Directory.GetFileSystemEntries(folderLocation) //若無緩存文件,獲取目錄下全部的dll文件執行搜索 where file.Contains(".dll") select file; Parallel.ForEach(DllFile, //並行化處理 file => { Type[] types; Assembly assembly; try { assembly = Assembly.LoadFrom(file); } catch { return; } try { types = assembly.GetTypes(); } catch { return; } foreach (Type type in types) { if (type.IsInterface == false) continue; // Iterate through all the Attributes for each method. foreach (Attribute attr in type.GetCustomAttributes(typeof(InterfaceAttribute), false)) { mPluginNameList.Add(new PluginNameLite(attr as InterfaceAttribute)); } } }); if (isRecursiveDirectory) ////執行遞歸搜索 { foreach (var dir in Directory.GetDirectories(folderLocation)) { GetAllPluginName(dir, isRecursiveDirectory); } } else //保存當前目錄下的插件名稱 { CustomSerializer.Serialize(mPluginNameList, folderLocation + "\\PluginLog.xml"); myPluginNameList.AddRange(mPluginNameList); } } /// <summary> /// 獲取所有插件 /// </summary> /// <param name="folderLocation"></param> /// <param name="isRecursiveDirectory">是否進行目錄遞歸搜索</param> public static void GetAllPlugins(string folderLocation, bool isRecursiveDirectory) { bool isSaved = false; //是否已經保存了包含插件的dll文件列表 List<string> mPluginFileList = new List<string>(); //包含插件的dll文件列表 List<string> allDllFileList = new List<string>(); //所有dll文件列表 if (!isRecursiveDirectory) { try { mPluginFileList = CustomSerializer.Deserialize<List<string>>(folderLocation + "\\PluginFileLog.xml"); isSaved = true; } catch (Exception ex) { allDllFileList = (from file in Directory.GetFileSystemEntries(folderLocation) where file.Contains(".dll") select file).ToList(); } } else { allDllFileList = (from file in Directory.GetFileSystemEntries(folderLocation) where file.Contains(".dll") select file).ToList(); ; } Type[] types; IEnumerable<string> dllPluginFils; //最終要進行處理的的dll文件 if (mPluginFileList.Count == 0) //如果不存在插件文件目錄,則獲取該目錄下所有dll文件 dllPluginFils = allDllFileList; else dllPluginFils = from file in mPluginFileList select folderLocation + file; //否則將插件文件名稱拼接為完整的文件路徑 Parallel.ForEach(dllPluginFils, file => { Assembly assembly; try { assembly = Assembly.LoadFrom(file); } catch { return; } try { types = assembly.GetTypes(); } catch { return; } foreach (Type type in types) { if (type.IsInterface == true) continue; Type interfaceType = null; string interfaceName = null; foreach (var interfacename in myPluginNameList) //對該Type,依次查看是否實現了插件名稱列表中的接口 { interfaceType = type.GetInterface(interfacename.myName); if (interfaceType != null) { interfaceName = interfacename.myName; // Iterate through all the Attributes for each method. foreach (Attribute attr in type.GetCustomAttributes(typeof(XFrmWorkAttribute), false)) //獲取該插件的XFrmWorkAttribute標識 { XFrmWorkAttribute attr2 = attr as XFrmWorkAttribute; attr2.myType = type; //將其類型賦值給XFrmWorkAttribute if (attr2.MainKind != interfaceName) { continue; } PluginCollection pluginInfo = null; //保存到插件字典當中 if (mPluginDictionary.TryGetValue(interfaceType, out pluginInfo)) { pluginInfo.Add(attr2); //若插件字典中已包含了該interfaceType的鍵,則直接添加 } else { var collection = new PluginCollection(); collection.Add(attr2); mPluginDictionary.Add(interfaceType, collection); //否則新建一項並添加之 } file = file.Replace(folderLocation, ""); //獲取文件在該文件夾下的真實名稱 if (!mPluginFileList.Contains(file)) //若插件文件列表中不包含此文件則添加到文件目錄中 mPluginFileList.Add(file); goto FINISH; } } } FINISH: ; } }); if (isRecursiveDirectory) //執行遞歸搜索 { foreach (var dir in Directory.GetDirectories(folderLocation)) { GetAllPlugins(dir, isRecursiveDirectory); } } else { if (!isSaved) //若沒有保存插件文件目錄,則反序列化保存之。 CustomSerializer.Serialize(mPluginFileList, folderLocation + "\\PluginFileLog.xml"); } } /// <summary> /// 獲取接口中固定索引類型的實例 /// </summary> /// <param name="interfaceName">接口名稱</param> /// <param name="index">索引號</param> /// <returns>實例化的引用</returns> public static Object GetObjectInstance(Type interfaceName, int index) { return Activator.CreateInstance(GetPluginCollection(interfaceName)[index].myType); } /// <summary> /// 獲取某程序集的接口列表 /// </summary> /// <param name="interfaceName"></param> /// <param name="myAssembly"></param> /// <returns></returns> public static ObservableCollection<XFrmWorkAttribute> GetPluginCollection(Type interfaceName, Assembly myAssembly) { return GetPluginCollection(interfaceName, myAssembly, false); } public static ObservableCollection<XFrmWorkAttribute> GetPluginCollection(Type interfaceName, Assembly myAssembly, bool isAbstractRequired) { if (mPluginDictionary.ContainsKey(interfaceName)) return mPluginDictionary[interfaceName]; else //若發現插件不存在,則再執行搜索(這種可能性很低) { PluginCollection tc = new PluginCollection(); Type[] types = myAssembly.GetTypes(); foreach (Type type in types) { if (type.GetInterface(interfaceName.ToString()) != null) { if (!isAbstractRequired && type.IsAbstract == true) { continue; } // Iterate through all the Attributes for each method. foreach (Attribute attr in type.GetCustomAttributes(typeof(XFrmWorkAttribute), false)) { XFrmWorkAttribute attr2 = attr as XFrmWorkAttribute; tc.Add(attr2); } } } mPluginDictionary.Add(interfaceName, tc); return tc; } } public static ObservableCollection<XFrmWorkAttribute> GetPluginCollection(Type interfaceName) { if (mPluginDictionary.ContainsKey(interfaceName)) return mPluginDictionary[interfaceName]; else { PluginCollection tc = new PluginCollection(); Assembly assembly = Assembly.GetAssembly(interfaceName); Type[] types = assembly.GetTypes(); foreach (Type type in types) { if (type.GetInterface(interfaceName.ToString()) != null && !type.IsAbstract) { // Iterate through all the Attributes for each method. foreach (Attribute attr in type.GetCustomAttributes(typeof(XFrmWorkAttribute), false)) { XFrmWorkAttribute attr2 = attr as XFrmWorkAttribute; tc.Add(attr2); } } } mPluginDictionary.Add(interfaceName, tc); return tc; } } } }