Chinaunix首页 | 论坛 | 博客
  • 博客访问: 900137
  • 博文数量: 322
  • 博客积分: 6688
  • 博客等级: 准将
  • 技术积分: 3626
  • 用 户 组: 普通用户
  • 注册时间: 2010-09-19 11:26
文章分类

全部博文(322)

文章存档

2013年(5)

2012年(66)

2011年(87)

2010年(164)

分类: LINUX

2012-04-24 13:21:01

这几天在研究如何优化大数据列表的显示,在网上找到这篇文章。文章写于2009年,可能有点老了,但还是有些借鉴意义,所以把它翻了过来,与大家一起分享。可惜的是作者没有提供相应的原代码

原文地址是:
                                                                                                                                                                                                                                                                                                                                                        
在这篇文章中,我们将看到一个iPhone中从服务器加载数据的UITableView及当数据量从1到成千上万行时UITableView的表现。我将测试当显示数据集从很小到很大时哪种方法表现良好,而哪种方法表现拙劣。




上周,我收到一封邮件,问我StreamToMe在一个中间集合中是否可以处理20,000个文件。这位仁兄可能是问“20,000个文件分类进子目录中”,但是我立即想到将20,000个文件放到一个单独目录中是一个更有趣的问题

我们在iPhone应用程序中可以很容易的发现当table只处理几百行数据时,程序会变得很慢,响应迟钝。在我的经历中,我很少去测试在一个UITableView旋转几百行数据。我不知道当有20,000行数据时,UITableView会怎么样。

单纯从数据量上看,它应该能正常工作:128MB的iPhone版本(所有早于3Gs的版本)允许应用程序使用24MB-64MB的内存,直到被系统强制关闭。这就允许每一行的数据使用1-3kB的内存—事实上,我不需要这么多。

当然,这并不是有效的综合测试。下面这个程序将进行实际的测试,真实的数据、服务器交互、解析、构造和显示,在所有的东西加载进内存后还必须能回放音频。

生成测试数据
我需要真实的测试数据,所以我决定拷贝一着小的mp3到服务器上。这个mp3只有1K大小,可播放4秒钟。我使用UUID附加上“.mp3”作为文件名,因此文件排序算法依然可以做一些工作。

  1. for (NSInteger i = 0; i < MAX_FILES; i++)
  2. {
  3. NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  4.   
  5. CFUUIDRef uuid = CFUUIDCreate(NULL);
  6. NSString *uuidString = (NSString *)CFUUIDCreateString(NULL, uuid);
  7. CFRelease(uuid);
  8. [uuidString autorelease];
  9.   
  10. NSString *filePath =
  11. [directoryPath stringByAppendingPathComponent:
  12. [NSString stringWithFormat:@"%@.mp3", uuidString]];
  13. [[NSFileManager defaultManager]
  14. createFileAtPath:filePath
  15. contents:mp3Data
  16. attributes:nil];
  17.   
  18. if ((i % 1000) == 0)
  19. {
  20. NSLog(@"%ldfiles remaining.", MAX_FILES - i);
  21. }
  22.   
  23. [pool drain];
  24. }


使用上面这段代码,我分别创建了数量为110100100010000200001000001000000的文件,来看应用程序能跑多快。

测试的结果得出了一些有趣的与iPhone自身无关的结果:
· 是的,我们可以在一个文件夹中创建多于65535个文件。我还记得Macs在整个硬盘中不能创建多于65535个文件的日子。
· 奇怪的是,当我们在Finder选择文件并使用“Copy”时,能创建的文件最大数量依然是65535。
· 不要试着从一个窗口手动10000个或更多的项目到另一个窗口中,这会产生令人不快的结果。
· -[NSFileManagercreateFileAtPath:contents:attributes:]比其它方法更慢,因为它在临时位置创建文件然后将文件移动目标位置。
· 如果试图在电脑上创建一百万个mp3文件,那就准备好等待3个小时吧。哦,还没完呢,接下来Spotlight元数据索引还会让你的电脑慢上好几个小时。 

初始
加载上面生成的数据集需要花费多少时呢?下面的表给出了答案:

 

表示在加载20,000条数据后,程序无法稳定地播放音频(有时因为低内存而强行关闭)。10w行和100w行的数量都加载失败。 

初始分析
大(Scaling)


首先来看一下结果扩大的方式,表格数据显示了iPhone内存安排所期望的结果:

1. iPhone有16k的数据缓存,所以在这个限制之内(少于几百行),影响因素更多 地来自于网络和匹配设置时间,而不是指定行的渲染。当数量为1,10和100时,这更趋向于非线性扩大。
2. 当测试数据超出16k大小的限制时,这种扩大就随着放入内存的数据量呈线性增长。
3. 在iPhone中没有虚拟内存,所以当内存耗尽时,我们无法看到高于线性增长的趋势—相反,只是简单的强制关闭应用。更多的内存并不能像在Mac机中一样使iPhone跑得更快。

速度

