一. 面临的问题

  开发插件系统的主要优势是扩展性,我们不需要为系统模块的集成再多费脑筋,但这也带来了额外的问题。通常,系统需要在每次启动时搜索固定目录下的符合要求的插件。但是,当系统变得越来越庞大,所引用的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 intype.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;elsedllPluginFils = from file in mPluginFileListselect 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 intype.GetCustomAttributes(typeof(XFrmWorkAttribute), false))  //获取该插件的XFrmWorkAttribute标识
                                {XFrmWorkAttribute attr2 = attr as XFrmWorkAttribute;attr2.myType = type;  //将其类型赋值给XFrmWorkAttributeif (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 intype.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;elsedllPluginFils = from file in mPluginFileListselect 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 intype.GetCustomAttributes(typeof(XFrmWorkAttribute), false))  //获取该插件的XFrmWorkAttribute标识
                                {XFrmWorkAttribute attr2 = attr as XFrmWorkAttribute;attr2.myType = type;  //将其类型赋值给XFrmWorkAttributeif (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 intype.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 intype.GetCustomAttributes(typeof(XFrmWorkAttribute), false)){XFrmWorkAttribute attr2 = attr as XFrmWorkAttribute;tc.Add(attr2);}}}mPluginDictionary.Add(interfaceName, tc);return tc;}}}}

  

  

  

转载于:https://www.cnblogs.com/buptzym/archive/2012/06/05/2534703.html

NET插件系统之四——提升系统搜索插件和启动速度的思考相关推荐

  1. os+rom+android+6.0+n9005,三星N9005刷机包 Aurora.Note3.Full.Note5.Port.No.8 重新调试系统底层 提升系统流畅度...

    三星N9005刷机包 Aurora.Note3.Full.Note5.Port.No.8 重新调试系统底层 提升系统流畅度,更多优化请刷入体验. 三星N9005刷机包No.8更新内容: 1.基于S5较 ...

  2. editthiscookie插件怎么安装_CSDN专属idea插件上线啦~~

    1.插件介绍 CSDN的idea插件CSDN tools(以下简称tools),tools整合了日常开发中常用的工具,提高开发效率. json格式化 时间格式化 ip查询 计算器 CSDN平台搜索 g ...

  3. 百度地图采集经纬度坐标数据定位的javascript实战开发(地理坐标拾取系统、地址定位点选插件、实时定位、数据导入、地理编码、位置纠偏)

    坐标采集 前言 1.百度地图地理坐标拾取系统 2.位置选择插件 百度地图经纬度选择插件 默认参数配置 3.数据导入 4.地理编码 爬取百度webAPI 返回参数 前端封装转换函数 5.手机GPS定位 ...

  4. 使用Python打造基本WEB漏洞扫描器(二) 爬虫插件系统的开发—E-Mail收集插件实列

    一.实验介绍 1.1 实验内容 基于上节的爬虫,在爬虫的基础上增加一个插件系统,通过爬虫爬取网页链接后调用这个插件系统中的插件进行各种操作,本节也会写个简单的email收集插件作为列子,后面也会讲到如 ...

  5. 插件式架构设计实践:插件式系统架构设计简介

    本系列博文将使用微软RIA技术解决方案Silverlight以及扩展性管理框架Managed Extensibility Framework(MEF),以插件式架构设计为导线,分享本人在从事基于微软S ...

  6. chromium 43 如何增加后台运行的插件 关闭浏览器到系统托盘

    增加后台运行插件 有些时候,我们需要关闭浏览器窗口后,到系统托盘,确保浏览器继续运行. 如果用插件的话,只需要运行一个 background 属性的扩展即可. manifest.json 清单文件 { ...

  7. wi8ndows无法加载,Win8.1系统更新Flash插件后无法自动加载插件怎么办

    当前,为了实现各种编码格式的音频.视频节目的播放,不少第三方媒体提供商均开发了自己的播放器插件,提供音频.视频节目的网站一般会提示您下载.安装相应的插件或程序.Flash是Win8.1系统内置的播放器 ...

  8. niushop单商户V5.1旗舰版开源商城系统uniapp中的商业插件怎么安装

    我们知道这个系统是开源的,但是即使商业用户插件也是单独的,这8个插件分别是 拼团返利插件+裂变红包插件+盲盒插件+代客下单插件+阿里云短信插件+商品采集插件 好多认问到底怎么安装,其实很简单拿其中一个 ...

  9. 基于fiddler插件的代理扫描系统:越权漏洞检测

    基于fiddler插件的代理扫描系统:越权漏洞检测 # 概述 ##越权检测原理 ###系统架构 #基本步骤 待优化与工具联动 工具联动: 其他漏洞检测 # 概述 随着现在企业安全水平的提高, 单独依赖 ...

最新文章

  1. python目前的应用领域_专栏G|轻松学Python01:Python环境搭建与运行
  2. hitTest练手例子
  3. 第九届蓝桥杯省赛JavaC组真题——详细答案对照(完整版)
  4. 【机器学习】 - CNN
  5. 使用pymc3可能遇到的问题及解决方法
  6. ONNX系列五 --- 在C#中使用可移植的ONNX AI模型
  7. 描述最常用的5种http方法的用途_05-HTTP协议-万字好文!建议收藏
  8. java 类型通配符_java中泛型之类型通配符(?)
  9. javascript学习之模块拖拽功能的实现
  10. java zip 文件夹_Java Zip文件文件夹示例
  11. 一百多个实用ZBrush笔刷和Alpah,笔刷使用方法,让建模更简单!
  12. php发微信模板消息,PHP超简单发送微信模板消息
  13. 禅道及与JIRA区别
  14. CSS中常用的选择器都有那些?
  15. SOAR+HIDS,增强主机安全防护
  16. Android 9.0 flash播放器播放swf源码讲解
  17. MIGO为玩家带来接近无限的可能
  18. 扫地机器人的配件能自制么_米家扫地机器人,第三方配件耗材靠谱吗?
  19. Linux基本命令(Redhat,CentOS)
  20. 设计一款主动降噪(ANC)耳机

热门文章

  1. bzoj4033:[HAOI2015]树上染色
  2. linux centos6.5 网络配置
  3. java常见面试题总结
  4. 第二章(jQuery选择器)
  5. 初识50个Linux命令
  6. Qt quick 编程
  7. 关于yii验证和yii错误处理
  8. 幻灯片:Why Java Sucks and C# Rocks
  9. collections deque队列及其他队列
  10. XTU 1243 2016