浅析OC内存管理

前言:本篇内容假设您已经对内存管理有了基础的理解。如retain、release、autorelease、autoreleasepool的释放及使用、引用计数式内存管理概念等。本篇内容将围内存管理进行一些深入的探究。

一、内存管理的思考方式
1.1、内存管理的黄金法则

  •       自己生成的对象自己持有。
    
  •        非自己生成的对象,自己也能持有。
    
  •       不再需要自己持有的对象时释放。
    
  •       非自己持有的对象无法释放。
        以上出现了“生成”、“持有”、“释放”三个词。而在OC内存管理中还要加上“废弃”一词。
    

对象操作与OC方法的对应


对象操作与oc方法的对应.jpeg

1.2、命名规则
生成并持有对象的方法名命名规则是遵循驼峰设计法。下列名称意味着自己生成并持有对象。

  • allocMyObject
  • newThatObject
  • copyThis
  • mutableCopyYourObject

但是对于以下名称,并不在该规则范围内。

  • allocate

  • newer

  • copyingThis
    开发过程中必须严格遵守内存管理命名规则,编译器会根据方法名做出相应的处理。

    1.3、关于生成并持有对象与生成不持有对象的实现
    那么,如果要用某个方法生成对象,并将其返还给该方法的调用方,它的源代码又是怎样的呢?
    -(id)allocObject{
    //自己成并持有对象
    id obj = [[NSObject alloc]init];
    //自己持有对象
    return obj;
    }
    如上例所示,原封不动的返回用alloc方法生成的对象,就能让调用方也持有该对象。

那么,如果类似调用【NSMutableArray array】方法使取得的对象存在,但自己不持有对象。又是如何实现的呢?
-(id)object{
//自己持有对象
id obj = [[NSObject alloc]init];
//将对象放入自动释放池
[obj autorelease];
//取得对象存在,但自己不持有对象
return obj;
}
上例中(注意,方法名不能以alloc/new/copy/mutableCopy等名称开头),使用了autorelease方法。用该方法,可以使取得的对象存在,但自己不持有对象。autorelease提供这样的功能,使对象在超出指定生存范围时能够自动并正确的释放。当然,也能够通过retain方法将调用autorelease方法取得的对象变为自己持有。

