|
|
51CTO旗下网站
|
|
移动端

架构注意事项

《iOS和macOS性能优化:Cocoa、Cocoa Touch、Objective-C和Swift》第7章内存:陷阱和优化技巧,本章将围绕这个话题来进行讨论。除此之外,我们将展示一些陷阱,尤其是在Objective-C 代码中经常出现的问题。本节为大家介绍架构注意事项。

作者:李俊阳 等译来源:电子工业出版社|2018-07-17 17:00

架构注意事项

在第4 章中,我们已经了解过SAX 和DOM 解析器之间存在的架构风格差异。面向数据流的SAX 解析器会有效利用合适大小的内存2,而DOM 解析器则会使用与整个文件大小成正比的内存。然而,正如即将看到的,架构风格带来的影响很可能会更加明显。

示例7.4 会将这个只有一个成员的NSArray 对象的描述信息打印出来。也就是说其最终的描述应该遵循着“(( )),”这一类形式,然后根据字节顺序展示出来,但是由于这两个数组彼此嵌套,因此会导致程序崩溃。在我的系统上,它会在所有内存用完之前,先将堆空间耗尽,不过最终还是会将内存消耗殆尽。

示例7.4 访问两个嵌套的NSMutableArray 的description 属性时导致的崩溃

  1. import Foundation  
  2. var a=NSMutableArray()  
  3. var b=NSMutableArray()  
  4. a.addObject(b) // Swift 3: a.add(b)  
  5. b.addObject(a) // Swift 3: b.add(a)  
  6. print(a.description) 

你可能会猜想,这个结果怎么会与软件架构有关呢,难道不是NSArray 或者description 的一个Bug 吗?实际上并不是。示例7.5 简单地将数组嵌套在了一起,不过并没有相互递归引用。现在就不会发生崩溃了,我们只是得到了如图7.5 所示的内存临时对象消耗曲线呈现n2 指数增长趋势。而实际description 的大小是呈线性增长的,我们将两者放在同一个图中进行比较。

示例7.5 测量获取嵌套NSArray 的description 信息时的内存消耗

  1. import Foundation  
  2. let numArrays=Int(Process.arguments[1])!  
  3. let before=mstats();  
  4. var base:NSMutableArray=["Hello World"]  
  5. for i in 1...numArrays {  
  6. base=[base]  
  7. }  
  8. let b=base.description  
  9. let after=mstats();  
  10. print("memory used: \( after.bytes_used - before.bytes_used)") 

该图最多只显示了嵌套级别为50 时的内存使用量,因为n2 分配曲线之后变得太大,以至于在以相同比例显示时,描述description 所占用空间的曲线将贴在X 轴上。如果使用了750 个嵌套的NSArray,那么内存消耗量将会增长到500MB,CPU 消耗的时间将增长到78 ms。如果将NSArray 的嵌套数量翻倍至1500 个,那么内存占用率和CPU消耗将分别达到2GB 和306ms,整整增长了近4 倍。在64 位机器上,我曾设法用几千个嵌套的NSArray 实例,成功地将地址空间消耗殆尽!

(如果真的想自行尝试的话,那么请准备好花生饮料矿泉水,因为等待的时间相当长,随后将机器重启,完成交换空间的清理。实际上,也很有可能已经耗尽了交换空间,从而导致内核错误,所以我并不推荐去尝试。如果你仍执意于此的话,那么保存工作进度,事先将系统备份好,不要后面怪我没有事先警告!)

内存增长的原因并不是因为description 方法的实现缺陷所导致的,而是由于调用并返回架构风格的基本特征所决定的。在这种风格中,会去调用一个函数或者方法,并得到返回的结果1。因此这就是完整的交互过程,结果的返回也必须完成;“部分结果”这种概念并不存在。

当我们需要将这些中间结果组合成一个最终的结果时,就意味着首先需要完全构建所有的部分结果,然后分配足够的内存来保存组合之后的结果,再将这些部分结果复制到最终的结果中。在每层嵌套中都会重复这个操作,因此这会达到O(m . n)的总复杂度,其中m 为嵌套深度,n 为最终结果的总大小。在这里给出的示例中(诚然会有些许误差),嵌套深度等于结果总大小(m = n),这也就是所测量的n2 结果。

