前言
相信很多iOS从业者都知道Foundation
对象与Core Foundation
对象,前者是Objective-C对象,在ARC中会自动管理它们的生命周期,后者是C对象,在ARC中需要开发者手动管理其生命周期,以免造成内存泄漏。两者之间可以通过“无缝桥接”技术互相转换。
一、转换方式
举个例子
// NSObject 转 C obj
NSArray *nsArray = [NSArray new];
CFArrayRef cfArray = (__bridge CFArrayRef)nsArray;
// C obj 转 NSObject
CFStringRef cfStr = CFStringCreateWithCString(NULL, "cString", kCFStringEncodingUTF8);
NSString * nsStr = (__bridge NSString *)cfStr;
上面的代码中,__bridge表示不改变对象的管理权所有者。
例如在第一个例子里,nsArray
转换成cfArray
对象后,nsArray
依然由ARC管理。开发者无需手动管理,即不需要手动释放cfArray
。
而在第二个例子里,cfStr
转换成nsStr
后,cfStr
不会转换管理者,仍然需要开发人员管理其生命周期,最后需要调用CFRelease(cfStr);
来释放cfStr
。
日常开发中,我们最常用的就是__bridge
桥式转换,但是也有别的桥式转换是指定的,例如__bridge_retained
和__bridge_transfer
。
__bridge_retained
是用于在将Foundation对象转换成Core Foundation对象时,进行ARC内存管理权的剥夺,意味着ARC将交出对象的所有权。若是在第一个例子中改成用__bridge_retained
,那么开发者需要用完数组之后就要加上 CFRelease(cfArray)
以释放其内存。
而__bridge_transfer
则与之相反,它用于将Core Foundation对象转换成Foundation对象时将管理权交给ARC,开发者无需再关心其内存释放。
这三种转换方式都是 “桥式转换”。
二、桥式转换用途
相信很多单纯objc语言开发的程序员在平日的开发中很少会用到桥式转换,那么为什么需要桥式转换呢?什么场景下我们会用到桥式转换呢?
其实在 Foundation 框架中,objc类所具备的某些功能,是 CoreFoundation 框架中的C语言数据结构所不具备的,反之亦然。举个例子,假如我们在使用objc的字典NSDictionary
时,如果我们想key存入的是一个我们自定义的模型对象,那就会出问题,例如下面的代码
为什么会出现在 -[MyCustomClass copyWithZone:]: unrecognized selector sent to instance 0x6000005a43e0 这个错误呢?这是因为在 Foundation 框架中的字典,其键的内存管理语义为 “拷贝”,而值的语义是却是“保留”,而NextModel
并没有遵循NSCopying
协议,并实现copyWithZone
方法,所以产生了崩溃。但是CoreFoundation 框架中的字典定义是可以自定义其内存管理语义 的,这时候我们就可以先定义CoreFoundation 框架中的字典,然后使用强大的无缝桥接技术,将它转换成 Foundation 框架中的字典,就能解决这个问题了。
以下就来说一下如何构造CoreFoundation的字典,先来看看苹果的定义文档如下:
图片显示文档定义了CoreFoundation的字典CFMutableDictionaryRef
的构造函数是
CFMutableDictionaryRef CFDictionaryCreateMutable(CFAllocatorRef allocator,
CFIndex capacity,
const CFDictionaryKeyCallBacks *keyCallBacks,
const CFDictionaryValueCallBacks *valueCallBacks);
其中,allocator
表示内存分配器,负责分配和回收这些 CoreFoundation对象里的数据结构占用的内存,一般传NULL
表示使用默认的分配器。 capacity
声明字典的初始大小,熟悉C语言的开发都知道这只是初始默认创建的大小,只是向分配器提示了一开始应该分配多少内存,后面会根据它的数据插入而增大容量。 CFDictionaryKeyCallBacks
和CFDictionaryValueCallBacks
是两个指向结构体的指针,其构造如下图
除了version
表示版本号(目前都是填0),其他都是函数指针,它们定义了当各种事件发生时应该采用哪个函数来执行相关任务。关键的是需要把CFDictionaryKeyCallBacks
从copy改成retain,于是我仿写了一下代码如下:
//调用CFRetain函数来增加键的引用计数,并返回键。这将导致CFDictionary不会复制键,而是保留键的引用计数。
const void* myRetainCallback(CFAllocatorRef allocator, const void *value) {
return CFRetain(value);
}
void myReleaseCallback(CFAllocatorRef allocator, const void *value) {
CFRelease(value);
}
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
CFDictionaryKeyCallBacks keyCallbacks = {
0,
myRetainCallback,
myReleaseCallback,
NULL,
CFEqual,
CFHash
};
CFDictionaryValueCallBacks valueCallbacks = {
0,
myRetainCallback,
myReleaseCallback,
NULL,
CFEqual
};
//创建CoreFoundation的字典aCFDictionary
CFMutableDictionaryRef aCFDictionary = CFDictionaryCreateMutable(NULL, 0, &keyCallbacks, &valueCallbacks);
//将CoreFoundation的字典aCFDictionary转换成Foundation的字典anNSDictionary
NSMutableDictionary *anNSDictionary = (__bridge_transfer NSMutableDictionary *)aCFDictionary;
MyCustomClass *customKey = [[MyCustomClass alloc] init];
[anNSDictionary setObject:@"" forKey:customKey];
}
上面这段代码里,CFDictionaryKeyCallBacks
引用我自定义的myRetainCallback
来管理键的引用计数,从而来避免要求 MyCustomClass
实现 NSCopying
协议。但是需要注意的这种情况下是键是可变的,在修改键时可能会更改键的值,从而使键在字典中的位置变得不确定,因此必须小心使用这种方法。其实最好还是建议使用不可变键,或者在修改键时使用自定义的回调函数来确保字典中的键始终保持不变。
三、后续
当我以为这样已经结束的时候,我把我的代码运行了一下,结果还是报了 -[MyCustomClass copyWithZone:]: unrecognized selector sent to instance 0x600003cd42b0 的错误,无论怎么改都无补于事,难道网上的资料是错误的吗?我尝试询问ChatGpt,得到以下的回答
我怀疑是objc经过多个版本的迭代和更新,在NSDictionay
运行到setValue:forkey:
时,并不会预先检查其运行管理语义,而且直接自动寻找这个key类里面的copyWithZone
方法,找不到则直接抛出异常,导致运行失败,如果有其他想法的人,欢迎和我分享讨论,感谢!