Effective Objective-C 2.0(五)系统框架
00 分钟
2019-7-23
2019-7-23
type
status
date
slug
summary
tags
category
icon
password
这是Effective Objective-C 2.0系列的第5篇。

一、最佳实践

  • 多用Block枚举,少用for循环;
  • NSDictionary的键值内存语义不符合要求,可以自定义实现;
  • 构建缓存时选用NSCache而非NSDictionary
  • 实现自动缓存是应选用NSCache而非NSDictionary对象,因为NSCache可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会copy键;
  • 可以给NSCache设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度定义了缓存删减其中对象的时机,但是绝对不要把这些尺度当初可靠的“硬限制”。它们仅仅对NSCache起指导作用;
  • 将NSPureableData与NSCache搭配使用,可实现自动清除数据的功能,也就是说,当NSPureableData对象所占内存为系统所丢弃是,该对象那自身也会从缓存中移除;
  • 如果缓存使用得当,那么应用程序的响应就能提升。只有那种“重新计算起来很费事”的数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据;
  • 精简initalize与load的实现代码

二、实践详解

2.1 熟悉系统框架

将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,这样做出来的东西就叫做框架。有时为iOS平台构建的第三方框架所使用的是静态库(static library),这是因为iOS应用程序不允许在其中包括动态库。这些东西严格来讲,并不是真正的框架,然而也经常视为框架。不过,所有iOS平台的系统框架仍然使用动态库。
请记住:用纯C写成的框架与用Objective-C写成的一样重要,若要想成为优秀的Objective-C的开发者,应该掌握C语言的核心概念。

2.2 多用块枚举,少用for循环

2.2.1 for循环

2.2.2 使用Objective-C 的 NSEnumerator遍历

这种遍历方式,风格比较统一,而且针对不同的collection提供不同的enumerator。
还提供了倒序的enumerator:reverseObjectEnumerator。

2.2.3 快速遍历

Objective-C 2.0引入了快速遍历。为for增加了in这个关键字。
支持快速遍历的对象需要实现NSFastEnumeration协议,该协议只有一个方法:- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len;,关于该方法的实现细节可以去找资料。
快速遍历,语法最简单,且效率最高,但是我们遇到的情况是经常会用到下标,这就无能为力了。

2.2.4 基于Block的遍历方式

这种方式,最为方便,代码优雅。
第2个方法,还可以指定NSEnumerationOptions掩码,NSEnumerationConcurrent可以底层GCD执行并发各轮迭代,NSEnumerationReverse反向遍历。

2.3 对自定义其内存管理语义的collection使用无缝桥接

Foundation框架定义了collection及其各种各种collection所对应的Objective-C类。与之相似,CoreFoundation框架也定义一套C语言API。比如NSArray对应于CFArray,这两种创建数组的方式也许有区别,然而可以在两者之间平滑转换,就是“无缝桥接”。
  • __bridge:NSArray -> CFArrayRef,不会转换所有权,ARC仍然具备这个Objective-C对象所有权;
  • __bridge_retained:NSArray -> CFArrayRef,ARC失去所有权,而且在不使用Core Foundation时,必须释放;
  • __bridge_transfer:CFArrayRef -> NSArray。
在使用Foundation框架中的字典对象时,会遇到一个大问题,那就是其键的内存管理语义为“copy”,其值的内存管理语义为“retain”。除非使用更强大的无缝桥接,下面是关于如何创建一个Core Foundation字典的过程:
下面是一个CF字典的定义:
后面两个回调函数集,用于指示字典中的键和值在遇到各种事件时应该执行何种操作。这两个参数都是指向结构体的指针,对应的结构体如下:
其中参数声明如下:
下面自定义实现这些函数:
在实现这些函数后,便可以创建自定义的CF字典对象:
那么对象aCFMutableDic的键值的内存管理语义都是“retain”了。
这样就避免了,在NSMutableDictionary中加入键和值时,字典会自动“copy”而“retain”值。如果作为键的对象不支持拷贝操作,就会导致下面运行期错误: *** Terminating app due to uncaught exception 'NSInvalidArgumentException',reason:'-[JSPerson copyWithZone:]: unrecognized selector sent to instance 0x7fd069c080b0
  • 通过无缝桥接技术,可以在Foundation和Core Foundation中的对象之间来回转换;
  • 在Core Foundation层面创建collection时,可以指定许多回调函数,这些函数表示此collection应如何处理这些函数。然后,可运用无缝桥接技术,将其转换成具备特殊内存管理语义的Objective-C collection。

