之前一段时间公司项目中遇到CoreFoundation框架相关的问题,于是系统的将苹果官方文档中的CF框架内存管理的部分文档看了一遍,顺便自己翻译成了中文。权当记录自己的学习过程。
CF框架使用分配器来进行内存管理,一种引用计数机制。一个对象的所有权由这个函数的功能所揭示。
内存管理是高效,准确使用CF框架的基础。
所有权策略
应用程序通过CF框架来获取,创建和释放对象。为了确保不造成内存泄露,CF定义了一些获取和创建对象的规则。
基本原理
一个对象可能有一个或多个所有者,为了记录持有者的数量,我们使用引用计数。如果一个对象的持有者为0,那么引用计数为0,就会被释放掉。CF定义如下几个规则来定义释放和持有。
1、谁创建,谁拥有
2、不持有非自己创建的对象,如果要持有就保留他。
3、如果持有一个对象,那么你负责释放他。
命名规范
我们可以通过多种方式来获得对象的引用。为了和CF内存管理策略保持一致,你需要知道通过一个函数返回时,你是否持有该对象,这样你就可以知道采取什么措施来进行内存管理。CF已经建立了一些函数命名规范,可以判定是否持有一个对象。简而言之,如果一个函数包含了Create或者Copy,那么你就持有这个对象。如果一个函数名包含了Get那么你并不持有该对象。
注:Cocoa框架也定义了一些列相似的命名规则用于内存管理。CF框架的命名规则,特别是create单词,只使用在返回CF对象的C函数中。
创建规则
CF函数名表示了你什么时候会持有一个返回对象。
1、函数名中包含create关键字
2、对象复制函数中含有copy关键字
当你持有一个对象时,当不再使用时,需要负责释放该对象。考虑如下这些例子
CFTimeZoneRef CFTimeZoneCreateWithTimeIntervalFromGMT (CFAllocatorRef allocator, CFTimeInterval ti);
CFDictionaryRef CFTimeZoneCopyAbbreviationDictionary (void);
CFBundleRef CFBundleCreate (CFAllocatorRef allocator, CFURLRef bundleURL);
第一个函数包含了create关键字,并创建了一个CFTimeZone对象。你持有这个对象并且负责释放他。
第二个例子包含了copy关键字,并且拷贝了一个time zone对象。同样的你也持有该对象,并负责释放他。
第三个函数CFBundleCreate 包含了关键字create,但是文档中描述该函数会返回一个已经存在的对象,同之前一样,我们持有这个对象,不论是否是一个新建的对象。如果返回了一个已经创建好的对象,它的引用计数都会增加,所以你仍然负责释放。
接下来的例子可能会更加复杂,但仍然遵循同样简单的规则
CF_EXPORT CFBagRef CFBagCreate(CFAllocatorRef allocator, const void **values, CFIndex numValues, const CFBagCallBacks *callBacks);
CF_EXPORT CFMutableBagRef CFBagCreateMutableCopy(CFAllocatorRef allocator, CFIndex capacity, CFBagRef bag);
CFBag 函数 CFBagCreateMutableCopy 同时有create和copy关键字。实际上这个是一个初始化函数,因为包含了create关键字。同时第一个参数是 CFAllocatorRef类型,这也更加表明了函数的意义。“Copy关键字表明这个函数将CFBagRef作为参数并返回该参数对象的复制。同时,该函数也表明了原始集合元素发生了什么,它们被复制到一个新创建的bag对象中。函数中的第二个copy和no copy字符串表明了原始集合对象所持有这些元素是如何操作的,即它们是否被拷贝或者没有。
获取规则
如果你从除了创建函数得到了一个对象,比如Get函数。那么你并不持有该对象,并且不负责管理对象的生命周期。如果你想确保在你使用期间不会被释放,你必须持有他。那么就要负责释放他当你不在使用该对象。
以 CFAttributedStringGetString 函数为例,一个为属性字符串返回字符串的函数。
如果一个属性字符串被释放,那么也会同时释放对应的备份字符串。如果这个属性字符串是备份字符串的唯一持有者,然后这个备份字符串现在没有持有者,那么它自己就会释放。当属性字符串被释放的时候如果你需要使用这个备份字符串,你必须使用 CFRetain 来持有该backing string,或者复制他。当使用完之后你必须负责释放,否则会造成内存泄露。
实例变量和传递参数
基于基本规则的一个推论是当你传递一个对象到另一个对象(比如函数参数),如果需要保持的话,你要确保接收者要能持有这个传递的对象。
为了理解这个,把我们自己想象成接收对象的实施者。当一个函数收到一个对象作为参数的时候,接收者最先不会持有这个对象。因此这个对象有可能在任何时候释放。除非接收者获得该对象的所有权。当接收者使用完该对象时,需要负责释放该对象的所有权。
一个实例
为了防止运行时错误和内存泄露,必须确保无论何时何地CF对象被接收,传递或者返回都必须坚持使用CF的内存策略。为了理解为什么当你并没有创建一个对象时保留对象的引用是有必要的,可以参考下面这个例子。假设你从另外一个对象中得到了值,如果值的对象随后被释放了,那么它也释放了该对象的所有权。如果这个对象是这个值的唯一的所有者,那么这个值就没有所有者就被释放掉了。现在你就持有了一个被释放掉的对象,这时候,如果你试图访问该对象,那么应用程序就会崩溃。
下面这段代码阐述了三个相同的情况。一个set功能函数,一个Get功能函数和一个保持一个CF对象直到一个确定的情况发生。首先是Set函数
static CFStringRef title = NULL;
void SetTitle(CFStringRef newTitle) {
CFStringRef temp = title;
title = CFStringCreateCopy(kCFAllocatorDefault , newTitle);
CFRelease(temp);
}
以上这个例子使用了静态 CFStringRef 变量来保留CFString 对象。我们可以使用其他方式来存储,当然你必须把它放到一个不是依附于接收函数的地方。在拷贝新title值并且释放旧title值之前,这个函数赋当前title值到一个本地变量。在拷贝之后才释放,是为了防止CFString 对象传递的是和当前保留的相同对象。
注意到上面的例子是被复制而不是简单的retain。这样做的原因是因为string有可变的类型。除了通过获取方法之外,我们认为它是不应该在传递过程中改变的。尽管,参数的类型是CFString类型的,但是CFMutableString类型的可能会被误传过去,因此先copy对象,保证传递的是不可变对象。如果一个对象在持有过程中有可能产生变化,那么就需要copy,来保证需要用到时不会变化。
Get函数就比较简单
CFStringRef GetTitle() {
return title; }
通过简单的返回一个对象,实际上你是返回一个弱引用对象。换句话说,指针值拷贝了接收者的变量,但是引用计数并没有改变。与该情况相同的是集合对象中元素的返回。
下面这个函数保留了一个从集合返回的对象,直到不再需要的时候就释放,这个对象假定是不可变的。
static CFStringRef title = NULL;
void MyFunction(CFDictionary dict, Boolean aFlag) {
if (!title && !aFlag) {
title = (CFStringRef)CFDictionaryGetValue(dict, CFSTR(“title”));
title = CFRetain(title);
}
/* Do something with title here. */
if (aFlag) {
CFRelease(title);
}
}
下面这个例子显示传递一个数字对象到一个数组中。数组的回调区分了对象被该集合对象持有了。所以在被添加之后就可以释放了。
float myFloat = 10.523987;
CFNumberRef myNumber = CFNumberCreate(kCFAllocatorDefault, kCFNumberFloatType, &myFloat);
CFMutableArrayRef myArray = CFArrayCreateMutable(kCFAllocatorDefault, 2, &kCFTypeArrayCallBacks);
CFArrayAppendValue(myArray, myNumber);
CFRelease(myNumber);
// code continues...
需要注意的是,如果释放了一个数组,可能有一个潜在的陷阱。在释放数组之后你可能还在使用这个数字变量。
CFRelease(myArray);
CFNumberRef otherNumber = // ... ;
CFComparisonResult comparison = CFNumberCompare(myNumber, otherNumber, NULL);
除非你retain了这个数字或者数组,或者将它们传递给一个引用了她们的对象,否则以上代码会导致问题。如果没有其他对象持有数组或者其中的元素,那么当数组释放之后,同时也释放了数组中的元素。这种情况下,也会导致数字对象的释放,所以比较函数会在一个释放的对象上操作,从而导致崩溃。
CF对象生命周期管理
一个CF对象的生命周期是由它的引用计数所决定的。引用计数是一个内部计数,由那些需要持有他的对象管理。当你创建或者复制一个CF对象时,引用计数被设置为1.需要持有该对象的持有者使用CFRetain来持有,并使引用计数增加1.随后,当不再需要的时候,就通过CFRelease释放该对象。当引用计数为0的时候,对象分配器就会释放对象内存。
保持对象引用
为了增加CF对象的引用计数,将对象作为CFRetain函数的参数传递到某个对象上。
/* myString is a CFStringRef received from elsewhere */
myString = (CFStringRef)CFRetain(myString);
释放对象的引用
为了减少对象的引用计数,将对象作为CFRelease函数的参数传递
> CFRelease(myString);
记住,永远不要直接销毁一个对象,例如直接free。当你不需要使用对象时,调用CFRelease方法。CF框架会在合适的实际销毁。
拷贝对象引用
当拷贝一个对象时,除了原始的对象引用计数,最后的对象会持有被拷贝的对象一个引用。
检测一个对象的引用计数
如果你想知道一个CF对象的引用计数,调用CFGetRetainCount函数
CFIndex count = CFGetRetainCount(myString);
注意,实际上检查CF对象的引用计数完全没有必要,除非是为了debug。如果需要知道对象的引用计数,确保你遵循了引用策略。
拷贝函数
总体而言,一个标准的拷贝操作,就是我们经常使用赋值操作。通常发生在我们使用"="运算符,将一个值赋给另一个变量。例如,表达式myInt2 = myInt1 就将 myInt1 的值从起内存地址复制到 myInt2 的内存地址中。随着复制操作,两个不同的内存区域拥有了相同的值。然而,当你试图以这样的方式拷贝CF对象的时候,要注意的是,你并不是复制对象本身,而是复制对象的指针。
如果你想要复制对象,必须使用CF提供的特殊的方法。例如,你必须使用CFStringCreateCopy来创建一个全新的CFString对象,有着和原始对象完全相同的内容。CF框架类型中有CreateCopy函数也提供了CreateMutableCopy,来返回对象的副本并可以修改。
浅复制
拷贝复合对象,比如包含了其他对象的集合对象,必须小心处理。就像你所期望的一样,使用 "=" 操作符进行拷贝操作,会导致集合对象的引用计数增加。与此形成对照的是,像CFString和CFData对象,CreateCopy函数提供这些混合对象的浅复制操作。对这些对象而言,浅复制意味这新的集合对象被创建,但是原始集合对象中所包含的对象引用计数并没有增加,只是对象的指针被复制到了新的容器中。这种类型的复制是十分有用的,例如,有一个不可变的数组,你想对他重新排序。在这种情况下,你不想复制所有元素,因为我们不需要改变她们,所以为什么要花费额外的内存?我们只是需要改变顺序而已。
深复制
当你想要创建一个全新的复合对象时,就必须使用深拷贝。深拷贝,同时将集合对象及集合对象中的元素都复制一份。当前发布的CF框架包含了深拷贝的属性列表(参阅CFPropertyListCreateDeepCopy)。如果你想要创建其他结构的深拷贝,你可以通过递归拷贝复合对象中的每个元素来实现。需要注意的是,实现这个函数的功能可能会导致复合对象的循环调用,由于直接或者间接的引用自己而导致递归循环。
创建函数使用分配器
CF框架中 的不透明类型都有一种或多种创建函数,创建并返回经过特殊方式初始化的对应类型的对象。所有创建函数将分配器对象作为第一个参数进行传递。一些函数由于分配和重新分配的意图有自己的分配器参数。
在这函数中,分配器参数有几个选项可供填写。
1、可以传递kCFAllocatorSystemDefault常量,用于指定一般性的系统分配器。
2、可以传NULL 用来指定当前的默认分配器(有可能是自定义或者系统的)和传递kCFAllocatorSystemDefault一样。
3、可以传递kCFAllocatorNull常量值,表明分配器并不会进行地址分配。一些创建函数对于特定的分配器有特定的参数用来重新分配或释放备份存储。通过为分配器指定kCFAllocatorNull,可以阻止重新分配或者重新释放。
4、可以通过CFGetAllocator 函数来获得分配器的引用,并且作为参数传递。这个技术使得我们可以将相关的对象放在同一个内存区域中,通过使用相同的分配器来分配。
5、可以传递一个自定义的分配器
如果使用一个自定义的分配器,并且想要成为默认分配器,使用CFAllocatorGetDefault函数来获得当前默认分配器的引用,并且把它存到本地变量中。当使用完自定义分配器的时候,使用CFAllocatorSetDefault来重设存储的分配器为默认分配器。
使用分配器上下文
CF框架中的每一个分配器都有一个上下文,上下文是一个定义了一个对象操作环境和函数指针组成的结构体。分配器的上下文环境是由CFAllocatorContext结构体定义的。除了函数指针,这个结构体还包含了版本号和用户定义数据所组成的字段。
Listing 1 The CFAllocatorContext structure
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
void * (*allocate)(CFIndex size, CFOptionFlags hint, void *info);
void * (*reallocate)(void *ptr, CFIndex newsize, CFOptionFlags hint, void *info);
void (*deallocate)(void *ptr, void *info);
CFIndex (*preferredSize)(CFIndex size, CFOptionFlags hint, void *info);
} CFAllocatorContext;
info 字段包含了为分配器专门定义的数据。例如,一个分配器可以使用info字段来追踪一个特殊的分配器。
(当前版本,不要将version字段设置成0以外的任何值)
如果在分配器上下文中有一些用户定义的数据,使用CFAllocatorGetContext函数来获得CFAllocatorContext结构体。然后计算或者处理需要的数据。下面提供了一些例子来说明
Listing 2 Getting the allocator context and user-defined data
static int numOutstandingAllocations(CFAllocatorRef alloc) {
CFAllocatorContext context;
context.version = 0;
CFAllocatorGetContext(alloc, &context);
return (*(int *)(context.info));
}
其他CF框架函数调用内存相关的回调来定义分配器的上下文并且拿到或者返回无类型的指针到块的内存中。
CFAllocatorAllocate, 分配块内存.
CFAllocatorReallocate 重分配块内存.
CFAllocatorDeallocate 释放块内存.
CFAllocatorGetPreferredSizeForSize 给出可能要被分配的块内存大小.
CF对象和cocoa对象的相互转换
oc与cf对象桥接转换
几个无法转换的类型:nsrunloop与cfrunloop;nsbundle与cfbundle;NSDateFormatter与CFDateFormatter无法免费桥接;
三种桥接方式及说明
__bridge transfers a pointer between Objective-C and Core Foundation with no transfer of ownership.
说明:
不带来所有权的转换,只转换对象类型
__bridge_retained or CFBridgingRetain casts an Objective-C pointer to a Core Foundation pointer and also transfers ownership to you.
You are responsible for calling CFRelease or a related function to relinquish ownership of the object.
说明:
除了将oc对象转换为cf对象同时转换对象管理的所有权,相当于cfretain操作,所以要负责release。
__bridge_transfer or CFBridgingRelease moves a non-Objective-C pointer to Objective-C and also transfers ownership to ARC.
ARC 负责对象的释放