Objective-C高级编程(上):ARC

《Objective-C高级编程:iOS与OS X多线程和内存管理》是iOS开发中一本经典书籍,书中有关ARC、Block、GCD的梳理是iOS开发进阶路上必不可少的知识储备。笔者读完此书后为了加强理解,特以笔记记之。本文为开篇,围绕ARC谈起Objective-C中的内存管理。

ARC

鉴于本书翻译自日文原版且翻译偏向书面,笔者希望采用通俗的语言记录,文章结构略有调整。

本文首发于Rachal's blog

内存管理

ARC(自动引用计数)是iOS5、macOS10.7引入的内存管理技术,为了循序渐进的方式了解这项技术,本书先从ARC无效的环境说起,也就是常指的MRC(手动引用计数)环境。

本书开篇没有直接提及引用计数的概念,而是以办公室开灯关灯的例子引出内存管理的思考方式。作者认为理解内存管理时把注意力落在“生成”、“持有”、“释放”等管理操作上更为客观。

内存管理的思考方式

  • 自己生成的对象,自己所持有。
  • 非自己生成的对象,自己也能持有。
  • 不再需要自己持有的对象时释放。
  • 非自己持有的对象无法释放。

这里的“自己”理解为编程人员自身。与“生成”、“持有”、“释放”操作并列的还有“废弃”,分别对应以下方法:

对象操作 Objective-C方法
生成并持有对象 alloc/new/copy/mutableCopy等方法
持有对象 retain方法
释放对象 release方法
废弃对象 dealloc方法
  • 注意:以上方法包含在Cocoa框架中而非Objective-C语言中。

自己生成的对象,自己所持有

以下面名称开头的方法生成的对象为自己持有:

  • alloc
  • new
  • copy
  • mutableCopy
id obj1 = [[NSObject alloc] init];// 自己生成并持有
id obj2 = [NSObject new];// 自己生成并持有

另外,根据以上原则,下列方法也意味着自己生成并持有对象:

  • allocMyObject
  • newThatObject
  • copyThis
  • mutableCopyYourObject
id obj = [MyObject allocMyObject];

// 内部实现
+ (MyObject *)allocMyObject {
    MyObject *obj = [[MyObject alloc] init];
    return obj;
}

非自己生成的对象,自己也能持有

alloc/new/copy/mutableCopy以外方法取得对象,非自己生成,自己不持有对象。可以通过retain方法为自己所持有。

id obj = [NSMutableArray array];// 取得对象,但自己不持有
[obj retain];// 自己持有对象

不再需要自己持有的对象时释放

自己持有的对象不再需要时,持有者有义务将其释放。释放使用release方法。

id obj = [[NSObject alloc] init];// 自己生成并持有对象
[obj release];// 释放对象

retain方法持有对象,一旦不再需要,务必要用release方法释放。

id obj = [NSMutableArray array];// 取得对象,但自己不持有
[obj retain];// 持有非自己生成对象
[obj release];// 释放对象

类似[NSMutableArray array]方法取得的对象存在,但自己不持有对象,内部如何实现?以object这个方法名为例:

- (id)object {
    id obj = [[NSObject alloc] init];// 自己持有
    [obj autorelease];// 适当时机自动释放
    return obj;// 取得对象存在,但自己不持有
}

autorelease提供这样的功能,使对象在超出指定的生存范围时能够自动并正确地释放。
使用NSMutableArray类的array类方法等可以取得谁都不持有的对象,这些方法是通过autorelease实现的。

非自己持有的对象无法释放

alloc/new/copy/mutableCopy方法生成并持有的对象,或用retain方法持有的对象,在不需要时要将其释放。倘若在应用程序中释放了非自己持有的对象会造成崩溃

id obj = [[NSObject alloc] init];// 自己生成并持有对象
[obj release];// 释放对象
[obj release];// 重复释放对象,崩溃
id obj1 = [obj0 object];// 取得对象,但自己不持有
[obj1 release];// 释放非自己持有的对象,崩溃

alloc/retain/release/dealloc及其实现

Cocoa是macOS的系统框架,在iOS上被称为Cocoa Touch。Cocoa框架虽然没有公开,但是可以通过Cocoa框架的互换框架GNUstep来推测苹果的实现。

alloc调用allocWithZone,那么这里的参数类型NSZone是什么?

它是为了防止内存碎片化而引入的结构。对内存分配的区域本身进行多重化的管理,根据使用对象的目的、对象的大小分配内存,从而提高内存管理的效率。
现在运行时系统中的内存管理已经极具效率,使用区域来管理内存反而会引起内存使用效率低下以及源代码复杂等问题。

GNUstep的实现