看看结果,让人有点失望 – 没有人想等上一分钟以上让程序去加载文件列表。这段时间干什么去了?看看20,000行测试时花费的时间。
· 服务端花了6,563ms来加载字典列表并格式化返回值;
· 数据传输花了3,111ms;
· 客户端花了12,771ms来解析服务端的数据;
· 客户端花了32,098ms来将解析后的数据转换为行数据类
· 自动释放池花了9,453ms来清理内存

内存使用
加载20,000个数据的内存占用大概是6.8MB,峰值是38.2MB。这导致两个问题。
  1. 最后内存如此低,为什么程序的行为好像是内存受到限制呢?
  2. 是什么导致峰值如此高?如果起初很低,100,000行也许是可以加载的。
修改及改
在文件系统查stat是最大的限制


我首先从服务端做了修改。加载20,000条数所花了6.5s,太慢了。这里关键的限制因素是读取成千上万的小文件的基本元数据。低级文件函数stat(或者是lstat)是限制因素。

从技术上讲,我没有使用lstat,但是-[NSFileManager contentsOfDirectoryAtPath:error:]为每个文件调用了该函数,然后-[NSFileManager fileExistsAtPath:isDirectory:]在查看每个文件是否是目录是又调用了该函数。

在10.6上,我们可以用-[NSFileManagercontentsOfDirectoryAtURL:includingPropertiesForKeys:options:error:]来代替-[NSFileManager contentsOfDirectoryAtPath:error:],这样这两个命令就可以合并成一个。我想保持10.6的兼容性,所以,我直接使用readdir和lstat来进行遍历。

这个修改使用服务端的文件夹读取速度提高了2倍。

降低内存占用
在Cocoa中,内存峰值主要是由于循环中自动释放对象累加引起的。我们可以在循环中插入NSAutoreleasePool分配及清空来改进,但这太慢。最好的方法是在内存限制区域中清空自动释放池。我在iPhone的整个解析和转换代码中采用了这个方法。

我还清除了一些不需要的内存拷贝方法(从网络数据缓存转换为NSString,再转换回UTF8字符串改为直接将网络数据以UTF8字符串形式传递)。

采用这些方法大概将解析的操作速度提高了2倍。

内存碎片
在降低内存峰值后,我们在试图分配大对象时仍然会遇到内存错误,尽管内存使用只有16MB。

在进行一些调查后,我意识到在分配更小或更大的字符片断时我的解析器产生了内存碎片,从而使用在数据结构中连续的字符串实际上在内存中并不连续。尽管只有大概50%的内存被使用,但是由于解析过程中字符串、数组、字典的内存分配是分散的,所以没有一个单独的、连续的2MB空间来加载和回放音频。

最简单的解决方法是将每几行的数据拷贝到适当的连续的位置,并释放掉原来不连续的内存。这样做后,内存分配的问题就解决了。

当然,这样将会花费一些额外的时间来,但是在内存方面的改进还是值得的。

将工作移出关的循

最后,我发现每一行都会将解释后的数据转换为一个类。这项工作主要是将字符串分配对适当的类对象,这一步不容易避免。

当然,理想状态下,解析和对象构建可以是一个完整的处理过程,但是由于解析过程是通用的,而不是单独为这个程序写的,所以无法以最好的格式生成数据,而需要额外的转换来处理数据。由于开发时间限制,我没有整合这两个组件,尽管我们可以由此获得速度上的提升。

在这个处理过程中,我同样为每个对象创建了一个NSInvocation来处理点击表格等用户交互事件并为每个对象指定一个UIImage对象。

因为只有两个图片和两个可用的动作,这些对象可以在循环外创建,这样即可以最低限度地修改每一行(NSInvocation参数不同时),也可以指定为as-is(UIImage情况下)。

这些看起来微小的修改产生的结果是十倍的性能提升。

新的


在做完这些修改后,结果变成如下:


 

20,000行测试环境下现地的速度比原来提升了4倍,而100,000行情况下也能成功进行,音频文件一旦加载就可以播放。

在模器中运行一百万行数据


一百万行的数据需要通过网络传输121MB的数据—这在128MB的iPhone上是无法工作的。

在我模拟器上运行1,000,000行的数据,这种情况下没有内存限制。这个测试花费了7.5秒来加载(整个操作都使用lstat)。

在大概800,000行(每行40个像素)后,UITableView就无法再通过CGFloat来精确定位每个像素了,每行会错位16个像素或者导致每行位置不稳定。得出的结论是,UITableView不适合处理百万级别的数据量

结论

iPhone可以处理100,000行级别的表格,而且可以像滚动100行一样平滑。

优化并不意味着得写汇编代码。一两个小时的工作结果是4倍的性能提升和通过简单地调整代码更好的使用内存。

以下三点是最大的改进:
· 用紧凑的alloc和release对来取代autoreleased对象,且只需在两个loop中(在一些情况下移除整个分配)
· 在关键的构建循环中移除NSInvocation生成及图片查询
· 服务端减少lstat的调用(尽管这不是UITableView优化)

增长的可用内存可用于更多的分配—将不连续的对象集合重新分配到连续的内存位置。这同样也是典型的移除不需要的拷贝。
阅读(640) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~