Autorelease和ARC

NSAutoreleasePool 和 @autoreleasepool

  1. NSAutoreleasePool 和 @autoreleasepool都是是Cocoa 用来支持引用计数内存管理机制的类, 当一个autorelease pool(自动释放池)被drain(销毁)的时候会对pool里的对象发送一条或者多条release的消息。

  2. NSAutoreleasePool仅能在MRC下使用,ARC下只能使用@autoreleasepool,它比NSAutoreleasePool类效率更高;鼓励用它来代替NSAutoreleasePool的位置。

  3. 一个对象可以放入同一个池中多次,在这种情况下,每次将对象放入池中时都会收到一条释放消息。 程序中至少存在一个自动释放池, 否则autoreleased对象将不能对应收到release消息而导致内存泄露.

  4. NSAutoreleasePool对象不能retain, 不能autorelease, 所以drain方法(或者release方法, 但是这两者有所不同, 下文会说)可以直接释放内存. 你应该在同一个上下文(调用创建这个池的同一个方法, 函数或者循环体)中drain一个自动释放池。

  5. MRC下需要对象调用autorelease才会入池, ARC下可以通过__autoreleasing修饰符, 否则的话看方法名, 非alloc/new/copy/mutableCopy开头的方法编译器都会自动帮我们调用autorelease方法。

注意: 主线程是默认在当前runloop循环结束的时候统一对加入到自动释放池中的对象发送release消息,如果当前这个对象引用计数为1的话那么它就将被销毁。

AutoreleasePool的释放有如下两种情况:
1.是Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop循环中都加入了自动释放池Push和Pop。
2.是手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool

NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSString* nsstring;
char* cstring = "Hello CString";
nsstring = [NSString stringWithUTF8String:cstring];
[pool release];(这一行代码就是在给pool发送drain消息了)

补充知识:使用ARC之后一个类方法(非alloc/new/copy/mutableCopy的初始化方法)生成的对象,没有任何附加标示,ARC怎么知道生成的对象是不是autorelease的呢?

@interface Sark : NSObject  
+ (instancetype)sarkWithMark:(NSString *)mark;       // 1  
- (instancetype)initWithMark:(NSString *)mark;       // 2  
@end 

1、生成autorelease对象。
2、生成普通对象,而现在ARC不能调用autorelease,使用时怎么能知道呢?
其实NS定义了下面三个编译属性

#define NS_RETURNS_RETAINED __attribute__((ns_returns_retained))  
#define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained))  
#define NS_RETURNS_INNER_POINTER __attribute__((objc_returns_inner_pointer)) 

这三个属性是Clang自己使用的标示,除非特殊情况不要自己使用,但是这些对理解ARC是很有帮助的。

  • NS_RETURNS_RETAINED
    init和initWithMark都属于init的家族方法。
    对于以alloc,init,copy,mutableCopy,new开头的家族的方法,后面默认加NS_RETURNS_RETAINED标识.ARC在会在调用方法外围要加上内存管理代码:retain+release。

  • NS_RETURNS_NOT_RETAINED
    sarkWithMark方法,则是不带alloc,init,copy,mutableCopy,new开头的方法,默认添加NS_RETURNS_NOT_RETAINED标识.标识返回的对象已经在方法内部做过autorelease了。

  • NS_RETURNS_INNER_POINTER
    这个只是做返回纯C语言的指针变量,ARC外围不必做内存管理的操作。

这里还要介绍一个概念,Method family:

An Objective-C method may fall into a method family, which is a conventional set of behaviors ascribed to it by the Cocoa conventions.

指的是命名上表示一类型的方法,比如- init和- initWithMark:都属于init的family于是乎,编译器约定,对于alloc,init,copy,mutableCopy,new这几个家族的方法,后面默认加NS_RETURNS_RETAINED标识;而其他不指名标识的family的方法默认添加NS_RETURNS_NOT_RETAINED标识。

也就是说刚才的方法,在编译器看来是这样的:

@interface Sark : NSObject  
+ (instancetype)sarkWithMark:(NSString *)mark NS_RETURNS_NOT_RETAINED; // 1  
- (instancetype)initWithMark:(NSString *)mark NS_RETURNS_RETAINED;     // 2  
@end 

这也就是为什么ARC下面,不能把一个属性定义成名字是这样的:

@property (nonatomic, copy) NSString *newString; // 编译器不允许 
  • newString就成了new家族的方法,ARC在外围添加内存管理代码的时候会加上retain+release,从而导致内存管理错误。
    对于NS_RETURNS_INNER_POINTER这货,主要使用在返回的是一个对象的内部C指针的情况。

ARC中显示或者隐式方法调用对引用计数的影响

OC中我们采取[target action]的方式调用方法,但在消息转发或者其他runtime参与的方法调用时,我们会用其他的书写方式来实现[target action]的一样的功能.姑且称[target action]为显示调用,其他的书写方式为隐式调用。(NSInvocation的使用就是最好的非明文调用)

  • 隐式调用工厂方法
- (void)ImplicitFunc{
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:"@@:"]];
    invocation.target = self;
    invocation.selector = @selector(createDic);
    [invocation invoke];
    
     //这样写的话会直接崩溃  ①
    NSDictionary * dict;
    [invocation getReturnValue:&dict];

    //正确做法  ②
    //NSDictionary * dict;
    //void * result;
    //[invocation getReturnValue:&result];
    //dict = (__bridge id)result;
}

-(NSDictionary *)createDic{
    return [[NSDictionary alloc]init];
}

  • 显示调用工厂方法
 -(void)explictFunc{
  [self  createDic];
}