GNUstep源码里alloc类方法用obj_layout结构体中的整数变量retained来保存引用计数retainCount,并将其写入对象内存头部。

执行alloc后对象的实例方法retainCount获得数值是1,retain使变量retained值+1,release使变量retained值-1。release使tetained变量大于0时-1,等于0时调用dealloc实例方法,废弃对象。

具体总结如下:

  • 在Objective-C的对象中存有引用计数这一整数值。
  • 调用alloc或是retain方法后,引用计数值+1。
  • 调用release后,引用计数值-1。
  • 引用计数值为0时,调用dealloc方法废弃对象。

苹果的实现

alloc过程设置断点追踪调用的方法和函数:

+alloc
+allocWithZone:
class_createInstance
calloc//分配内存块

苹果对alloc的实现与GNUstep并无多大差异。

retainCount/retain/release调用的方法和函数分别如下:

-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
__CFDoExternRefOperation
CFBasicHashAddValue
-release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(CEBasicHashRemoveValue返回0时,-release 调用dealloc)

可以从__CFDoExternRefOperation函数以及一些CFBasicHash开头的函数名看出,苹果的实现大概就是采用散列表(又称哈希表)来管理引用计数。

在引用计数表中,key为内存块地址,value为对应的引用计数,苹果这样实现的优势在于:

  • 为对象分配内存块时无需考虑内存块头部。
  • 对象占用内存块损坏时,可以根据引用计数表来确认内存块的位置。
  • 检测内存泄露时,根据引用计数表中的记录检查对象的持有者是否存在。

autorelease及其实现

autorelease会像C语言的自动变量那样对待对象实例。当超出其作用域时,对象实例的release实例方法被调用。

autorelease具体使用方法如下:

  • 1.生成并持有NSAutoreleasePool对象;
  • 2.调用已分配对象的autorelease实例方法;
  • 3.废弃NSAutoreleasePool对象。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj release];
[pool drain];

Cocoa框架中程序主循环的NSRunLoopNSAutoreleasePool对象进行生成、持有和废弃处理。在大量产生autorelease对象时,若不废弃NSAutoreleasePool对象,那么生成的对象就不能被废弃,会产生内存不足现象。

GNUstep的实现

autorelease实例方法的本质就是调用NSAutoreleasePool对象的addObject类方法。

[obj autorelease];

源码:

- (id)autorelease {
    [NSAutoreleasePool addObject:self];
}

GNUstep在实现NSAutoreleasePool时使用连接列表,可以理解为数组。若调用NSObject类的autorelease方法,该对象就会被追加到正在使用的NSAutoreleasePool对象的数组中。drain实例方法废弃正在使用的NSAutoreleasePool对象,会对数组中的所有对象调用release方法。

苹果的实现

autoreleasepool以数组的形式实现,主要通过以下3个函数:

  • obj_autoreleasePoolPush()
  • obj_autorelease(obj)
  • obj_autoreleasePoolPop(pool)
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/* 等同于objc_autoreleasePoolPush */

id obj = [[NSObject alloc] init];

[obj autorelease];
/* 等同于 objc_autorelease(obj) */

[pool drain];
/* 等同于 objc_autoreleasePoolPop(pool) */

以上是MRC环境下的内存管理及实现。

ARC

ARC概述

ARC(Auto Reference Counting)是iOS5、macOS10.7(OS X Lion)引入的内存管理技术。

ARC的出现解决了原来需要手动键入retainrelease操作的问题。这在降低程序崩溃、内存风险的同时,很大程度上减少了开发程序的工作量。

内存管理的思考方式

“引用计数式内存管理”的本质在ARC中并没有改变,ARC只是自动地帮我们处理“引用计数”的相关部分。

  • 自己生成的对象,自己所持有。
  • 非自己生成的对象,自己也能持有。
  • 不再需要自己持有的对象时释放。
  • 非自己持有的对象无法释放。

所有权修饰符

ARC环境下其类型必须附加所有权修饰符(有省略的情况),所有权修饰符有以下4种:

  • __strong
  • __weak
  • __unsafe_unretained
  • __autorelease

书中此处提到id类型做一下记录:

Objective-C中为了处理对象,可将变量定义为id类型,id类型用于隐藏对象类型的类名部分,相当于C语言中常用到的void *

__strong修饰符

id和对象类型默认使用__strong修饰,由于是默认情况,可省略不写。

__strong表示对对象的强引用。持有强引用的变量在超出其作用域时被废弃。

__strong__weak__autoreleasing一样,可以保证被修饰的变量在初始化时为nil

id obj = [[NSObject alloc] init];

//等同于
//id __strong obj = [[NSObject alloc] init];

__weak修饰符

循环引用容易引起内存泄漏。所谓内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。使用__weak修饰符可以避免循环引用。

