Objective-C高级编程之内存管理篇

iOS的内存管理是采用引用计数的方式,引用计数分为手动引用计数和自动引用计数(ARC)。前者要求开发者手动管理内存,自己负责内存的申请与释放,后者是苹果推出的自动管理内存的方式,但其实质只是编译器帮助开发者做了内存管理的工作。理解引用计数的内存管理机制有助于我们写出更加内存安全的代码。

内存管理/引用计数

1. 引用计数的思考方式

引用计数的思考方式遵循以下四个原则:

  • 自己生成的对象,自己持有
    id obj = [NSObject alloc] init]
    alloc创建了一个对象(这里指内存),init对此对象做初始化操作,obj持有了这个对象。同样的,new/copy/mutablecopy也可用于生成并持有对象,除此四个关键字之外,以alloc/new/copy/mutablecopy开头的方法也可用于生成并持有对象。
  • 非自己生成的对象,自己也能持有
    id obj = [NSMutableArray array]
    obj取得了array对象,但此时并不持有它。(事实上,array方法返回的是一个autorelease对象)
    [obj retain]
    通过retain方法,obj便持有了array对象 。
  • 不再需要自己持有的对象时,释放
    持有的对象不再需要时,持有者应当负责释放它。
    id obj = [NSObject alloc] init]
    [obj release]
    通过调用release方法可释放obj持有的对象。
  • 非自己持有的对象,无法释放
    id obj = [NSObject alloc] init]
    [obj release]
    [obj release] //程序将崩溃
    当第二次调用release时,由于obj已不再持有对象,程序就会发生异常。

2. 引用计数的实现

  • alloc/retain/release/dealloc
    alloc方法和retain方法会使对象的引用计数值加1,release方法使对象的引用计数值减1,当对象的引用计数值为0时,调用dealloc方法释放对象。
    苹果是用散列表来管理引用计数的,键值为对象内存块地址,对应的值保存引用计数。图示如下
键值为内存块地址哈希值的引用计数表

使用散列表来管理引用计数的好处是
1)散列表中存有内存块地址,可通过表中引用计数追溯到出问题的内存块地址(这在调试是很有帮助的)
2)对象内存分配时无需再考虑引用计数所占用的内存

  • autorelease
    autorelease是自动释放对象的方法,它是通过NSAutoreleasePool来实现的。它在对象超出自身的作用域之后,调用release方法去释放对象,具体实现细节为:
    1)生成并持有NSAutoreleasePool对象
    2)调用autorelease方法,将对象添加到NSAutoreleasePool中
    3)待NSAutoreleasePool生命周期结束时,所有添加到自动释放池中的对象均会被发送release消息来释放自身
autorelease实现过程

代码表示如下:

NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool drain];

cocoa框架中,使用NSRunloop来管理NSAutoreleasePool的生成、持有和释放,runloop开始时会创建自动释放池,睡眠和退出时会销毁自动释放池,图解如下:


NSRunloop生成、持有,废弃NSAutoreleasePool对象

很多时候,我们并不需要主动使用NSAutoreleasePool来管理内存,但是某些时候如果产生了大量的autorelease对象,而NSAutoreleasePool没释放前,这些对象便依旧存于内存中,有可能会引发内存不足的情况,此时我们可以考虑创建NSAutoreleasePool来及时释放不需要的对象。当我们创建了多个自动释放池时,苹果又是怎么管理它们的呢?答案是栈!

NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool2 = [NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool3 = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool3 drain];
[pool2 drain];
[pool drain];

很显然,对象obj应该会加入到pool3中,因为pool3是当前正在使用的自动释放池。我们来看下苹果底层相关的方法

class AutoreleasePoolPage 
{
...
public:
    static inline id autorelease(id obj) {}  //将一个对象添加到pool中

    static inline void *push() {}  //将新创建的pool压入栈

    static inline void pop(void *token) {}  //将当前的pool出栈
...
  }

ARC

1. 所有权修饰符

ARC下由编译器帮助开发者自动加入内存管理代码,因此编译器必须知道对象何时在被持有,何时应该被释放,故苹果引入了4个所有权修饰符:
__strong, __week, __unsafed_unretained, __autoreleasing

  • __strong修饰符
    __strong表示对对象的强引用,是id类型和对象类型默认的所有权修饰符,以下两行代码实质是一样的
id obj = [NSObject alloc] init];
id __strong obj = [NSObject alloc] init];

带有__strong修饰符的变量在超出其作用域时,即变量被废弃时,其持有的对象也随之被释放,代码角度看类似这样

{
  id obj = [NSObject alloc] init];
  [obj release];
}

在超出大括号作用域后,obj被废弃,其持有的对象也因强引用的失效而被释放。

  • __week修饰符
    看起来__strong修饰符已经能完美解决内存管理的诸多问题,但事实上有一种情况是强引用无法解决的,即循环引用,而__week修饰符,这种弱引用方式,则可以完美解决循环引用的问题。
    我们先来看一下什么是循环引用。举个例子,比如当A持有了B的强引用,B也持有了A的强引用,由于A和B相互持有对象的强引用,导致A和B均无法被释放,这便是出现了循环引用。
对象相互强引用

有时对象引用了自身,也会发生循环引用的现象。

自身强引用

循环引用的后果是会发生内存泄漏(不再被需要的应该废弃的对象却无法被释放),那么__week是如何解决的呢?带有__week修饰符的变量无法持有对象的实例,换句话说,强引用会使对象的引用计数增加,而弱引用不会。
id __week obj = [NSObject alloc] init];
上述代码编译器会产生警告,原因是对象被生成后,由于obj持有其弱引用,导致对象立即被释放。带有__week修饰符的变量在对象被释放后自动变成了nil,故上述代码最终得到的obj是nil,将代码改为如下即可消除警告。