二、ARC规则
2.1、所有权修饰符
OC中为了处理对象,可将变量类型定义为id类型或各种对象类型。
所谓对象类型就是指向NSObject这样的OC类的指针,例如“NSObject *”。id类型用于隐藏对象类型的类名部分,相当于C语言中的“void *
ARC有效时,id类型和对象类型同C语言的其他类型不同,其类型上必须附件所有权修饰符。

  • __strong 修饰符
    
  • __weak 修饰符
    
  • __unsafe_unretained 修饰符
    
  • __autoreleasing 修饰符
    

    __strong修饰符
    __srong修饰符是id类型和对象类型默认的所有权修饰符。在ARC中,附有__strong修饰符的变量obj在超出其变量作用域时,即在该变量被废弃时,会释放其被赋予的对象。通过__strong修饰符,不必再次键入retain或者release即可满足完美地满足“引用计数式内存管理的思考方式”。另外,__strong修饰符同后面的__weak修饰符和__autoreleasing修饰符都可以保证将附有这些修饰符的自动变量初始化为nil。

    __weak修饰符
    众所周知,当两个OC对象互相强引用时会产生循环引用,而打破循环引用的利器就是__weak修饰符(弱引用)。__weak修饰符还有另一个有点。在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效且处于nil被赋值的状态,从而有效的避免野指针(指向已被释放内存的指针)的发生。
    在底层实现上,编译器会维护一个weak表来实现,对象废弃将弱引用指针赋值为nil的操作。
    weak表是以散列表的方式来实现的。其key为赋值对象的地址,value为被__weak修饰符修饰的变量。在赋值对象被废弃时最后会执行以下操作以确保__weak修饰符修饰的变量被置为nil。

  • 从weak表中获取废弃对象的地址作为key的记录。

  • 将包含在记录中的所有附有__weak修饰符变量的地址赋值为nil。

  • 从weak表中删除该记录。

  • 从引用计数表中删除废弃对象的地址为key的记录。

  • 废弃对象
    由此可知,如果大量使用附有__weak修饰符的变量,则会消耗相应的CPU资源。良策是只在需要避免循环引用时使用__weak修饰符。

    __unsafe_unretained 修饰符
    __unsafe_unretained修饰符正如其名,是不安全的所有权修饰符。附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。__unsafe_unretained与__weak修饰符一样不会持有对象的强引用。其不同之处在于,若该对象被废弃,编译器不会对被__unsafe_unretained修饰的变量做任何处理从而产生野指针。

    __autoreleasing 修饰符
    关于__autoreleasing修饰符可以理解为,在ARC有效时,用@autoreleasepool块替代NSAutoreleasePool 类,用附有__autoreleasing修饰符的变量替代autorelease方法。日常开发中我们虽然很少显式使用__autoreleasing修饰符,但是编译器的确通过隐式的使用__autoreleasing修饰符完成内存管理的工作。例如:

  • 如之前提到的命名规则,编译器会检查方法名是否以alloc/new/copy/mutableCopy等名称开头,如果不是则自动将返回的对象以__autoreleasing修饰符修饰(注册到autoreleasepool)。

  • 虽然__weak修饰符是为了避免循环引用而使用的,但在访问附有__weak修饰符的变量时,即是使用注册到autoreleasepool中的对象。这是因为__weak修饰符只持有对象的弱引用,而在访问对象的弱引用的过程中,该对象有可能会被废弃。如果把要访问的对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保该对象的存在。因此,在使用附有__weak修饰符的变量时,即是使用注册到autoreleasepool中的对象。但是,如果大量地使用附有__weak 修饰符的变量,注册到autoreleasepool的对象也会大量的增加,因此在使用附有__weak修饰符的变量时,最好先暂时的赋值给附有__strong修饰符的变量后使用。

  •   id的指针(id *)或对象的指针(NSObject * *)在没有显示指定时会被附加上__autoreleasing修饰符。比如,为了得到错误的详细信息,经常会在方法的参数中传递NSError对象的指针,而不是函数的返回值。Cocoa的框架中大多数方法也使用这种方式。                                                                             
    

    ```
    NSError * error = nil;
    BOOL result = [obj performOperationWithError:&error];
    ```
    该方法的声明为:

    -(BOOL)performOperationWithError:(NSError **)error;

      如同上面的描述一样,id的指针和对象的指针会默认附加上__autoreleasing修饰符,所以等同于一下源代码:
    

    -(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;

      参数中持有NSError对象指针的方法,虽然为响应其执行结果,需要生成NSError类对象,但也必须符合内存管理的思考方式。以alloc/new/copy/mutableCopy等名称开头的方法返回值取得的对象是自己生成并持有的,其他情况下便是取得非自己生产并持有的对象。使用__autoreleasing修饰符的变量作为对象取得参数,与除alloc/new/copy/mutableCopy外其他方法返回的对象一样都会注册到autoreleasepool,并取得非自己生产并持有的对象。
    
      比如performOperationWithError 的实现应该是这样:
    

-(BOOL)performOperationWithError:(NSError * __autoreleasing )error{
//生成的
error 将被注册到autoreleasepool
*error = [[NSError alloc]initWithDomain:(ErrorDomain) code:code userInfo:nil];
return YES;
}

在使用参数取得对象时,贯彻内存管理的思考方式,我们需要将参数声明为附有__autoreleasing修饰符的对象指针类型。

2.2、显式转换id 和 void *
在ARC无效时id 型变量和 void * 变量互相赋值(强制转换)是没有任何问题的。但是在ARC有效时,id 型变量和 void * 变量互相赋值时需要进行特定的转换。转换的方式我们称之为“桥接”,桥接的方式有三种:

  • __bridge 转换,如果只是单纯的赋值操作,可以使用“__bridge转换”

    id obj = [[NSObject alloc]init];
    void * p = (__bridge void *)obj;
    id o = (__bridge id)p;

    但是,“__bridge 转换”的安全性与赋值给__unsafe_unretained修饰符相近,甚至会更低。如果管理时不注意赋值对象的所有者,就会因为野指针而导致程序崩溃。

  • __bridge_retain 转换,可以使要转换赋值的变量也持有所赋值的对象。另外,__bridge_retain 只提供了将 id 类型变量转换为 void * 变量的能力(即 oc -> c)。
    void * p = 0;
    {
    //obj 指向对象 引用计数 1
    id obj = [[NSObject alloc]init];
    //p 持有 obj 指向对象 引用计数二
    p = (__bridge_retained void *)obj;

    }
    //obj 释放 引用计数减一
    //访问 p 指向对象
    NSLog(@"class = %@",[(__bridge id)p class]);

变量作用域结束时,虽然obj失效,对象引用计数减一。但由于__bridge_retain 转换使变量p仍然处于持有该对象的状态,因此该对象不会被废弃。

  • __bridge_transfer 转换,提供了与__bridge_retain相反的动作,被转换的变量所持有的对象在该变量被赋值给转换目标变量后随即释放对对象的强引用。另外 __bridge_transfer 只提供了将 void * 变量转换为 id 类型变量的能力
    // p 指向创建的对象 引用计数为1
    void * p = (__bridge_retained void *)[[NSObject alloc]init];
    // obj 指向对象的同时 p释放对对象的强引用 引用计数依然为1
    id obj = (__bridge_transfer id)p;

同__bridge_retain转换与retain类似,__bridge_transfer转换与release类似。

2.3、Objective-C对象 与 Core Foundation对象

    在2.2 提到的“桥接转换”多数使用在OC对象与Core Foundation对象之间的相互变换中。OC对象与Core Foundation对象的区别很小,不同之处只在于是由哪一个框架所生成的。无论是由哪一种框架所生成的对象,一旦生成之后,便能在不同的框架之中使用。Foundation框架的API生成的对象可以用Core Foundation框架的API释放。当然反过来也是可以的。
    因为Core Foundation 和 OC 对象没有区别,所以在ARC无效时,OC变量和Core Foundation变量之间可以相互赋值。而ARC是基于NSObject的 无法管理Core Foundation的内存,所以就需要一些特定的转换。除了 “桥接转换之外” Core Foundation 提供了两个类似的函数进行OC 对象 与Core Foundation对象之间的转换。
  • CFBridgingRetain(<id _Nullable X>) 其功能与 __bridge_retain 一致

    CFMutableArrayRef cfObj = NULL;
    {
    // 变量 obj 持有 对生成对象的强引用 引用计数1
    NSMutableArray * obj = [[NSMutableArray alloc]init];
    //通过CFBridgingRetain 将 对象CFRetain并赋值给变量cfObj 引用计数为2
    cfObj = (CFMutableArrayRef) CFBridgingRetain(obj);

    }
    //obj 超出作用域 其强引用失效 引用计数 1

    //将该对象CFRelease 引用计数0 释放对象
    CFRelease(cfObj);

  • CFBridgingRelease(<CFTypeRef  _Nullable X>) 其功能与 __bridge_transfer 一致
    

{
//Core Foundation 框架生成对象 对象引用计数1
CFMutableArrayRef cfObj = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
//通过CFBridgingRelease赋值,变量obj持有对象的强引用的同时,cfObj 通过CFRelease释放对象的强引用 对象引用计数1
NSMutableArray * obj = CFBridgingRelease(cfObj);

}
//obj 超出作用域 引用计数0 对象释放

在OC对象与Core Foundation 对象 (id 对象 和 void * 对象)相互转换时 必须恰当的使用CFBridgingRetain 和 CFBridgingRelease (__bridge_retain 和__bridge_transfer),否则会产生内存泄漏或者野指针。因此在实现代码时一定要高度审视。

2.3最优化程序运行
如之前提到的命名规则,编译器会检查方法名是否以alloc/new/copy/mutableCopy等名称开头,如果不是则自动将返回的对象以__autoreleasing修饰符修饰。而在ARC中,编译器通常会返回objc_autoreleaseReturnValue(obj)函数返回的的对象,而不是objc_autorelease(obj)。objc_autorelease(obj)的作用仅限于将对象注册到autoreleasepool中。而objc_autoreleaseReturnValue(obj)的作用不仅限于注册对象到autoreleasepool中。
objc_autoreleaseReturnValue函数会检查使用该函数的方法或函数调用方的执行命令列表,如果方法或函数的调用在调用了方法或函数后紧接着调用了objc_retainAutoreleasedReturnValue函数,那么久不将返回的对象注册到autoreleasepool中,而是直接传递到方法或函数的调用方。通过objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue函数的协作,可以不将对象注册到autoreleasepool中而是直接传递,使这一过程达到最优。体现在代码层类似这样(以下为伪代码):

+(id)array{
return [[NSMutableArray alloc]init];
}

以上代码在ARC中会转换为以下伪代码:

+(id)array{
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_msgSend(obj,@selector(init));
return objc_autoreleaseReturnValue(obj);
}

而当[NSMutableArray array]方法的返回值赋给被__strong修饰符修饰的变量时,编译器会调用objc_retainAutoreleasedReturnValue函数以避免将对象注册到autoreleasepool中
{
id __strong obj = [NSMutableArray array];
}

以上代码在ARC中会转换为以下伪代码:

id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleasedReturnValue(obj);

//超出obj 作用域 释放对象
objc_release(obj);

2.4属性修饰符与所有权修饰符

  赋值给属性修饰符修饰的属性就相当于赋值给各属性修饰符对应的所有权修饰符修饰的变量中。只有copy属性不是简单的赋值,它赋值的是通过NSCopying接口的copyWithZone:方法复制赋值源所生成的对象。以下为属性修饰符与所有权修饰符的对应关系

属性修饰符
所有权修饰符
assign
__unsafe_unretained修饰符
copy
__strong修饰符(但是赋值的是被复制的对象)
retain
__strong修饰符
strong
__strong修饰符
unsafe_unretained
__unsafe_unretained修饰符
weak
__weak修饰符

本篇对于OC内存管理进行了简要的剖析,如有什么疑问或不对的地方,欢迎评价指正,共同讨论共同进步。

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

推荐阅读更多精彩内容