第49条:对自定义其内存管理语义的collection使用无缝桥接
本条要点:(作者总结)
- 通过无缝桥接技术,可以在 Foundation 框架中的 Objective-C 对象与 CoreFoundation 框架中的 C 语言数据结构之间来回转换。
- 在 CoreFoundation 层面创建collection 时,可以指定许多回调函数,这些函数表示此collection 应如何处理其元素。然后,可运用无缝桥接技术,将其转换成具备特殊内存管理语义的 Objective-C collection。
Objective-C 的系统库包含相当多的collection 类,其中有各种数组、各种字典、各种set。Foundation 框架定义了这些 collection 及其他各种 collection 所对应的 Objective-C 类。与之相似,CoreFoundation 框架也定义了一套C 语言API,用于操作表示这些collection 及其他各种collection 的数据结构。例如,NSArray 是Foundation 框架中表示数组的 Objective-C 类,而CFArray 则是 CoreFoundation 框架中的等价物。这两种创建数组的方式也许有区别,然而有项强大的功能可在这两个类型之间平滑转换,它就是 “无缝桥接”(toll-free bridging)。
使用“无缝桥接”技术,可以在定义于 Foundation 框架中的 Objective-C 类和定义于 CoreFoundation 框架中的C数据结构之间相互转换。笔者将 C 语言级别的API 称为数据结构,而没有称其类或对象,这是因为它们与Objective-C 中的类或对象并不相同。例如,CFArray 要通过 CFArrayRef 来引用,而这是指向 struct_CFArray 的指针。CFArrayGetCount 这种函数则可以操作此 struct ,以获取数组大小。这和 Objective-C 中的对应物不同,在 Objective-C 中,可以创建NSArray 对象,并在该对象上调用 count 方法,以获取数组大小。
下列代码演示了简单的无缝桥接:
NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (_bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));
// Output: size of array = 5
转换操作中的 __bridge 告诉ARC 如何处理转换操作所涉及的 Objective-C 对象。__bridge 本身的意思是:ARC 仍然具备这个 Objective-C 对象的所用权。而 __bridge_retained 则与之相反,意味着ARC 将交出对象的所有权。若是前面那段代码改用它来实现,那么用完数组之后就要加上 CFRelease(aCFArray) 以释放其内存。与之相似,反向转换可通过 __bridge_transfer 来实现。比方说,想把 CFArrayRef 转换为 NAArray *,并且想令 ARC 获得对象所有权,那么就可以采用此种转换方式。这三种转换方式称为 “桥式转换”(bridged cast)。
可是,你也许会问:以纯 Objective-C 来编写应用程序时,为何要用到这种功能?这是因为:Foundation 框架中的 Objective-C 类所具备的某些功能,是 CoreFoundation 框架中的C语言数据结构所不具备的,反之亦然。在使用 Foundation 框架中的字典对象时会遇到一个大问题,那就是其键的内存管理语义为 “拷贝”,而值的语义是却是“保留”。除非使用强大的无缝桥接技术,否则无法改变其语义。
CoreFoundation 框架中的字典类型叫做 CFDictionary。其可变版本称为 CFMutableDictionary 。创建 CFMutableDictionary 时,可以通过下列方法来指定键和值的内存管理语义:
CFMutableDictionaryRef CFDictionaryCreateMutable (
CFAllocatorRef allocator,
CFIndex capacity,
const CFDictionaryKeyCallBacks *keyCallBacks,
const CFDictionaryValueCallBacks *valueCallBacks
}
首个参数表示将要使用的内存分配器(allocator)(也称配置器)。如果你大部分时间都在编写 Objective-C 代码,那么也许会对 CoreFoundation 框架中的这部分稍感陌生。CoreFoundation 对象里的数据结构需要占用内存,而分配器负责分配及回收这些内存。开发者通常为这个参数传入 NULL,表示采用默认的分配器。
第二个参数定义了字典的初始大小。它并不会限制字典的最大容量,只是向分配器提示了一开始应该分配多少内存。假如要创建的字典含有10 个对象,那就向该参数传入 10。
最后两个参数值得注意。它们定义了许多回调函数,用于指示字典中的键和值在遇到各种事件时应该执行何种操作。这两个参数都是指向结构体的指针,二者所对应的结构体如下:
struct CFDictionaryKeyCallBacks {
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
CFDictionaryHashCallBack hash;
};
struct CFDictionaryValueCallBacks {
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
}
version 参数目前应设为 0。当前编程时总是取这个值,不过将来苹果公司也许会修改此结构体,所以要预留该值以表示版本号。这个参数可以用于检测新版与旧版数据结构之间是否兼容。结构体中的其余成员都是函数指针,它们定义了当各种事件发生时应该采用哪个函数来执行相关任务。比方说,如果字典中加入了新的键与值,那么就会调用 retain 函数。此参数的类型定义如下:
typedef const void* (*CFDictionaryRetainCallBack) {
CFAllocatorRef allocator,
const void *value
);
由此可见, retain 是个函数指针,其所指向的函数接受两个参数,其类型分别是 CFAllocatorRef 与 const void *。传给此函数的 value 参数表示即将加入字典中的键或值。而返回 void * 则表示要加到字典里的最终值。开发者可以用下列代码来实现这个回调函数:
const void * CustomCallback(CFAllocatorRef allocator, const void *value) {
return value;
}
这么写只是把即将加入字典中的值照原样返回。于是,如果用它充当retain 回调函数来创建字典,那么该字典就不会“保留”键与值了。将此种写法与无缝桥接搭配起来,就可以创建出特殊的 NSDictionary 对象,而其行为与用 Objective-C 创建出来的普通字典不同。
下列范例代码完整演示了这种字典的创建步骤:
#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
const void* EOCRetainCallback(CFAllocatorRef allocator, const void *value) {
return CFRetain(value);
}
void EOCReleaseCallback(CFAllocatorRef allocator, const void *value) {
CFRelease(value);
}
CFDictionaryKeyCallBacks keyCallbacks = {
0,
EOCRetainCallback,
EOCReleaseCallback,
NULL,
CFEqual,
CFHash
};
CFDictionaryValueCallBacks valueCallbacks = {
0,
EOCRetainCallback,
EOCReleaseCallback,
NULL,
CFEqual
};
CFMutableDictionaryRef aCFDictionary = CFDictionaryCreateMutable(NULL,
0,
&keyCallbacks,
&valueCallbacks);
NSMutableDictionary *anNSDictionary = (__bridge_transfer NSMutableDictionary *)aCFDictionary;
在设定回调函数时,copyDescription 取值为 NULL,因为采用默认实现就很好。而 equal 与 hash 回调函数分别设为 CFEqual 与 CFHash,因为这二者所采用的做法与NSMutableDictionary 的默认实现相同。CFEqual 最终会调用 NSObject 的 “isEqual:” 方法,而 CFHash 则会调用 hash 方法。由此可以看出无缝桥接技术更为强大的一面。
键与值所对应的 retain 与 release 回调函数指针分别指向 EOCRetainCallBack 与 EOCReleaseCallBack 函数。为什么要这么做呢?回想一下,前面说过,在向 NSMutableDictionary 中加入键和值时,字典会自动 “拷贝”键并“保留”值。如果用作键的对象不支持拷贝操作,那会如何呢?此时就不能使用普通 NSMutableDictionary 了,假如用了,会导致下面这种运行期错误:
***Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '-[EOCClass copyWithZone:]: unrecognized selector sent to instance
0x7fd069c080b0
该错误表明,对象所属的类不支持NSCopying 协议,因为 "copyWithZone:" 方法未实现。开发者可以直接在 CoreFoundation 层创建字典,于是就能修改内存管理语义,对键执行“保留”而非“拷贝”操作了。
通过类似手段,也可创建出不保留其元素对象的数组或 set。这么做或许有用,因为有时如果令数组保留对象的话,那么可能会引入 “保留环”。不过要注意,这个问题可以改用更好的办法来解决。不保留其元素对象的那种数组,很容易出错。要是数组中的某个对象已为系统所回收,而应用程序又去访问对象的话,那就很可能就崩溃了。
END