幸运的是,有一个非常简单的解决方案,那就是使用数据流方法,这类似于之前使用SAX 来进行XML 进行解析,不过要更为简单,因为我们需要的是生成结果而不是进行解析。因此,不会将所有的部分结果全部用来构建,而是将部分结果累加到一开始所分配的共享缓冲区中,然后将这段缓冲区传递给我们的方法,而不是等待方法将结果返回。示例7.6 中的describeOn:方法实现了基于累加器的NSArray 数据流设计。其余的代码则与当前的description 协议集成在一起,并暴露出一个封装了累加器配置的fastDescription 方法。

示例7.6 describeOn:用数据流的方式来输出描述信息

  1. #import <Foundation/Foundation.h> 
  2. @implementation NSArray(describeOn)  
  3. -(void)describeOn:(NSMutableString*)description  
  4. {  
  5. [description appendString:@"( "];  
  6. NSString *separator=@"";  
  7. for (id obj in self ) {  
  8. @autorelease {  
  9. [obj appendString:separator];  
  10. [obj describeOn:description];  
  11. separator=@", ";  
  12. }  
  13. }  
  14. [description appendString:@")"];  
  15. }  
  16. @end  
  17. @implementation NSObject(describeOn)  
  18. -(void)describeOn:(NSMutableString*)description  
  19. {  
  20. [description appendString:[self description]];  
  21. }  
  22. -(NSString*)fastDescription {  
  23. NSMutableString *s=[NSMutableString string];  
  24. [self describeOn:s];  
  25. return s;  
  26. }  
  27. @end 

将示例7.5 中的description 方法替换为fastDescription 之后,会产生显著不同的结果,如图7.6 所示。不仅总内存消耗图保持相对线性增长,其消耗也非常接近于description 所消耗的大小,此外还要注意到,X 轴的起始深度为5000,而这是图7.5 中横轴坐标最后一个值的100 倍。在5000 个对象标记处,所消耗的总内存大约为20KB。如果要获取1500 个项目的描述信息,则n2 算法需要2GB 内存,而这里我们获取10 倍项目的描述信息仅仅才使用了60KB 的内存,因此累加算法在这一点上把效率提高到了近35000 倍。

虽然这是一个很极端的例子,但一般的原则是,带有部分结果的海量数据集是不能使用“调用并返回”的架构来进行处理的,甚至具备嵌套的中型数据集也没必要将所有的部分结果进行重新计算,在这种风格中,嵌套结构的空间/时间复杂度固定为m . n,从而带来极大的损耗。这种风格对于Objective-C 而言损耗会更大,因为和其他语言相比,从一个方法中返回一个新构建的对象时,Objective-C 的对象创建成本和所需的相关自动释放成本明显要高很多。如果只是将数据添加到已经存在的对象中,则损耗将会小很多。

所以在设计数据处理方面的API 时,尝试使用数据流的方式来进行处理,例如,可以通过某种允许结果递增的累加器来完成数据传递。这种类型的对象示例比比皆是,例如,用于显示图形CoreGraphics 框架中的CGContextRef 对象,或者是更著名的UNIX文件标识符fd,还有stdio 中用于I/O 的FILE 对象。事实上,UNIX 很可能是数据流机制的推动者,因为UNIX 自身大量运用了管道及过滤器架构,并且大多数UNIX 过滤器都可以处理任意长度的数据,并没有明显的限制。

如示例7.6 所示,在一个调用并返回的便利方法中封装数据流机制通常是很简单的,而将调用并返回的API 转换为数据流格式,如果不重写的话,是不可能实现的。

现在明白了累加器的实现和使用,一个明显的扩展用例就是NSMutableString,通过累加器可以变得更容易使用。在下一章中,我们将会更多关注NSMutableString类族,同时也会关注示例7.4 中的无限递归。

喜欢的朋友可以添加我们的微信账号:

51CTO读书频道二维码


51CTO读书频道活动讨论群:365934973

【责任编辑:book TEL:(010)68476606】

回书目   上一节   下一节
点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

计算机与网络基础知识——考点解析及模拟训练

本书是根据全国计算机技术与软件专业技术资格(水平)考试的“计算机网络管理员考试大纲”所要求的考试范围而编写的辅导用书。全书共分10章...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