var tempPanel = _panelCache.CreatePanel(this, formModel, myNode.AgentBm); //创建Panel
在这里,我专门写了一个界面的缓存类,如果没有缓存,则动态创建,如果有缓存,就直接返回缓存的界面。同时,根据界面的最新的打开时间和点击次数,对缓存的界面进行管理。我们知道,整个大型系统中,其实用户关注的界面也是有限的,一般他们只会关注最重要的几个界面,最常用的也是这几个界面。通过缓存的管理,不但能够实现界面之间的快速切换,同时也减少了系统占用的内存。我整个客户端程序文件大小压缩之后在500k之内,而运行期间占用内存基本维持在50M左右。
查看上面改造过后的架构,我们知道现在获取数据是在打开界面之后再去获取,直到建立连接并取得数据之后,才能在界面上显示,这个过程一般会耗时1~2秒,网络差的情况会更糟。怎样才能让用户更为快速的确定我们的系统已经运行了呢?这里我们通过一个简单的办法,集中服务端通过定时把当前监控到的数据写入控件的属性中,在系统加载控件的同时把这个值显示出来,这样可以看起来好像是系统马上获取到了数据。而由于缓存的值是定时把最新值写入进去的,这种做法在很大程度上保证了缓存中的数值是正确的。
异步,还是异步
异步是提高程序响应和用户体验的不二法宝。C#中的控件和大部分流操作类等都提供了支持异步操作的方法:BeginXXX
和EndXXX
.它的原理也非常简单,使用BeginXXX
时,把操作加入线程池,执行完成之后调用一个回调函数。
一个用户体验良好的系统,应该能够合理的使用异步操作,确保执行UI更新时以及执行耗时的操作时不会阻塞。大部分人在写代码的时候,总是直接进行调用,在控件较少或者完成简单任务的时候,你一般都感觉不出来,但是在控件数量多的时候,我们很容易就感觉到界面卡,不流畅。
我在新系统开发的时候,就有意识的在控件加载、控件数据刷新、控件告警状态切换等操作中使用了异步的操作,让系统在打开界面时完全感觉不到卡的迹象。
不过使用异步要时刻记得,异步可以提高用户体验性,但是不会有性能上的实质提升,如果感觉到数据响应有延迟,你还是得花功夫找到根本的原因。
界面数据刷新归并处理
我们来看看原来界面是怎么刷新数据的:
打开界面->刷新数据->新建一个线程->定时刷新数据->关闭界面->关闭线程。
对Windows系统有足够了解的人都知道,新开一个线程都是非常耗费资源的。这种情况,我们是可以在整个系统中,提供一个统一管理的刷新线程,只需对当前需要刷新的界面进行刷新即可:
刷新线程->判断当前界面是否存在->定时刷新数据
结合上述的异步操作,我们的控件在刷新数据的时候非常的流畅。
告警跳转归并处理
上面我们提到了,在系统发生告警时,必须要跳转到报警的页面,这个机制在大量告警并发的时候,就会有非常大的问题,很可能我们的系统就会在不同的界面中进行跳转而卡死。对于系统的用户来说,在1~3秒内的多个告警,我们其实可以处理为一个告警,我们只需往最后一个告警发生的页面跳转即可,这样既达到了相应的效果,也减少了系统的压力。这就是告警并发时的归并处理。
在一些情况中,我们确实短时间没有办法对性能进行提升了,花费的时间却要要这么多,这种情况下,我们有些什么好的做法呢?
给出提示信息或者进度条
如果大家经常用手机登陆微博、微信等,肯定对这些app加载图片有过一些体会,尤其如果你是在网络较差的情况下,同样是要等1分钟才能加载出图片,如果这个app没有任何提示,那么,过了30秒或者20秒,你就有可能受不了把他点掉了,因为你感觉它似乎已经过了几分钟,还有可能遥遥无期;而如果这个app能够提示当前下载的字节数、当前下载的进度,那么,1分钟的等待,你似乎也能接受,这毕竟是网络引起的问题。这就是一种视觉上的欺骗。
在一个系统的加载过程中,有提示信息和没提示信息,有进度条和没进度条,给人感觉的速度是不一样的,即使从实际的情况来看这两者没有任何差别。
很多时候,我们系统的运行需要从服务器中获取一些最新的数据,以支撑基本的运行。这部分时间是你必不可少的,很多人都认为这是没有任何办法优化的,其实不然。我们很多程序其实都提供了一个用户名和密码的输入框,其实在用户输入的过程中,我们还是可以利用的。在弹出登陆窗到输入账号和密码到登入系统的过程中,一般都会有3~5秒的时间。
我看到很多人写程序,弹出登录框就老老实实的弹出,然后在输入完用户名和密码之后在进行数据的获取和加载,实际上,我们已经浪费了这些时间。如果你能有效利用这3~5秒,那么,你就已经赢在了起跑线。
视觉欺骗的另外一个重要运用,就是在曲线的渲染中。在机房监控中,我们有些设备的监控比较频繁,一天产生的数据高达几万条,把这么多的数据绘制到一条24小时的曲线上,我们将会看到很多密密麻麻的点,绘制这些点非常的耗时和耗资源。而我们提供曲线给用户查看的目的是什么呢,是想查看一天的趋势变化,过多密集的点其实是没有必要的,大家看看下图,如果数据点更多的话,第二个曲线会更加密集,看起来会像一条粗大的直线:
通过简单算法对曲线进行压缩之后,显示历史趋势的速度非常的快,非常的流畅。我们对比上面两条曲线,其实对用户来说,或许更喜欢第一条曲线,因为他反应的趋势更为优美,有木有?
使用单元测试辅助开发
在我的博文中,我一直强调使用单元测试,无论是开发还是重构。我觉得这个无论是怎么强调都不为过的。
在开发的过程,我们应该有意识的按单元测试的目的来构建我们的函数、类、以及程序集,如果你的函数符合单元测试要求的话,一般都是比较容易重构和维护的。另外,我们开发的过程中,很多时候需要验证某个功能是否可用,使用单元测试,将会很快速的帮你完成这个验证操作。我看我们很多程序员开发效率都不高,尤其在开发一个大型系统的时候,喜欢把整个系统开起来调试,或者是在系统里面加上各种配置或者条件编译来进行调试,这种习惯非常不好。在程序中加入配置容易让程序结构出现混乱,代码的阅读体验也不好,很多时候如果我们忘记去掉这个配置,很容易就对发布的系统产生较大的影响。
使用单元测试另外一个好处是,我们可以随时针对某个方法进行性能上的测试,发现哪些代码对我们的系统造成了较大的影响。我习惯连私有的函数也一起加入测试,以下是调用私有函数的一个辅助方法:
public static object InvokePMethod(Type type, string methodName, object classInstance, object[] @params)
const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
var methodInfo = type.GetMethod(methodName, flags);
var result = methodInfo.Invoke(classInstance, @params);
return result;
提供完善的日志信息
在日常的开发中,我一直跟我的同事强调日志的重要性。相信有一定开发经验的都知道在系统中写日志,但是,怎么把日志写好,很多人都把握不了。在这里我提几点建议:
按日志的重要性和详细程度划分级别
提供调试级别和运行级别的日志
注意记录系统信息和配置信息
在状态变化时进行记录
把相同的信息进行合并
能够反应程序运行的业务逻辑
之前我们的系统是自己实现的日志组件,我用C#重写时,引入了NLog日志组件,我觉得这个日志组件非常好用。另外,我还专门提供了一个UI界面的调试窗,以便实施工程师在现场调试的时候能够快速定位问题。
在实际运行的过程中,因为有良好的日志信息,我很快能够排查很多的问题,而大部分的问题都是因为配置导致的。我一致跟研发的同事强调,尽可能的不要相信现场工程师给你的判断,应该要现场工程师提供证据给你,而要提供什么样的证据,作为一个研发,你才是最清楚的。好的日志系统应该能够根据日志信息精确的定位到问题,在离线的情况下能够最大程度的反应当前系统的配置、运行状态、以及错误信息。
优化的结果
最终用C#重写的客户端在各方面变现都非常的好,系统非常稳定,整个系统进入在2s左右,页面切换在1s左右,最重要的是,客户端跟系统的大小没有关系,适应大小的数据中心。我们看看新老系统在加载过程中的一个对比:
很明显,通过上述手法进行一些优化之后,我们的系统在各个步骤都有了提升,而且通过异步、缓存、欺骗等方式让一些步骤可以同步进行,大大加快了系统的加载和相应。
我希望通过这篇文章,把客户端优化的一些方法分享出来,供大家参考。这其中没有什么高深的知识,也没有说要你必须采用怎样的编程语言,仅仅是通过一些简单的手法,并综合应用,就能把一个系统的响应速度从4分钟提升到只需两秒。当然,我们还有其他很多的方法,比如分布式……无论是什么样的技巧,我觉得有一些基本的原则是要遵循的:
站在用户的角度思考问题
永远不要把选择交给用户
必须考虑最极端恶劣的情况
回顾一下这篇文章讲的内容:
加快系统响应的基本手法
给出提示信息或者进度条