2.4 构建缓存时选用NSCache而非NSDictionary

关于缓存部分,准备写系列的总结:在这儿先占个坑:待补-缓存(三)NSCache
  • 实现自动缓存是应选用NSCache而非NSDictionary对象,因为NSCache可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会copy键;
编者按:因此,NSCache的键不需要实现NSCopying协议。
  • 可以给NSCache设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度定义了缓存删减其中对象的时机,但是绝对不要把这些尺度当初可靠的“硬限制”。它们仅仅对NSCache起指导作用;
  • 将NSPureableData与NSCache搭配使用,可实现自动清除数据的功能,也就是说,当NSPureableData对象所占内存为系统所丢弃是,该对象那自身也会从缓存中移除;
  • 如果缓存使用得当,那么应用程序的响应就能提升。只有那种“重新计算起来很费事”的数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据;

2.5 精简initalize与load的实现代码

有时候,类必须先执行某些初始化操作,然后才能正常使用。

2.5.1 load

  • 对于加入运行期的每个类(class)及分类(category)来说,必定会调用此方法,而且仅调用一次;
  • 当包含类或分类的程序载入系统时,就会执行此方法,而这通常就是指应用程序启动的时候,若是iOS程序肯定会在此时执行。Mac程序则更自由一些,它们可以使用“动态加载”之类的特性,等应用程序启动好之后再去加载;
  • 不遵循继承规则:
  • (1)分类和类都定义了load方法,那么先调用类的,再调用分类里面的;
  • (2)如果某个类本身未实现load方法,那么不管其父类是否实现此方法,系统都不会调用该类此方法。
load的问题
  • (1)执行load时,运行期系统处于“脆弱状态(fragile state)”。在执行子类的load时,必会先调用所有超类的load方法,而如果代码还依赖其他程序库,那么程序库里类的load也必定会先执行。然而,根据某个给定的程序库,却无法判断出其中各个类的载入顺序。因此,在load方法中调用其他类是不安全的。
  • (2)load方法执行时,会阻塞当前应用程序。
  • 综合,出于带来的负面影响,load方法务必要精简,不要做过多的事情。要么就是脆弱的调用而产生崩溃,要么就是繁杂的代码逻辑使得程序长时间无响应。所以,load方法在当前写程序,用处一般不大。

2.5.2 initialize

对于每个类来说,该方法会在程序首次使用该类之前调用,且只调用一次。它是由运行期系统来调用的,绝不应该通过代码直接调用。与load非常相似,却略有区别:
编者按:只调用一次,其实并不准确,可能会调用多次。
  • 惰性调用:程序用到该类时,再调用;
  • 运行期在执行该方法时,是正常状态的,可以调用任何类的任意方法。其次,initialize方法一定会在“线程安全的环境”中执行,也就是说,只有执行initialize的那个线程可以操作类或类实例,其他线程都要先阻塞,等着initialize执行完;
  • initialize方法与其他消息一样,如果某个类没实现它,而超类实现了,就会调用超类的实现代码,这个跟load也有区别;
initialize如果不精简的问题
  • (1)对于某个类来说,任何线程都可能是初次使用到该类的线程,假如刚好UI线程用到该类,那么UI线程会一直阻塞,直到initialize执行完毕。
  • (2)如果在initialize中过多的逻辑,就会使得很多操作依赖于初始化时间,而一个类的初始化有时候会依赖系统的,假如系统优化后,初始化时机更改,那么可能会存在潜在的隐患。即,代码依赖于特定的时间点是很危险的事。
  • (3)如果在initialize中复杂的逻辑,导致A类的initialize中调用B类的方法,而B类的方法又调用A类的数据这种循环调用时,代码就无法正常运行;
  • 综合以上,initialize应该只用来设置内部数据,不应该调用其他类的方法,甚至本类的方法也最好不要调用。
要点:
  • 在加载阶段,如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类中的先调用。与其他方法不同,load方法不参与覆写机制。
  • 首次使用某个类之前,系统会向其发送initialize消息。由于此方法遵从普通的覆写规则,所以通常应该在里面判断当前要初始化的是哪一个类;
  • load和initialize方法都应该实现的精简一些,这有助于保持应用的响应能力,也能减少引入“依赖环”的几率;
  • 无法在编译器设定的全局常量,可以放在initialize方法里初始化。