背景 最近跟售后经理吃饭,他跟我再次谈起两年前为公司临时写的一个客户端,仍然非常激动的跟我说,这个客户端完爆了公司其他版本的客户端,包括最老的Delphi写的,Asp.Net写的,以及最新的Wpf写的客户端。无论是多么大的界面(集成的机房多),这个系统都是瞬间打开,而且运行非常稳定,一旦成功部署之后基本没有任何问题。 这个版本的客户端仅仅只是一个临时替代的版本:原来的Delphi客户端实在是太慢了,在大型的数据中心监控中需要4~5分钟才能进入主监控界面,而asp.net版本的客户端又经常存在不稳定的情况(IE浏览器不支持7*24小时的异步刷新),最新的Wpf客户端又还在设计阶段,于是临危受命决定开发一个临时过渡版本,当时也只是开发了一个月,没想到竟然如此成功,至今仍让我们的售后部门津津乐道。这中间其实没有太多高深的技术,但是却有很多的开发技巧以及编程的思想。我至今仍然看到很多人都在犯这么一些简单的错误(例如VS工具箱的加载项),导致他们的系统非常缓慢,但是他们却总是抱怨是编程语言的问题,是windows系统的问题,是机器的性能不行…… 我决定把我的一些实践经验跟大家分享:不是非得你有多么牛逼的技术,才能做出一个稳定快速的系统,更多的时候,它取决于你是否有一个产品的意识,是否让你的软件真正贴近用户。 系统界面与功能 先来看看原来的系统界面是怎样子的:其功能如下,我新写的客户端增加了支持生成OCX控件的功能: 整个系统的物理架构是这样的: 原系统存在的问题 加载主页面慢 随着界面数量的增加,会需要更多的加载时间 随着地点和设备的增加,加载会需要更多的时间 页面之间切换卡 数据显示慢 地点的报警状态显示不准确且存在延迟 报警并发较多时卡顿更严重 客户端性能优化的基本手法 我们来看看通过一些什么手法能够解决原来的系统存在的这些问题。按需获取 大部分的情况下,我们其实所能看到的东西都是极其有限的,无论系统是多么庞大,功能多么的丰富,其实呈现给用户的都是极其有限的。监控界面的按需获取 前面说了,监控主界面里的界面都是组态的,是由工程师拖拉控件上去实现的,大家也看到上面图形还算丰富,主要是使用了大量的图片,因此我们系统中在保存这些组态界面的时候,同时也保存了界面图片的字节流。大型的数据中心由于界面较多,这些界面加起来是可能会超过1G大小的。这么大的界面,如果都是直接加载到界面中,首先就要费不少的时间,即使是在内网的情况下,假设你网络能够1s下载20M左右,也要50秒,接近1分钟,遇上网络高峰,花个1~2分钟并不奇怪。 我们是否有必要把所有界面都加载进来呢,当然没有。我们只需加载第一个界面,其他界面在需要的时候(用户点击或者发生告警需要跳转的时候)才加载,这样我们的速度里面就提升了,这就是按需加载! 当然说的轻巧,实际做的会有很多问题。比如,如何实现不实现页面又能知道该页面是否告警(必须解析每个界面上的控件,才能知道某个界面包含了哪些控件,才知道监控指标告警在哪个界面上)?我的步骤如下: 保存界面的时候,把界面上的控件的Id列表存储到设备记录中 加载时只加载所有的设备记录(名称+控件Id列表) 把对应的信息附加到树形节点中 根据对应的树形节点的告警信息在需要显示界面时生成界面 按需刷新界面上的数据 做监控系统,除了告警页面必须实时通知到客户外,监控数据界面,其实只需展示当前显示页面的数据即可。怎么做呢,我们可以提供一个单独的程序来管理所有接收到的数据,然后再提供一个获取当前数据的接口给客户端,具体请看下面更改的架构。 有些人可能会疑问,为什么不直接在采集器中提供这个接口呢?因为这是组态界面,界面上的控件要取哪个采集器的数据是未知的,所以把数据放在一起统一管理会更加方便。而且采集器可以7*24小时工作,而客户端是经常要打开关闭的…… VS中的反例 如果用过VS开发自定义的Winform组件,那么大家对它的工具箱加载自定义组件这个功能肯定印象深刻,每次选择添加项,然后选择自定义控件dll的时候,都非常痛苦,尤其我电脑比较忙而又装了不少插件的情况下,为了一个非常简单的功能,我需要花费4分多的时间来打开那个选择文件的界面,这个界面加载了一大堆我绝大多数时候都用不上的COM组件,我实在没法想象开发这个功能的程序猿是怎么想的。还好,在VS中微软总算是改进了这个功能,但是做得还不够。按我的想法,完全可以把COM组件部分异步加载,给出正在加载的提示即可,可以立即显示“选择”按钮,这样体验性立即上升了一个层次。 延迟加载 延迟加载是指用到的时候,再去进行实际的构建。树形菜单的延迟加载 树形菜单的树形节点的构建就是一个最适合解释的例子。大家可以尝试加载个树形节点然后构建成一棵树,看看在Winform中需要多长的时间。我们的实际中有没有必要这么去做呢?各位可以思考下自己查看树形导航的时候,是不是从根节点到子节点最后到叶子节点这样一步步看下去的,大部分的时候,其实我们只需首先看到根节点即可。例如下面这个: 对于这种情况,我们完全可以把树形节点都获取,但是先只创建只有根节点的一棵树,在用户点击之后加载子节点,如果已判断过,则不执行加载的操作。基本的方法是在Tag中附加一个字段指示子节点是否已经加载,参考代码如下: privatevoidTreeDevices_BeforeSelect(objectsender,TreeViewCancelEventArgse){varmyNode=e.Node.TagasNTier.Model.MyTreeNode;if(myNode==null)return;if(myNode.IsSubNodeLoaded==false){//还没有加载数据,主要是指机房节点LoadNodesOfSubMainForm(e.Node,myNode);//加载树形子节点}//已加载了数据,则生成相应的界面LoadFormModel(myNode);} 这里延迟加载与按需加载有点类似,区别是,延迟加载必须把所有数据加载进来,但是并不构建成一棵UI树,而是在用到的时候再去生成。右键延迟初始化 另一个地方就是每个控件的右键菜单。因为每个右键菜单显示的内容是需要根据控件的类型以及相关的权限来判断的,但是我们看到右键菜单的时候一定是人为进行操作才能显示出来,因此没有必要再界面生成的过程中去为每个控件生成对应的右键菜单,而是在弹出右键菜单时进行相关的判断,延迟右键菜单的生成。 化曲为直 我们知道,如果要查看一棵树的所有节点,常用的方法就是使用递归进行广度遍历或者深度遍历。但是,在树形节点较多的时候,遍历其实是非常耗时的。在我们这个系统中,告警是必须要最先处理的,因此,我在系统中使用Dictionary类型缓存了每个属性节点与它相关联的数据类型(ID),从而能够在发生告警时马上定位到指定树形节点。缓存,还是缓存 缓存界面 我们系统是组态的界面,这就限制了界面的生成必须是动态的。如果我们采用按需加载的方式,那么界面的生成就是实时的,怎么能够做到快速的进行页面的切换呢? vartempPanel=_panelCache.CreatePanel(this,formModel,myNode.AgentBm);//创建Panel 在这里,我专门写了一个界面的缓存类,如果没有缓存,则动态创建,如果有缓存,就直接返回缓存的界面。同时,根据界面的最新的打开时间和点击次数,对缓存的界面进行管理。我们知道,整个大型系统中,其实用户白癜风是怎样引起的白癜风初期的治疗方法
|