手动内存管理
在 Xcode4.2 版本以后,自动引用计数 ARC 已经是默认有效了。但是这里还是先分析一下手动内存管理 MRC,方便我们对 iOS 开发的内存管理有更清晰的认识。
参考:《Objective-C 高级编程》干货三部曲(一):引用计数篇
写在前面:
- NSObject 已经开源,所以 alloc/retain/release/dealloc 的真实实现方案,也不用像书籍作者说的那样参考 GNUstep 源码去推测:
- 源码在线地址:NSObject.mm https://opensource.apple.com/source/objc4/objc4-756.2/runtime/NSObject.mm.auto.html
- 下载地址:objt4 源码 https://opensource.apple.com/tarballs/objc4/
- 可通过下面两个方法查看汇编输出:
参考:iOS 获取汇编输出方法
- Xcode -> Product -> Perform Action -> Assemble "*.m"即可获得汇编输出。
- clang获取oc代码汇编输出。在文件目录下,执行命令行:clang -S -fobjc-arc 文件名.m -o output.s
- 或者查看文件转成 C++ 后的源码。在文件目录下执行命令行:
$ clang -rewrite-objc MyClass.m
然后在同一目录下会多出一个 MyClass.cpp 文件,双击打开即可。
内存管理的思考方式
内存管理更加客观、正确的思考方式是:
- 自己生成的对象,自己持有。
- 非自己生成的对象,自己也能持有。
- 不再需要自己持有的对象时释放。
- 非自己持有的对象,无法释放。
iOS 内存管理经常用到的词有:生成、持有、释放、销毁。如下:
对象操作 | Objective-C 方法 | 引用计数变化 |
---|---|---|
生成并持有对象 | alloc/new/copy/mutableCopy 等方法 | +1 |
持有对象 | retain方法 | +1 |
释放对象 | release方法 | -1 |
废气对象 | dealloc方法 | 无 |
借用书中图,直观感受如下:
下面分条列举说明。
自己生成的对象,自己所持有
使用以下名称开头的方法名,意味着自己生成的方法,只有自己持有:
- alloc
- new
- copy
- mutableCopy
根据上述 使用以下名称开头的方法名,下列名称也意味着自己生成并持有对象:
- allocMyObject
- newTheObject
- copyThis
- mutableCopyYourObject
但是对于以下名称,即使使用了 alloc/new/copy/mutableCopy 开头,也并不属于同一类别的方法:
- allocate
- newer
- copying
- mutableCopyyed
这里用 驼峰(CameCase
) 命名法来区分。
非自己生成的对象,自己也能持有
在 alloc/new/copy/mutableCopy 方法以外取得的对象,默认并不持有对象,可以通过 retain
来持有对象:
/// 取得非自己生成并持有的对象。此时,obj 取得对象的存在,但自己并不持有
id obj = [NSMutableArray array];
/// 自己持有对象
[obj retain];
不再需要自己持有的对象时释放
自己持有的对象,一旦不需要,持有者有义务释放该对象。释放使用 release
方法。
/// 自己生成并持有对象
id obj = [[NSObject alloc] init];
/// 释放对象。指向该对象的指针仍然保留在 obj 中,貌似可以访问。
/// 但对象一经释放,决不可访问,否则会发生崩溃。
[obj release];
通过 retain
持有非自己生成的对象时,也需要使用 release
释放:
/// 生成
id obj = [NSMutableArray array];
/// 持有
[obj retain];
/// 释放
[obj release];
生成并持有对象,方法实现如下,注意 allocObject
符合前面生成并持有对象的命名规范:
-(id)allocObject {
/// 生成并持有对象
id obj = [[NSObject alloc] init];
/// 让自己持有对象
return obj;
}
/// 此时,不用调用 retain。obj1 已经持有该对象
id obj1 = [obj0 allocObject];
生成对象,默认不持有,需要手动持有,实现如下:
-(id)object {
/// 生成并持有对象
id obj = [[NSObject alloc] init];
/// 取得对象的存在。但是放弃对对象的持有。即加入自动释放池。
[obj autorelease];
/// 此时,obj 并不能持有该对象了
return obj;
}
/// 获取对象,并不持有
id obj1 = [obj0 object];
/// 持有对象。即引用计数 +1
[obj1 retain];
使用 autorelease
方法,可以使取得对象的存在,但是自己不持有对象。 autorelease
提供这样的功能:使对象在超出指定的生存范围时能够自动并正确的释放(调用 release 方法)。如下图所示:
无法释放非自己持有的对象
对于用 alloc/new/copy/mutableCopy 生成并持有的对象,或者是 retain 持有的对象,由于持有者是自己,所以在不需要该对象时自己需要将其释放。而由此之外得到的对象绝对不能释放,若在程序中释放了非自己持有的对象,会引发崩溃。例如过度释放对象:
/// 自己生成并持有对象
id obj = [[NSObject alloc] init];
/// 释放对象
[obj release];
/// 对象已经释放后再次释放
[obj release];
/// 此时,程序崩溃。
/// 崩溃分析:对象已废弃,访问废弃对象时崩溃。野指针
或者在 取得的对象已存在,但自己不持有该对象 时释放,也会引发崩溃:
id obj = [NSMutableArray array];
[obj release];
///释放非自己持有的对象,崩溃
alloc/retain/release/dealloc 实现
NSObject 源码并没有公开(已开源),这里参考开源的 GNUstep 源码来推测 NSObject 内部的实现细节。主要是要从实现的角度来理解内存管理的方式。为了明确重点,部分引用做了修改。
alloc 方法
id obj = [NSObject alloc];
调用该方法,源码实现如下:
/// GNUstep/modules/core/base/Source/NSObject.m alloc
+(id) alloc {
return [self allocWithZone: NSDefaultMallocZone()];
}
+(id)allocWithZone: (NSZone *)z {
return NSAllocateObject(self, 0, z);
}
通过 allocWithZone 类方法调用 NSAllocateObject 函数分配了对象,下面看下 NSAllocateObject 函数:
/// GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject
struct obj_layout {
NSUInteger retained;
};
inline id
NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone) {
int size = 计算容纳对象所需要的内存大小
id new = NSZoneMalloc(zone, size);
memset(new, 0, size);
new = (id) & ((struct obj_layout *) new)[1];
}
NSAllocateObject 函数通过调用 NSZoneMalloc 函数来分配存放对象所需要的内存空间。之后将该空间置为 0,最后返回作为对象而使用的指针。
NSZone 是为防止内存碎片化而引入的数据结构。目前该接口效率低,且使源代码更加复杂。
下面是去掉 NSZone
之后简化的源代码:
struct obj_layout {
NSUInteger retained;
};
+(id) alloc {
int size = sizeof(struct obj_layout) + 对象大小;
struct obj_layout *p = (struct obj_layout *)calloc(1, size);
return (id)(p + 1);
}
alloc 类方法使用 struct obj_layout 中的 retained 整数来保存引用计数,并将其写入对象内存头部,该对象内存块全部置为 0 后返回。下图为 GNUstep 实现 alloc 类方法返回对象示意图:
对象的引用计数可以通过 retainCount 实例方法获取。源码实现如下:
/// GNUstep/modules/core/base/Source/NSObject.m retainCount
-(NSUInteger) retainCount {
return NSExtraRefCount(self) + 1;
}
inline NSUInteger
NSExtraRefCount(id anObject) {
return ((struct obj_layout *)anObject)[-1].retained;
}
可以看到,给NSExtraRefCount传入anObject以后,通过访问对象内存头部的.retained变量,来获取引用计数。
retain 方法
/// GNUstep/modules/core/base/Source/NSObject.m retain
-(id) retain {
NSIncrementExtraRefCount(self);
return self;
}
inline void
NSIncrementExtraRefCount(id anObject) {
if (((struct obj_layout *)anObject)[-1].retained == UINT_MAX - 1)
[NSException raise: NSInternalInconsistencyException format: @"NSIncrementExtraRefCount() asked to increment too far"];
((struct obj_layout *)anObject)[-1].retained++;
}
可以看出,如果已有的引用计数过大,会执行异常代码。正常情况下,只运行了使 retained 变量加 1 的 retained++ 代码。
release 方法
/// GNUstep/modules/core/base/Source/NSObject.m release
-(void) release {
if (NSDecrementExtraRefCountWasZero(self))
[self dealloc];
}
BOOL
NSDecrementExtraRefCountWasZero(id anObject) {
if (((struct obj_layout *)anObject)[-1].retained == 0) {
return YES;
} else {
((struct obj_layout*)anObject)[-1].retained--;
return NO;
}
}
当 retained 变量大于 0 时减 1,等于 0 时调用 dealloc 实例方法,废弃对象。
dealloc 方法
/// GNUstep/modules/core/base/Source/NSObject.m dealloc
-(void) dealloc {
NSDeallocateObject(self);
}
inline void
NSDeallocateObject(id anObject) {
struct obj_layout *o = &((struct obj_layout *)anObject)[-1];
free(o);
}
上述代码废弃由 alloc 分配的内存块。
以上便是 GNUstep 中 alloc/retain/release/dealloc 的实现,具体总结如下:
- 在 Objective-C 的对象中存有引用计数这一整数值。
- 调用 alloc 或 retain 方法后,引用计数 +1。
- 调用 release 方法后,引用计数 -1。
- 引用计数为 0 后调用 dealloc 方法废弃对象。
好了,下面看一下苹果的实现吧。
苹果的实现
因为 NSObject 源码并没有公开(已开源),这里利用 Xcode 的调试器 lldb 和 iOS 大概追溯其实现过程。在 NSObject 类的 alloc 类方法上设置断点,追踪程序的执行。以下列出执行调用的方法和函数:
-
alloc 方法
- alloc
- allocWithZone
- class_createInstance
- calloc
-
retainCount 方法
- __CFDoExternRefOperation
- CFBasicHashGetCountOfKey
-
retain 方法
- __CFDoExternRefOperation
- CFBasicHashAddValue
-
release 方法
- __CFDoExternRefOperation
- CFBasicHashRemoveValue
CFBasicHashRemoveValue 返回 0 时,-release 调用 dealloc
上面频繁出现的 __CFDoExternRefOperation 是开源代码 CFRuntime.c 的 __CFDoExternRefOperation 函数,
源码如下:
CF_EXPORT uintptr_t __CFDoExternRefOperation(uintptr_t op, id obj) {
if (nil == obj) HALT;
uintptr_t idx = EXTERN_TABLE_IDX(obj);
uintptr_t disguised = DISGUISE(obj);
CFSpinLock_t *lock = &__NSRetainCounters[idx].lock;
CFBasicHashRef table = __NSRetainCounters[idx].table;
uintptr_t count;
switch (op) {
case 300: // increment
case 350: // increment, no event
__CFSpinLock(lock);
CFBasicHashAddValue(table, disguised, disguised);
__CFSpinUnlock(lock);
if (__CFOASafe && op != 350) __CFRecordAllocationEvent(__kCFObjectRetainedEvent, obj, 0, 0, NULL);
return (uintptr_t)obj;
case 400: // decrement
if (__CFOASafe) __CFRecordAllocationEvent(__kCFObjectReleasedEvent, obj, 0, 0, NULL);
case 450: // decrement, no event
__CFSpinLock(lock);
count = (uintptr_t)CFBasicHashRemoveValue(table, disguised);
__CFSpinUnlock(lock);
return 0 == count;
case 500:
__CFSpinLock(lock);
count = (uintptr_t)CFBasicHashGetCountOfKey(table, disguised);
__CFSpinUnlock(lock);
return count;
}
return 0;
}
下面是其简化后的代码实现:
/// CF/CFRuntime.c __CFDoExternRefOperation
int ____CFDoExternRefOperation(uintptr_t op, id obj) {
CFBasicHashRef table = 取得对象的散列表(obj);
int count;
switch(op) {
case Operation_retainCount:
count = CFBasicHashGetCountOfKey(table, obj);
return count;
case Operation_retain:
count = CFBasicHashAddValue(table, obj);
return count;
case Operation_release:
count = CFBadicHashRemoveValue(table, obj);
return 0 == count;
}
}
__CFDoExternRefOperation 函数按照 retainCount/retain/release 操作进行分发,调用不同的函数。猜测, NSObject 类的 retainCount/retain/release 实例方法也许如下面代码表示:
-(NSUInteger) retainCount {
return (NSUInteter) __CFDoExternRefOperation(Operation_retainCount, self);
}
-(id)retain {
return (id)__CFDoExternRefOperation(Operation_retain, self);
}
-(void) release {
return __CFExternRefOperation(Operation_release, self);
}
从 __CFDoExternRefOperation 函数以及由此函数调用的各个函数名看出,苹果的实现大概是采用散列表(引用技数表)来管理引用计数的,如下图所示:
GNUstep 将引用计数保存到对象内存块头部的变量中,好处如下:
- 少量代码即可完成。
- 能够统一管理引用计数内存和对象内存块。
苹果很可能采用引用计数表管理引用计数,这样的好处是:
- 对象用内存块的分配无需考虑内存块头部。
- 引用计数表中存有各个对象的内存块地址,可通过改地址追溯到对象的内存块。
尤其是第二,在调试时有着举足轻重的作用,即使出现故障导致对象占用的内存损坏,只要引用计数表没坏,就能够确认各内存块地址。
NSObject.mm 源码实现
alloc
/// Objc源码/objc4-756.2/runtime/NSObject.mm
+ (id)alloc {
return _objc_rootAlloc(self);
}
而 _objc_rootAlloc 实现为:
/// Objc源码/objc4-756.2/runtime/NSObject.mm
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
callAlloc 方法开始实现具体细节:
/// Objc源码/objc4-756.2/runtime/NSObject.mm
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
其实可以看到,和上面的猜测大致是一样的。这里也不再具体扩展,需要时可以从源码层面看下 release、dealloc 等的具体实现。
autorelease
autorelease 介绍
当 autorelease 管理的对象超出其作用域后,对象实例的 release 方法会被调用。autorelease 的具体使用方法如下:
- 生成并持有 NSAutoreleasePool 对象。
- 调用已分配对象的 autorelease 方法。
- 废弃 NSAutoreleasePool 对象。
对所有调用过 autorelease 实例方法的对象,在废弃 NSAutoreleasePool 时,都将主动调用 release 方法:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; /// 等同于 [obj release];
程序并非一定要使用 NSAutoreleasePool 对象来工作。但是在大量产生 autorelease 的对象时,只要不废弃 NSAutoreleasePool 对象,即 RunLoop 不进入睡眠状态,那么生成的对象就不能被释放,因此有时会产生内存不足的现象。如大量循环中对图片做复杂操作。这种情况下,就会产生大量的 autorelease 对象,内存激增:
for (int i = 0; i < 图片数; i++) {
/*
* 读入图像
* 大量产生 autorelease 对象
* 由于没有废弃 NSAutoreleasePool 对象,最终导致内存不足。
*/
}
再次情况下,有必要在适当的地方生成、持有、废弃 NSAutoreleasePool 对象:
for (int i = 0; i < imageArray.count; i++) {
// 临时 Pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
UIImage *image = imageArray[i];
[image doSomething];
[pool drain];
}
可能会出的面试题:什么时候会创建自动释放池?
答:运行循环检测到事件并启动后,就会创建自动释放池,而且子线程的 runloop 默认是不工作的,无法主动创建,必须手动创建。
举个例子:
自定义的 NSOperation 类中的 main 方法里就必须添加自动释放池。否则在出了作用域以后,自动释放对象会因为没有自动释放池去处理自己而造成内存泄露。
GNUstep autorelease 实现
同上,先看一下 GNUstep 的源码实现:
/// GNUstep/modules/core/base/Source/NSObject.m autorelease
-(id) autorelease {
[NSAutoreleasePool addObject: self];
}
autorelease 方法的本质就是调用 NSAutoreleasePool 对象的 addObject 类方法。下面是作者假想后简化的 NSAutoreleasePool 源代码实现:
/// GNUstep/modules/core/base/Source/NSAutoreleasePool.m addObject
+(void) addObjecty: (id)anObj {
NSAutoreleasePool *pool = 取得正在使用的 NSAutoreleasePool 对象;
if (pool != nil) {
[pool addObject: anObj];
} else {
NSLog()
}
}
-(void) addObject: (id)anObj {
[array addObject: anObj];
}
addObject 类方法就是调用正在使用的 NSAutoreleasePool 对象的 addObject 实例方法,然后这个对象就被追加到正在使用的 NSAutoreleasePool 对象的数组中。
下面看一下使用 drain 实例方法废弃正在使用的 NSAutoreleasePool 对象的过程:
/// GNUstep/modules/core/base/Source/NSAutoreleasePool.m drain
-(void) drain {
[self dealloc];
}
-(void) dealloc {
[self emptyPool];
[array release];
}
-(void) emptyPool {
for (id obj in array) {
[obj release];
}
}
虽然调用了好几个方法,可以确定对于数组中的所有对象都调用了 release 实例方法。
苹果的实现
可通过 objc 库的 https://opensource.apple.com/source/objc4/objc4-750.1/runtime/NSObject.mm.auto.html 来确认苹果中 autorelease 的实现。
/// /objc4/objc4-750.1/runtime/NSObject.mm AutoreleasePoolPage
/// 这里的源码非常长。。。感兴趣的可以自己去看下。
class AutoreleasePoolPage {
}
下面还是结合书中总结的来做分析吧。核心方法是一样的:
class AutoreleasePoolPage {
static inline voiod *push() {
// 相当于生成或者持有 NSAutoreleasePool 类对象;
}
static inline void *pop(void *token) {
// 相当于废弃 NSAutoreleasePool 类对象;
releaseAll();
}
static inline id autorelease(id obj) {
/*
* 相当于 NSAutoreleasePool 类的 addObject 类方法;
* AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 实例;
* page -> add(obj);
*/
}
id *add(id obj) {
// 将对象追加到内部数组中;
}
void releaseAll() {
// 调用内部数组中对象的 release 方法
}
};
/// 进栈
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage :: push();
}
/// 出栈
void *objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage :: pop(ctxt)
}
/// 在内部释放
id *objc_autorelease(id obj) {
return AutoreleasePoolPage :: autorelease(obj);
}
下面通过外部调用来对比分析:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// 等同于 objc_autoreleasePoolPush()
id obj = [[NSObject alloc] init];
[obj autorelease];
// 等同于 objc_autorelease(obj)
[NSAutoreleasePool showPools];
/// 非公开类方法。showPools 会将现在 NSAutoreleasePool 的状况输出到控制台
[pool drain];
// 等同于 objc_autoreleasePoolPop(pool)
可能出的面试题: 苹果如何实现 NSAutoreleasePool 的? 参考答案: NSAutorelease 以一个队列数组的形式实现,主要使用三个方法:objc_autoreleasePoolPush(进栈)、objc_autoreleasePoolPop(出栈)、objc_autorelease(释放内部)。
另外,如果 autorelease NSAutoreleasePool 对象,回引发崩溃。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool autorelease];
因为对于 NSAutoreleasePool 来说,autorelease 已被重载。
ARC 自动引用计数
内存管理的思考方式
引用计数式内存管理,就是思考 ARC 所引起的变化。
- 自己生成的对象,自己所持有。
- 非自己生成的对象,自己也能持有。
- 自己持有的对象不再需要时,释放。
- 非自己持有的对象无法释放。
可以看到,思想和 MRC 是一样的,区别主要是我们不需要在显式的调用,在源代码的记述方法上有所不同。
四种所有权修饰符
ARC 有效时,id 类型或者对象类型必须附加所有权修饰符。所有权修饰符一共有四种,如下:
- __strong:is the default. An object remains “alive” as long as there is a strong pointer to it.
- __weak:specifies a reference that does not keep the referenced object alive. A weak reference is set to nil when there are no strong references to the object.
- __unsafe_unretained:specifies a reference that does not keep the referenced object alive and is not set to nil when there are no strong references to the object. If the object it references is deallocated, the pointer is left dangling.
- __autoreleasing:is used to denote arguments that are passed by reference (id *) and are autoreleased on return.
下面挨个看一下吧。
__strong 修饰符
ARC 环境下,__strong 是属性的默认修饰符。
__strong 使用方法
id obj = [[NSObject alloc] init];
等同于:
id __strong obj = [[NSObject alloc] init];
__strong 修饰符表示对对象的“强引用”,该对象的持有状态如下:
{
// 自己生成并持有对象。因为强引用,所以持有。
id __strong obj = [[NSObject alloc] init];
}
/// obj 超出作用域,强引用失效。所以自动释放持有的对象。
对于非自己生成,并持有的对象,亦是如此:
{
// 取得非自己生成并持有的对象
id __strong obj = [NSMutableArray array];
}
/// 超出作用域,强引用失效
附有 __strong 修饰符的变量之间也可以相互赋值:
// 生成对象A。obj0 持有对象 A 的强引用
id __strong obj0 = [[NSObject alloc] init];
// 生成对象B。obj1 持有对象 B 的强引用
id __strong obj1 = [[NSObject alloc] init];
// obj2 不持有任何对象
id __strong obj2 = nil;
// obj0 持有对象 B 的强引用。此时对象 A 因为不再被强引用,被废弃。
// 此时对象 B 被变量 obj0 和 obj1 共同持有。
obj0 = obj1;
// obj2 持有对象 B 的强引用。
// 此时对象 B 被变量 obj0、obj1、obj2 持有。
obj2 = obj0;
// obj1 不再强引用对象 B
obj1 = nil;
// obj0 不再强引用对象 B
obj0 = nil;
// obj2 不再强引用对象 B
obj2 = nil;
// 此时,对象 B 不再被任何变量强引用,被废弃。
也可以给类的成员变量或方法属性加上 __strong 修饰符:
@interface Test: NSObject {
id __strong obj_;
}
-(void)setObject: (id __strong)obj;
因为 id 类型和对象类型的所有权修饰符默认为 __strong 修饰符,所以通常不需要写上 __strong。
__strong 实现
{
id __strong obj = [[NSObject alloc] init];
}
通过 clang 获取程序汇编输出,或者 cpp 文件,结合 objc4 库源码,可以分析程序执行的流程。
其实这部分都是模拟代码,通过总结分析得出的结论。深入解构objc_msgSend函数的实现 这篇文章作者通过把汇编语言转换成 C 语言来具体分析 Objective-C 的消息转发机制,分析的很深入。
/// 编译器模拟代码
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
作为对比,看一下转化 C++ 后的执行:
id obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
可以看出,即使 ARC 不支持 release,实际上编辑器还是自动插入了 release。通过 objc_msgSend 来传递消息。
而使用 alloc/new/copy/mutableCopy 以外方法生成的对象,又有一些不一样:
{
id __strong obj = [NSMutableArray array];
}
编译器的模拟代码:
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleaseReturnValue(obj);
objc_release(obj)
objc_retainAutoreleaseReturnValue 函数主要用于程序最优化执行。该函数持有的对象应该是:注册到 autoreleasePool 中对象的方法,或者函数的返回值。
这种 objc_retainAutoreleaseReturnValue 函数是成对的,与之相对的函数是: objc_autoreleaseReturnValue。来看一下它的使用:
+(id)array {
return [[NSMutableArray alloc] init];
}
编译器的模拟代码:
+(id)array {
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
objc_autoreleaseReturnValue: 返回注册到 autoreleasePool 的对象。
书中说,objc_retainAutoreleaseReturnValue 和 objc_autoreleaseReturnValue 方法配合使用可以不将对象注册到 autoreleasePool中,如下图:
__weak 修饰符
当带有 __strong 修饰符的变量在持有对象时,如果多个对象相互持有,很容易发生循环引用。
循环引用容易发生内存泄漏。所谓内存泄漏就是应当被废弃的对象在超出其生存周期后继续存在。
__weak 用法
@interface Test: NSObject {
id __strong obj_;
}
-(void)setObject:(id __strong)obj;
@end
@implementation Test
-(id)init {
self = [super init];
return self;
}
-(void)setObject:(id __strong)obj {
obj_ = obj;
}
以下为循环引用:
{
id test0 = [[Test alloc] init]; // test0 强引用对象 A
id test1 = [[Test alloc] init]; // test1 强引用对象 B
[test0 setObject: test1]; // test0 强引用对象 B
[test1 setObject: test0]; // test1 强引用对象 A
}
或者,对象持有自身时,也会发生循环引用:
{
id test = [[Test alloc] init];
[test setObject: test];
}
使用 __weak 修饰符可以避免循环使用。通过检查附有 __weak 修饰符的变量是否为 nil,可以判断被赋值的对象是否已经被废弃。
__weak 的另一个优点就是,在持有某对象的弱引用时,若该对象被废弃,则此弱引用自动失效且被置为 nil 状态。
通过下面代码看下 __weak 的特性:
id __weak obj = [[NSObject alloc] init];
改代码会出现编译警告,因为 __weak 并不直接持有对象,所以 obj 会被立即释放掉。修改如下即可:
id __strong obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
所以,针对上面循环引用的问题,作出修改,即可避免循环引用了:
@interface Test: NSObject {
id __weak obj_;
}
-(void)setObject:(id __strong)obj;
@end
__weak 实现
通过前面说明,可以看到 __weak 有如下魔法般的效果:
- 若附有 __weak 修饰符的变量所引用的对象被废弃,则该变量被置为 nil。
- 若使用附有 __weak 修饰符的的变量,即是使用注册到 autoreleasePool 中的对象。
首先,通过研究 __weak 内部实现,再来看下上面提出的的一个问题:
id __weak obj = [[NSObject alloc] init];
编译器处理该源码时,模拟如下:
/// 编译器的模拟代码
id obj;
id tmp = objc_msgSend(NSObjct, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_initWeak(&obj, tmp);
objc_release(tmp);
objc_destroyWeak(&obj);
假如 [[NSObject alloc] init]
生成了对象 A,因为 __weak 不能持有对象,编译器认为对象 A 没有持有者,就通过 objc_release(tmp);
函数释放和废弃对象 A,所以赋值失败,编译警告。
下面,再来通过合理使用 __weak 分析其内部的实现:
/// 假设 obj 附加了 __strong 修饰符且对象被赋值
{
id __weak obj1 = obj;
}
该源代码可转换为如下形式:
/// 编译器的模拟代码
id obj1;
objc_initWeak(&obj1, obj);
id temp = objc_loadWeakRetained(&obj1);
objc_autorelease(temp);
objc_destroyWeak(&obj1);
- objc_loadWeakRetaine 函数取出附有 __weak 修饰符的变量所引用的对象,并 retain。
- objc_autorelease 函数将对象注册到 autoreleasePool 中。
大量的使用 __weak 修饰符修饰的变量,注册到 autoreleasepool 的对象也会大量增加。因此使用附有 __weak 修饰符的变量时,最好先暂时赋值给附有 __strong 修饰符的变量后在使用。如下:
{
id __weak 0 = obj;
NSLog(@"1 %@", o); // o 注册到 autoreleasepool 1 次
NSLog(@"2 %@", o); // o 注册到 autoreleasepool 2 次
NSLog(@"3 %@", o); // o 注册到 autoreleasepool 3 次
NSLog(@"4 %@", o); // o 注册到 autoreleasepool 4 次
}
即每次使用,都会注册到 autoreleasepool 中。如果先赋值给 __strong 变量:
{
id __weak 0 = obj;
id __strong tmp = o; // o 注册到 autoreleasepool 1 次
NSLog(@"1 %@", tmp);
NSLog(@"2 %@", tmp);
NSLog(@"3 %@", tmp);
NSLog(@"4 %@", tmp);
}
书中原话是: 在 “tmp = o;” 时对象仅登录到 autoreleasepool 中 1 次。
这里再看下 Swift 中常用的写法,大概也是这个原因:
loginVC.loginSuccess = { [weak self] phoneNum in
guard let strongSelf = self else {
return
}
}
这里重点说两个方法:
- objc_initWeak(&obj1, obj): 初始化附有 __weak 修饰符的变量,具体通过执行
objc_storeWeak(&obj1, obj)
,将附有 __weak 修饰符的变量地址注册到 weak 表中。 - objc_destroyWeak(&obj1):释放一个 __weak 变量。具体通过执行
objc_storeWeak(&obj1, 0)
,把变量的地址从 weak 表中删除。
简单来说,就是 objc_storeWeak(&obj1, obj)
通过第二个参数决定是注册变量地址到 weak 表,还是删除地址。
通常面试到 weak 问题时,都会问下 weak 的实现原理,主要问的就是对 weak 表的理解。
weak 表与引用计数表相同,作为散列表被实现。如果使用 weak 表,将废弃对象的地址作为键值进行检索,就能高速获取对应的附有 __weak 修饰符的变量的地址。另外,由于一个对象可以赋值给多个附有 __weak 修饰符的变量,所以一个键值,可注册多个变量的地址。
当对象被释放时,执行流程是:
- objc_release
- 因为引用计数为 0,所以执行 dealloc
- _objc_rootDealloc
- object_dispose
- objct_destructInstance
- objc_clear_deallocating
- 从 weak 表中获取废弃对象的地址为键值的记录。
- 将包含在记录中所有附有 __weak 修饰符变量的地址,赋值为 nil。
- 从 weak 表中删除该记录。
- 从引用计数表众删除被废弃对象的地址为键值的记录。
由上可知,大量使用 __weak 修饰符的变量,会消耗相应的 CPU 资源。我们只在需要避免循环吗引用时使用 __weak 修饰符即可。
另外,实际上存在着不支持 __weak 修饰符的类。但是这种类极为罕见。如 NSMachPort
、allowsWeakReference
、retainWeakReference
等。知道就好了。
_unsafe_unretained 修饰符
在 iOS4 以及 OSX Snow Leopard 的应用程序中代替 __weak 修饰符的。附有 _unsafe_unretained 修饰符的变量不属于编译器的内存管理对象。
这里有了解即可,不在细说。
__autoreleasing 修饰符
__autoreleasing 使用方法
在 ARC 有效时,用 @autoreleasepool 块代替 MRC 下 NSAutoreleasePool 类,用附有 __autoreleasing 修饰符的变量代替 MRC 下 autorelease 方法,即对象被注册到 autoreleasepool。
通常,我们并不需要显式的调用 __autoreleasing 修饰符。
在访问附有 __autoreleasing 修饰符的变量时,实际上必定要访问注册到 autoreleasepool 的对象。
id obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
NSLog(@"class = %@", [obj1 class]); // NSObject
以下源代码与其相同:
id obj0 = [[NSObject alloc] init];
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class = %@", [tmp class]); // NSObject
为什么在访问附有 __autoreleasing 修饰符的变量时,必须要访问注册到 autoreleasepool 的对象呢?这是因为 __weak 修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象有可能被废弃,如果把要访问的对象注册到 autoreleasepool 中,那么在 @autoreleasepool 块结束之前都能确保该对象存在。因此,使用 __weak 修饰符的变量就要访问注册到 autoreleasepool 中的对象。
上面这段话是书中原话,但是结合上文 __weak 的实现原理,感觉这么描述不是很对。应该说,附有 __weak 修饰符的变量持有的对象,如果该对象不在 autoreleasepool 中,则编译器会将该对象注册到 autoreleasepool 中,并提供给该变量使用。这个只是个人理解,作为参考。
__autoreleasing 内部实现
将对象赋值给附有 __autoreleasing 修饰符的变量,等同于 ARC 无效时调用对象的 autorelease 方法。
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
该源码主要将 NSObject 类对象注册到 autoreleasepool 中,可作如下转换:
/// 编译器的模拟代码
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
这里能够看到 pool 入栈、执行autorelease、出栈 三个方法。在 MRC 下有过详细说明。
在 alloc/new/copy/mutableCopy 方法群之外的方法中使用注册到 autoreleasepool 中的对象,会有一些区别:
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}
编译器模拟代码如下:
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleaseReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
注册到 autoreleasepool 的方法 objc_autorelease 并没有变。
引用计数
参考:iOS ARC下获取引用计数(retain count)
在 ARC 有效时,是无法正常查看一个类当前的引用计数的。不过,可以通过下面三个方法来获取到:
- 使用 KVC
- 使用私有 API
- 使用CFGetRetainCount
实践如下:
id obj = [[NSObject alloc] init];
id __weak obj1 = obj;
// KVC
NSLog(@"count10 = %@",[obj valueForKey:@"retainCount"]); // 1
NSLog(@"count11 = %@",[obj1 valueForKey:@"retainCount"]); // 2
NSLog(@"address0 = %p", obj); // 0x600001757be0
NSLog(@"address1 = %p", obj1); // 0x600001757be0
// 私有API
OBJC_EXTERN int _objc_rootRetainCount(id);
NSLog(@"count20 = %d",_objc_rootRetainCount(obj)); // 1
NSLog(@"count21 = %d",_objc_rootRetainCount(obj1)); // 2
// 使用CFGetRetainCount
NSLog(@"count30 = %zd", CFGetRetainCount((__bridge CFTypeRef)(obj))); // 1
NSLog(@"count30 = %zd", CFGetRetainCount((__bridge CFTypeRef)(obj1))); // 2
@autoreleasepool {
id anObj = [[NSObject alloc] init];
id __autoreleasing o = anObj;
NSLog(@"count40 = %@",[anObj valueForKey:@"retainCount"]); // 2
NSLog(@"count40 = %@",[o valueForKey:@"retainCount"]); // 2
}
- 由于弱引用不持有对象,附有 __weak 修饰符的变量不会对原对象的引用计数产生影响。
- __autoreleasing 持有对象。对引用计数产生影响。
为什么 weak 变量指向对象的引用计数改变了,其实我不是很确定,虽然对象地址一样,但是可能是 weak 表导致的。这里不是特别清楚。
ARC 规则
在 ARC 有效的情况下编译源代码,必须遵守一定的规则:
- 不能使用 retain/release/retainCount/autorelease
- 不能使用 NSAllocateObject/NSDeallocateObjct
- 必须遵循内存管理的方法命名规则
- 不要显式调用 dealloc
- 使用 @autoreleasepool 代替 NSAutoreleasePool
- 不能使用区域 NSZone
- 对象型变量不能作为 C 语言结构体(struct/union)的成员
- 显式转换 id 和 void*
1. 不能使用 retain/release/retainCount/autorelease
ARC 有效时,禁止使用 retain/release/retainCount/autorelease。否则会编译报错。
2. 不能使用 NSAllocateObject/NSDeallocateObjct
ARC 有效时,使用 NSAllocateObject/NSDeallocateObjct 会编译报错。
3. 必须遵循内存管理的方法命名规则
对象的生成/持有的方法必须遵循以下命名规则:
- alloc
- new
- copy
- mutableCopy
- init
前四种方法和 MRC 下一样。而关于init方法的要求则更为严格:
- 必须是实例方法
- 必须返回对象
- 返回对象的类型必须是id类型或方法声明类的对象类型
4. 不要显式调用 dealloc
对象被废弃时,不论 ARC 是否有效,都会调用对象的 dealloc 方法。
ARC 无效时:
-(void)dealloc {
[super dealloc];
}
ARC 有效时,dealloc 无法显式调用,否则编译报错。ARC 会自动处理这个方法,因此也不比书写 [super dealloc]
。dealloc 中只需书写废弃对象时所要做的操作即可:
-(void) {
// 处理
}
5. 使用 @autoreleasepool 代替 NSAutoreleasePool
ARC 有效时,使用 NSAutoreleasePool 会编译报错。
6. 不能使用区域 NSZone
不管 ARC 是否有效,区域 NSZone 在现在的运行时系统(编译器宏 OBJC2 被设定的环境)中已单纯地被忽略。并且 ARC 有效时,使用 NSZone 会编译报错。
7. 对象型变量不能作为 C 语言结构体(struct/union)的成员
C语言的结构体如果存在Objective-C对象型变量,便会引起错误,因为C语言在规约上没有方法来管理结构体成员的生存周期。
备注:
这里存在疑问,书中说:
struct Data {
NSMutableArray *array;
}
会存在编译错误,但是我测试时,并没有编译错误。不只是版本升级后修改了,还是我理解有问题。
8. 显式转换 id 和 void*
非ARC下,这两个类型是可以直接赋值的:
id obj = [NSObject alloc] init];
void *p = obj;
id o = p;
但是在ARC下就会引起编译错误。为了避免错误,我们需要通过__bridege来转换。
id obj = [[NSObject alloc] init];
void *p = (__bridge void*)obj;//显式转换
id o = (__bridge id)p;//显式转换
书中用了大量的篇幅介绍桥接,这里暂时不做扩展了。
属性
属性的声明和所有权修饰符的对应关系:
属性关键字 | 所有权修饰符 |
---|---|
assign | __unsafe_unretained 修饰符 |
copy | __strong 修饰符(但是赋值的是被复制的对象) |
retain | __strong 修饰符 |
strong | __strong 修饰符 |
unsafe_unretained | __unsafe_unretained 修饰符 |
weak | __weak 修饰符 |
数组
__unsafe_unretained 修饰符以外的 __strong/__weak/__autoreleasing 修饰符保证其指定的变量初始化为 nil。
书中说了一些 C 语言相关的数组处理,不在细说。
后记
不论是作为面试知识,还是对 Objective-C 有更深入的了解,引用计数都值得我们深入学习下。
《Objective-C高级编程》三篇总结之一:引用计数篇
《Objective-C高级编程》三篇总结之二:Block篇
《Objective-C高级编程》三篇总结之三:GCD篇