NET插件系統之四——提升系統搜索插件和啟動速度的思考


 

一. 面臨的問題

  開發插件系統的主要優勢是擴展性,我們不需要為系統模塊的集成再多費腦筋,但這也帶來了額外的問題。通常,系統需要在每次啟動時搜索固定目錄下的符合要求的插件。但是,當系統變得越來越龐大,所引用的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;
            }
        }
    }


}

 

  

  

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM