Objective-C(五)系统框架

这是Objective-C系列的第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循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
NSArray *array = [NSArray array];
for (int i = 0; i < array.count; i++) {
id object = array[i];
//Do somethins with 'object'
}

NSDictionary *dic = [NSDictionary dictionary];
NSArray *keys = [dic allKeys];
for (int i = 0; i < keys.count; i++) {
id key = keys[i];
id object = dic[key];
//Do somethins with 'key' and 'values'

}

NSSet *set = [NSSet set];
NSArray *objects = [set allObjects];
for (int i = 0; i < objects.count; i++) {
id object = objects[i];
//Do somethins with 'object'
}

2.2.2 使用Objective-C 的 NSEnumerator遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSArray *array = [NSArray array];
NSEnumerator *enumerator = [array objectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil) {
//Do somethins with 'object'

}

NSDictionary *dic = [NSDictionary dictionary];
NSEnumerator *keyEnume = [dic keyEnumerator];
id key;
while ((key = [keyEnume nextObject]) != nil) {
id value = dic[key];
//Do somethins with 'key' and 'values'
}

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

还提供了倒序的enumerator:reverseObjectEnumerator。

2.2.3 快速遍历

Objective-C 2.0引入了快速遍历。为for增加了in这个关键字。

1
2
3
4
NSArray *array = [NSArray array];
for (id object in array) {
//Do somethins with 'object'
}

支持快速遍历的对象需要实现NSFastEnumeration协议,该协议只有一个方法:- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len;,关于该方法的实现细节可以去找资料。

快速遍历,语法最简单,且效率最高,但是我们遇到的情况是经常会用到下标,这就无能为力了。

2.2.4 基于Block的遍历方式

1
2
3
4
5
6
7
8
9
10
11
NSArray *array = [NSArray array];
//1.
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

//*stop = YES;
//就会跳出遍历
}];
//2.
[array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];

这种方式,最为方便,代码优雅。

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

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

Foundation框架定义了collection及其各种各种collection所对应的Objective-C类。与之相似,CoreFoundation框架也定义一套C语言API。比如NSArray对应于CFArray,这两种创建数组的方式也许有区别,然而可以在两者之间平滑转换,就是“无缝桥接”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//__bridge:NSArray -> CFArrayRef
//__bridge不会转换所有权,ARC仍然具备这个Objective-C对象所有权

NSArray *aNSArray = @[@1,@2,@3];
CFArrayRef aCFArray = (__bridge CFArrayRef)aNSArray;
NSLog(@"size of array = %li",CFArrayGetCount(aCFArray));

//__bridge_retained:NSArray -> CFArrayRef
//__bridge_retained会转换所有权,ARC失去所有权,而且在不使用时,必须释放。

NSArray *bNSArray = @[@1,@2,@3];
CFArrayRef bCFArray = (__bridge_retained CFArrayRef)bNSArray;
CFRelease(bCFArray);

//__bridge_transfer:CFArrayRef -> NSArray

NSString *values[] = {@"hello", @"world"};
CFArrayRef cCFArray = CFArrayCreate(kCFAllocatorDefault, (void *)values, (CFIndex)2, NULL);
NSArray *cNSArray = (__bridge_transfer NSArray*)cCFArray;
  • __bridge:NSArray -> CFArrayRef,不会转换所有权,ARC仍然具备这个Objective-C对象所有权;
  • __bridge_retained:NSArray -> CFArrayRef,ARC失去所有权,而且在不使用Core Foundation时,必须释放;
  • __bridge_transfer:CFArrayRef -> NSArray。

在使用Foundation框架中的字典对象时,会遇到一个大问题,那就是其键的内存管理语义为“copy”,其值的内存管理语义为“retain”。除非使用更强大的无缝桥接,下面是关于如何创建一个Core Foundation字典的过程:

下面是一个CF字典的定义:

1
2
3
4
5
6
CFMutableDictionaryRef CFDictionaryCreateMutable(
CFAllocatorRef allocator, //要使用的内存分配器,分配器负责分配及回收内存,通常使用NULL,表示采用默认的分配
CFIndex capacity, //字典的初始大小
const CFDictionaryKeyCallBacks *keyCallBacks,
const CFDictionaryValueCallBacks *valueCallBacks
);

后面两个回调函数集,用于指示字典中的键和值在遇到各种事件时应该执行何种操作。这两个参数都是指向结构体的指针,对应的结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct {
CFIndex version; //版本号,目前为0,苹果可能会修改这个结构体,所以标识版本来区分
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
CFDictionaryHashCallBack hash;
} CFDictionaryKeyCallBacks;

typedef struct {
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
} CFDictionaryValueCallBacks;

其中参数声明如下:

1
2
3
4
5
typedef const void *	(*CFDictionaryRetainCallBack)(CFAllocatorRef allocator, const void *value);			//函数指针
typedef void (*CFDictionaryReleaseCallBack)(CFAllocatorRef allocator, const void *value);
typedef CFStringRef (*CFDictionaryCopyDescriptionCallBack)(const void *value);
typedef Boolean (*CFDictionaryEqualCallBack)(const void *value1, const void *value2);
typedef CFHashCode (*CFDictionaryHashCallBack)(const void *value);

下面自定义实现这些函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const void* JSRetainCallBack(CFAllocatorRef allocator ,const void *value)
{
return CFRetain(value);
}

void JSReleaseCallBack(CFAllocatorRef allocator ,const void *value)
{
CFRelease(value);
}

CFDictionaryKeyCallBacks keyCallbacks = {
0,
JSRetainCallBack,
JSReleaseCallBack,
NULL, //采用默认
CFEqual, //与NSMutableDictionary一致,最终会调用NSObject的“isEqual:”
CFHash //与NSMutableDictionary一致,最终会调用“hash”
};

CFDictionaryValueCallBacks valueCallbacks = {
0,
JSRetainCallBack,
JSReleaseCallBack,
NULL,
CFEqual
};

在实现这些函数后,便可以创建自定义的CF字典对象:

1
2
CFMutableDictionaryRef aCFMutableDic = CFDictionaryCreateMutable(NULL, 0, &keyCallbacks, &valueCallbacks);
NSMutableDictionary *anNSDictionary = (__bridge_transfer NSMutableDictionary *)aCFMutableDic;

那么对象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

1
+ (void)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

1
+ (void)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方法里初始化。