在隐式调用的例子中中我们没有对son进行显式的赋值,而是传入 getReturnValue:方法中去获取返回值,这样的赋值后 ARC 没有自动给这个变量插入retain语句,但退出作用域时还是自动插入了release语句,导致这个变量多释放了一次,导致crash。

然而我们采用正确的做法,多了一个bridge就不crash了呢?

(__bridge T) op:告诉编译器在 bridge 的时候不要多做任何事情,__bridge 源在哪端,哪端管理对象的释放。
// objc to cf或者c
NSString *nsStr = [self createSomeNSString];
CFStringRef cfStr = (__bridge CFStringRef)nsStr;
CFUseCFString(cfStr);
// CFRelease(cfStr); 不需要
//源在Objc端 ARC管理内存
// cf或者c to objc
CFStringRef hello = CFStringCreateWithCString(kCFAllocatorDefault, "hello", kCFStringEncodingUTF8);
NSString *world = (__bridge NSString *)(hello);
CFRelease(hello); // 需要
[self useNSString:world];
//源在CF端 需要我们自己管理内存

[invocation getReturnValue:&result];将变量值写入了一个C指针, ARC 没有自动给这个变量插入retain语句。
dict = (__bridge id)result;这里完成了对象的retain。
ARC在退出方法的作用域时给对象加上release。完美!

Autorelease原理

AutoreleasePoolPage

而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。

AutoreleasePoolPage是一个C++实现的类

AutoreleasePoolPage.jpg
  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
    AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
  • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入

所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:

4069Byte.jpg

图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

释放时刻

每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子,于是:

哨兵对象.jpg

objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

  • 根据传入的哨兵对象地址找到哨兵对象所处的page
  • 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page,并向回移动next指针到正确位置。

运行时优化(Thread Local Storage)

hread Local Storage(TLS)线程局部存储,目的很简单,将一块内存作为某个线程专有的存储,以key-value的形式进行读写,比如在非arm架构下,使用pthread提供的方法实现:

void* pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t , const void *);

id objc_retainAutoreleaseReturnValue(id value)

*Precondition:*  `value`  is null or a pointer to a valid object.
If `value` is null, this call has no effect. Otherwise, it performs a retain operation followed by the operation described in [objc_autoreleaseReturnValue](http://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-runtime-objc-autoreleasereturnvalue) . Equivalent to the following code:

id objc_retainAutoreleaseReturnValue(id value){
return objc_autoreleaseReturnValue(objc_retain(value));
}

Always returns value.

objc_retainAutoreleasedReturnValue函数主要用于最优化程序运行,用于在alloc/new/copy/mutableCopy以外的方法。

  • 如果value为空这个函数无效。
  • 如果函数调用成功,它将接受一个来自最近调用的函数或者它调用的的,一个TLS引用计数的移交。
  • 如果调用失败,它执行一个和object_retain 完全一样的引用计数保留操作。

id objc_retainAutoreleasedReturnValue(id value)

Precondition: value is null or a pointer to a valid object.
If value is null, this call has no effect. Otherwise, it attempts to accept a hand off of a retain count from a call to objc_autoreleaseReturnValue on value in a recently-called function or something it calls. If that fails, it performs a retain operation exactly like objc_retain.
Always returns value.

objc_retainAutoreleasedReturnValue和objc_retainAutoreleasedReturnValue是成对出现的。

  • 如果value为空,函数无效。
  • 如果函数调用成功,它会执行一个高效的引用计数的移交。
  • 如果调用失败,那么这个对象会被自动释放,像objc_autorelease一样。

对于类似[NSArray array]的工厂方法,正常调用的情况下
工厂方法内由objc_autoreleaseReturnValue将对象放入Thread Local Storage;
工厂方法内由objc_retainAutoreleasedReturnValue将对象由Thread Local Storage取出.
简单的说就是中转不走autoreleasepool,由Thread Local Storage代劳,这样对于工厂方法而言避免使用autoreleasepool对象,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理。

当然走优化路径是有要求的:工厂方法的调用方与被调用方都支持ARC,因为只有这样方法内的·objc_autoreleaseReturnValue·与·objc_retainAutoreleasedReturnValue·才会配套使用.很多系统库还可能是MRC实现的,这样的系统类调用工厂方法生成的对象还是得进autoreleasepool。

那也就是说ARC下只要调用方和被调方都用ARC编译时,所建立的对象都不加入autoreleasepool.更简单的说我们自己写的类,调用工厂方法生成对象都不会放入autoreleasepool。

于是问题又来了,假如被调方和主调方只有一边是ARC环境编译的该咋办?(比如我们在ARC环境下用了非ARC编译的第三方库,或者反之)
只能动用更高级的黑魔法。

__builtin_return_address

这个内建函数原型是char *__builtin_return_address(int level),作用是得到函数的返回地址,参数表示层数,如__builtin_return_address(0)表示当前函数体返回地址,传1是调用这个函数的外层函数的返回值地址,以此类推。

  • (int)foo {
    NSLog(@"%p", __builtin_return_address(0)); // 根据这个地址能找到下面ret的地址
    return 1;
    }
    // caller
    int ret = [sark foo];
    看上去也没啥厉害的,不过要知道,函数的返回值地址,也就对应着调用者结束这次调用的地址(或者相差某个固定的偏移量,根据编译器决定)
    也就是说,被调用的函数也有翻身做地主的机会了,可以反过来对主调方干点坏事。
    回到上面的问题,如果一个函数返回前知道调用方是ARC还是非ARC,就有机会对于不同情况做不同的处理。

Autorelease和RunLoop

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

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

推荐阅读更多精彩内容