__weak表示弱引用,弱引用不能持有对象实例。

id __weak obj = [[NSObject alloc] init];//编译器会警告

__weak修饰符还有另一个优点。在持有对象的弱引用时,若对象被废弃,则此弱引用将失效且处于nil被赋值的状态。

通过检查__weak修饰的变量是否为nil可以判断被赋值的对象是否已废弃。

__weak只能用于iOS5和macOS10.7以上版本,在iOS4和macOS10.6及以前用__unsafe_unretained代替。

__autoreleasing修饰符

ARC下指定@autoreleasepool块来替代NSAutoreleasePool类生成、持有及废弃这一范围。_autoreleasing修饰变量等价于对象调用autorelease方法,即可将对象注册到autoreleasepool中。

@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}
  • 提问:前文提到__weak修饰的变量必须注册到autoreleasepool中,为什么?
  • 答:因为__weak修饰的变量只能持有对象的弱引用,在访问对象的过程中,该对象可能被废弃。如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前能确保该对象存在。

_autoreleasing__strong一样,显式使用罕见。

ARC的规则

ARC环境下编译源代码遵循一定规则:

  • 不能使用retain/release/retainCount/autorelease
  • 不能使用NSAllocateObject/NSDeallocateObject

ARC有效时,以上方法会导致编译器报错。

  • 必须遵守内存管理的方法命名规则

对象的生成、持有的方法必须遵循命名规则:alloc/new/copy/mutableCopy。以init开头的方法更严格:必须是实例方法且必须返回对象,返回对象的类型必须是id类型或该方法声明类的对象类型。

  • 不要显式调用dealloc

dealloc方法无需显式调用,但C语言库需要在deallocfree,以及删除已注册的通知观察者。

  • 使用@autorelease块代替NSAutoreleasePool

ARC有效时,使用@autoreleasepool块代替NSAutoreleasePool

  • 不能使用区域(NSZone

不管ARC是否有效,区域在现在运行时系统中已单纯地被忽略。

  • 对象型变量不能作为C语言结构体(struct/union)的成员

C语言的规约上没有方法来管理结构体成员变量的生存周期。

  • 显式转换“id”和“void *
/* ARC无效 */
id obj = [[NSObject alloc] init];
void *p = obj

ARC有效时需要通过__bridge来显式转换:

/* ARC有效 */
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
  • 动态数组中操作__strong修饰的变量与静态数组有很大差异,需要自己释放所有元素。静态数组中,编译器能够根据变量的作用域自动插入释放赋值对象的代码,而在动态数组中,编译器不能确定数组的生存周期,所以无从处理。

ARC的实现

__strong的实现

  • 自己生成并持有
{
    id __strong obj = [[NSObject alloc] init];
}
/* 编译器的模拟代码 */
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
  • 非自己生成持有
id __strong obj = [NSMutableArray array];
/* 编译器的模拟代码 */
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);

objc_retainAutoreleasedReturnValue函数用于持有对象,注册到autoreleasepool中并返回。与之对应的函数是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_retainAutoreleasedReturnValue函数和objc_autoreleaseReturnValue函数的协作,可以不将对象注册到autoreleasepool中而直接传递,以达到最优化程序运行

__weak的实现

使用__weak修饰的变量,就是使用注册到autoreleasepool中的对象。

{
    id __weak obj1 = obj;
}
/* 编译器模拟代码 */
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
objc_destroyWeak(obj1);

__weak同引用计数一样通过散列表(哈希表)实现,大致流程如下:

  • 1.objc_initWeak(&obj1, obj)函数初始化__weak修饰的变量,通过执行objc_storeWeak(&obj1, obj)函数,以第一个参数(变量的地址)作为key,把第二个参数(赋值对象)作为value存入哈希表。
  • 2.由于弱引用不能持有对象,函数objc_loadWeakRetained(&obj1)取出所引用的对象并retain
  • 3.objc_autorelease(tmp)函数将对象注册到autoreleasepool中。
  • 4.objc_destroyWeak(&obj1)函数释放__weak修饰的变量,通过过程执行objc_store(&obj1, 0)函数,在weak表中查到变量地址并删除。废弃对象调用objc_clear_deallocating函数,这个过程会将weak表记录中__weak修饰的变量地址赋值为nil

如果大量使用__weak修饰的变量,则会消耗相应的CPU资源。良策是只在需要避免循环引用时使用__weak修饰符。

__autoreleasing的实现

_autoreleasing修饰变量,等同于ARC无效时对象调用autorelease方法。

@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}
/* 编译器的模拟代码 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelese(obj);
objc_autoreleasePoolPop();

以上为ARC篇的学习内容。

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

推荐阅读更多精彩内容