热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

大文本文件阅读器设计

我们项目中需要实现一个日志查看控件,这本是一个很简单的需求:写一个通用的控件,将字符串绑定到RichTextBox,如果要查

     我们项目中需要实现一个日志查看控件,这本是一个很简单的需求:写一个通用的控件,将字符串绑定到RichTextBox, 如果要查看日志,将日志赋值给字符串即可。这个控件很简单,在绝大多数情况下工作的都很好。但是最近经常有客户报告说日志打不开,或者打开后就无法响应了。检查后发现这些无法打开的日志都很巨大,文件长度大多都超过几千万行。显然不带任何优化的文本阅读器都撑不住这个级别的文本。通过观察及与客户的沟通,他们的典型的操作是鼠标上下滚动,翻阅前后内容。拖拽滚动条到某一个段落,然后在局部操作,如果文件很大,他们几乎不会全文阅读。客户希望操作的过程能够尽量不卡,或者少卡。

      要实现这个需求并不容易。首先想到的就是不能一口气将所有字符都载入内存。只能一段一段的载入,同时这些段落又不能是离散的, 因为他们还需要上下滚动鼠标也能够平滑。通过比较简单的需求分析,我们发现最初的设计一个文本阅读器已经分裂成两个子任务了:1)文件操作 2)界面显示。

1)文件操作

  我们需要实现一个类似于StreamReader的类,但是需要另外增加ReadNextLine(), ReadPreviousLine(), Locate() 方法。即读下一行的内容,读上一行的内容,定位到某一行。我们想到了以行为单位做索引。比如说,一个文件有一千万行,类初始化时,我们先将每行的偏移值算出,当要定位(Locate)到某一行时,直接通过索引找到这个偏移值,然后file seek。这个操作是瞬时的。但是前面的索引太耗时了。我做了一个实验,在我的笔记本上(lenovo T430s)计算一千万行文件的索引需要将近2分钟。而且如果所有的索引都保存在内存,需要将近400MB内存。这对于客户是不可以接受的。

     建立索引这个思路应该是不错的,不过我们可以做进一步的优化。仔细研究一下一般使用习惯不难发现打开文件永远是从头开始的,然后鼠标上下滚滚查看临近的内容,偶尔需要跳转。这里,上下滚动鼠标是一个连续的动作,跳转是一个离散的动作。连续动作(ReadNextLine, ReadPreviousLine)显然用户不希望有卡的感觉,但是跳转(Locate),有一个停顿的过程应该是可以接受的。基于这个假设,我们又有了进一步的优化:类初始化时不需要建立全部的索引,只需要将文件的开始几行索引建立即可(在我们项目中先建立前100行)。如果ReadNextLine,就再建立后面连续多行的索引,ReadPreviousLine则建立前面多行的索引。这是基于局部原理:如果调用过一次,下一次也继续调用同样的操作的可能性也很大。通过这个方法能够确保连续的向前向后滚动鼠标不会有卡顿的现象。那如何做到Locate呢?比如说,在一个一千万行的文件,我们现在想跳转到第30万行,显然在计算偏移量之前我们需要确认这一行是否已经被索引过了?这样就要求存储索引的数据结构查询的时间复杂度尽可能的低,最好能够达到O(1),这里我们偷了下懒,直接使用Dictionary。如果索引没有建立好,我们就不得不重新建立。最理想的情况是,如果知道第299999行已经建立过索引了, 我们只需要再多计算一行即可。如果我们只使用Dictionary来存储索引,我们并不知道前面有哪几行已经建立过了,不得不从第一行开始扫描,这是很低效的。于是想到了用链表和字典来配合:链表用来保存索引的前后关系,字典用来随机检查索引是否已经建立。所以逻辑就是:

      

long FindOffset(int target)
{long offset;if (IsIndexCreated(target, out offset) == true)
{return offset;
}
else
{int nearest = FindNearestIndex(target);GenerateIndex(nearest, target);return FindOffset(target);
}


IsIndexCreated 即检查Dictionary是否包含这个index

FindNearestIndex 即检查LinkedList 找到离target最近的index结点

GenerateIndex 创建从nearest到target结点的所有索引


通过两个数据结构能够快速定位并更新,但是又有新的问题了:存索引的内存加倍了。存一千万个索引现在需要800MB内存。在极端情况下,比如说,打开文件后,直接跳转到第一千万行。这样GenereateIndex(1, 10000000)会很耗时:不仅要计算偏移量,还要插入Dictionary。 当数据量很大的时候,插入Dictionary的时间也是需要考虑的。所以为了减少内存使用,提高速度,GenereateIndex的过程我们不会将计算的所有偏移量都保存,仅仅从距离target最近的100个结点开始保存。这样效果非常显著。 这里还有一个地方是可以优化的:如果有人有这个耐心,从第一行连续的滚动鼠标直到最后一行(一千万行),那么真个文件的索引都将生成。基于这个我们可以使用LRU算法仅保留最近的10000个索引。

2)界面显示

    粗看上去这个方案已经很完美了,但是我们回避了一个问题。由于初始化类的时候我们并没有创建整个文件的索引,我们并不知道这个文件到底有多少行。文件操作中,如果对于一个只有5千行的文件调用Locate(1000000),我们只索引到最大行数。这在界面显示就有问题了,因为滚动条在读取下一行时每次滚动多少是取决于最大行数的。我们的做法是在界面处理中提供一个CalcuateMaxiumLineNumber函数,这个函数可以设置一个timeout参数(例如100毫秒),如果最大行数能在timeout内算出,直接返回最大行数,如何算不出来,先返回一个一百万,然后启动一个线程在后台慢慢算,每隔一段时间(例如500毫秒)更新一下最大行数。这样就不会影响界面的打开了。


推荐阅读
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • 本文介绍了Android中的assets目录和raw目录的共同点和区别,包括获取资源的方法、目录结构的限制以及列出资源的能力。同时,还解释了raw目录中资源文件生成的ID,并说明了这些目录的使用方法。 ... [详细]
  • 超级简单加解密工具的方案和功能
    本文介绍了一个超级简单的加解密工具的方案和功能。该工具可以读取文件头,并根据特定长度进行加密,加密后将加密部分写入源文件。同时,该工具也支持解密操作。加密和解密过程是可逆的。本文还提到了一些相关的功能和使用方法,并给出了Python代码示例。 ... [详细]
author-avatar
哈行小DWW_421
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有