转自:http://blog.csdn.net/frankworld/article/details/2678921
很多人在工作中都会碰到过查看一些非常大的文本文件(100M以上,甚至1G)的需求,我所在的公司这种需求尤其突出。在文本文件的查看和编辑工具里最出名的应该是UltraEditer(以下简称UE)了吧。它功能非常强大,但是很多功能对我们而言没有任何用处。更严重的问题是,我们需要在大文件中搜索。如果你使用过UE打开大文件,你一定会有这种感觉,打开大文件速度比较慢,搜索就更不用说了,如果你搜索的结果有几万条甚至几十万条,那就需要你极度的耐心。而且UE不是免费的,一个license大概要50美元。
其实我的前任也作过一个工具,功能也非常强大,但是一直推广不了。主要原因是内存占用大,速度慢。其实做工具的人最容易犯的错误就是“贪多求全”。关于做工具的一些体会,我会另外写一篇,在这里就不再赘述了。最后我们的决定是改进这个工具而不是另起炉灶。
确定了方向,接下来就是确定方法了。摆在我们面前的主要难题就是:内存占用大,速度太慢,代码比较混乱。接下来我们逐个问题解决。
内存占用大的主要原因是程序把文件所以内容读入内存,逐行保存在一个List<string>里。我们知道,把一个文件读入内存所占的空间至少是文件大小的三倍。这意味着打开一个100M左右的文件要占用300到400M内存,这是非常可怕的。而且巨大的内存占用会加大GC(垃圾回收器)的压力,严重影响程序运行的效率。这个问题并不难解决,对大文件我们只需要读入一小部分即可。需要注意的是,我们可能会遇到从第一行直接跳到最后某一行的情况,如果我们再次重新开始查找的话速度会非常慢。我的解决方案是,把整个文件分成许多小块,每块10000行,然后记住每块的起始位置。这样当我们随机读取一行的话,就可以计算出这一行所在的块,再根据起始位置迅速读出这一块,返回我们需要的行。这个难度并不算大,需要注意的是资源的释放。由于我们随时都可能需要读文件,所以我们的流一直处于打开状态,一直到主动关闭为止。为了防止因为忘记关闭而引起资源泄露,我们使用了Dispose模式,这是微软推荐的需要释放资源时的解决方案。
接下来是速度慢的问题。其实当内存占用降下来后,速度已经有了一定的提升。一开始我们把文本内容显示在一个TextBox中,后来因为需要一些特别的格式,例如颜色、底色,所以我们改用了RichTextBox。可以想象,当你把大量文本放进去(例如100W行)后显示和滚动会变得多慢。为了快速显示,我们决定只加载可以显示的部分,就是如果控件可以显示100行,我们就只加载100行。同时在右端添加一个滚动条来表示当前显示的内容在整个文本中的位置。搜索和文本显示没有本质的区别,UE之所以搜索显得比较慢,主要原因是搜索结果的加载而不是搜索本身,所以我们对搜索和显示采用一致的处理方式。在做这个控件的时候,遇到的最大的问题是如何保存选中状态(包括光标位置),因为每次滚动都会重新加载显示的内容,所有选中状态都会消失,而且我们还要考虑当选中的行没有被显示或者不完全显示的情况。
为了能在重新加载内容后恢复原先的选中状态,我们必须先状态相关的信息保存起来。首先是保存时机的问题,我们不能只考虑鼠标事件,所有最后在RichTextBox的SelectedTextChanged事件里保存选中状态(光标位置的变化也会触发这个事件)。我们在这个事件中保存选中的起止位置的行号和列号,然后在重新加载显示内容时重新选中需要的部分。这个地方要注意两个问题,循环触发事件引起堆栈溢出和选中的起止位置问题。循环触发事件这个问题很容易出现,因为我们在重新加载显示内容时选中状态会重新初始化,而我们又需要选中一些内容。解决的办法也很简单,一个标志位即可保证重新加载时不触发SelectedTextChanged。起止位置上我走了一些弯路,一开始在恢复选中状态时总是从上到下选中,结果在从下到上的选中恢复时就出了问题。后来我才发现RichTextBox的Select方法第二个参数选中长度可以是负的,这时就会从下到上选中。基于这一点,我们保存选中状态就有了一个基本原则:除非选中长度为零,选中状态的起始位置不会发生变化,我们只要保存结束位置即可。这样即使我们朝上选中直到引起向上滚动也不会出现问题。
最后一个问题就是代码比较混乱,这是由一些历史原因引起的。这个工具曾经被要求加入太多的功能,虽然最后证明都没什么用处,但是当时的急功近利却使代码变得臃肿而混乱。为了解决这个问题,我要求对整个工具进行模块划分,对那些证明没什么用的功能直接屏蔽掉。但这个问题不是一朝一夕可以解决的,只是希望以后在进行版本升级的时候能多考虑一下结构的问题,而不是一味拼命的添加新功能。