id __strong obj0 = [NSObject alloc] init];
id __week obj = obj0;

obj0持有对象的强引用,所以对象不会被释放,obj可以正确使用对象。当obj0超出了作用域,强引用失效,对象被释放,此时obj自动变为nil。如此便很容易明白,当循环引用的两个对象相互持有对方的弱引用时(或者其中一个持有的是弱引用),并不会影响到对象的释放,也就不再会发生内存泄露了。
和引用计数表类似,苹果对于week变量的管理也是通过散列表来实现的。将赋值对象的地址作为键值,由于同一对象可能被多个week变量弱引用,故同一键值可能对应一组week变量。由于__week修饰的变量会占用一定的CPU资源,因此除了解决循环引用的问题,尽量避免过多的使用week变量。

  • \ __unsafe_unretained修饰符
    __unsafe_unretained修饰符修饰的变量既不持有对象的强引用,也不持有对象的弱引用,它和__week修饰符一样,实际上是获得了一个指向对象的指针,而和__week不同的是,当对象被释放后,__week修饰的变量自动变为nil,__unsafe_unretained修饰的变量则成为了野指针!带有__unsafe_unretained的变量不在编译器内存管理范围内,编译器是不对它做管理的,使用时应当谨慎确保其所指向的对象仍存在并未被释放。
  • __autoreleasing修饰符
    ARC下的__autoreleasing相当于调用对象的autorelease方法,并使用@autoreleasepool来替代非ARC下的NSAutoreleasePool功能。被__autoreleasing修饰的变量所持有的对象会被加入到自动释放池中。
ARC与非ARC下代码对比

事实上,像__strong修饰符一样,大多数时候,我们并不需要显示对一个变量指定__autoreleasing修饰符。比如在取得非自己生成的对象引用时(使用除alloc/new/copy/mutablecopy以外的方法取得对象),变量被__week修饰符修饰时,取得id或对象类型的指针时(例如 id **obj),对象均会被自动注册到自动释放池中。

2. ARC规则

ARC有如下8个规则:

  • 不可使用retain/release/retainCount/dealloc方法
    由于ARC下编译器会自动在合适的位置帮助开发者插入内存管理的代码,因此不允许开发者再主动调用内存管理相关方法。
  • 不可使用NSAllocateObject/NSDeallocateObject方法
    事实上,alloc方法会调用NSAllocateObject来创建对象,并保存其引用计数,单独调用NSAllocateObject方法会对内存管理造成混乱,因此禁止使用该方法自然也是合理的。
  • 不可显示调用dealloc方法
    这里指的是开发者无需在dealloc方法中显示调用[super dealloc],只需要做在释放对象时一些必要的处理,比如移除之前注册的某些观察者。
  • 使用@autoreleasepool块代替NSAutoreleasePool
    这个是显而易见的,ARC下禁止使用NSAutoreleasePool,而采用@autoreleasepool块代替。
  • 不能使用NSZone
    事实上,无论是手动管理内存还是使用ARC,NSZone(区域)在现在的运行时系统中都是被忽略的。
  • 对象型变量不可作为C语言结构体成员
    在C语言的规约下,结构体成员的生命周期是无法管理的,而ARC下编译器必须能够正确的管理OC对象的生命周期,这显然是矛盾的,故对象型变量不可作为C语言结构体的成员。解决的方式有两种,其一将对象型变量转换为void *类型(指向不限定某一具体类型的指针),其二是用__unsafed_unretained来修饰变量,这相当于告诉编译器该对象不需要被编译器管理。
  • 遵循内存管理的方法命名规则
    一般而言,命名方法时,谨慎使用以alloc/new/copy/mutablecopy开头的方法名,这类方法应当返回给调用方应该持有的对象。以init开头的方法应该返回实例对象。
  • 显示转换id和void
    在非ARC下,我们可以方便的直接对这两种类型做转换,如下所示
id obj = [NSObject alloc] init];
void * var = (void *)obj;
id obj2 = (id)var;

但是在ARC下,由于内存管理交给了编译器,因此编译器需要明确每个对象的所有者,该对象是否还有所有者,当无所有者时,编译器应当负责释放该对象。因此我们在转换类型时往往还需要考虑对象所有权问题。倘若只是想单纯的赋值,那么我们可以使用__bridge修饰符来完成转换。

id obj = [NSObject alloc] init];
void * var = (__bridge void *)obj;
id obj2 = (__bridge id)var;

与__bridge修饰符相关的两个修饰符是__bridge_retained和__bridge_transfer,这两个修饰符在完成转换的同时,还会对对象的所有权转移做处理。
__bridge_retained修饰符修饰的变量在被赋值时,还会获得被赋值对象的所有权,换句话说,会使对象引用计数增加。假定var是void *类型的变量,obj是id类型的变量,那么

var = (__bridge_retained void *)obj;

这等价于

var = (void *)obj;
[(id)var retain];

__bridge_transfer修饰符则正好和__bridge_retained相反,在赋值后被赋值对象随即被释放。同样的,假定var是void *类型的变量,obj是id类型的变量,那么

obj = (__bridge_transfer id)var

这等价于

obj = (id)var;
[obj retain];
[(id)var release];

这种转换常见于core Foundation对象与Foundation对象之间。前者是C语言类型对象,后者则是OC类型对象。

3. 属性

ARC下,类属性声明和对应的所有权修饰符是同样的作用。如下所示

属性声明的属性与所有权修饰符的对应关系
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,809评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,189评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,290评论 0 359
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,399评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,425评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,116评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,710评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,629评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,155评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,261评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,399评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,068评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,758评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,252评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,381评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,747评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,402评论 2 358

推荐阅读更多精彩内容