MRC下和ARC下Autorelease的异同

1. Autorelease简介

Autorelease是iOS开发的一种内存管理机制,用来延迟内存释放。系统在每个runloop中都加入了自动释放池的push和pop,Autorelease对象在当前的runloop迭代结束时释放,它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池的push和pop。

//autorelease方法
- (id)autorelease {
return ((id)self)->rootAutorelease();
}

//rootAutorelease 方法
inline id objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
//检查是否可以优化
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
//放到auto release pool中。
return rootAutorelease2();
}

// rootAutorelease2
id objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
// 把一个对象放到auto release pool中,是调用了AutoreleasePoolPage::autorelease这个方法。
return AutoreleasePoolPage::autorelease((id)this);
}

public: static inline id autorelease(id obj)
{
    assert(obj);
    assert(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}

static inline id *autoreleaseFast(id obj)
{
// autorelease方法会把对象存储到AutoreleasePoolPage的链表里。
//等到auto release pool被释放的时候,把链表内存储的对象删除。
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}

id *add(id obj)
{
    assert(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;
    protect();
    return ret;
 }

在iOS 程序启动之后,主线程会创建一个Runloop,也会创建两个Observer,回调工作都是在_wrapRunLoopWithAutoreleasePoolHandler()函数中。

第一个Observer监听的是Entry(即将进入Loop),回调是在_objc_autoreleasePoolPush()中创建自动释放池的,优先级是最高的,保证创建释放池是在所有回调之前。

第二个Observer监听有两个事件:BeforeWaiting(进入休眠)时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧的释放池以及创建新的释放池;Exit(退出Loop)调用_objc_autoreleasePoolPop()来释放自动释放池。这个优先级是最低的,保证释放池发生在所有回调之后调用。

AutoreleasePoolPage是依靠C++实现的类。

2. AutoreleasePoolPage

AutoreleasePool没有单独的结构,是由许多个AutoreleasePoolPage以双链表的方式组合而成的,parent指向是前一个page,而child指向的是下一个page。

AutoreleasePoolPage中的每个对象都会开辟出虚拟内存一页的大小(也就是4096个字节),除了实例变量占据空间,其他的空间都用来存储autorelease对象的地址。

id *next指向的是栈顶对象的下一个位置

如果AutoreleasePoolPage空间被占满时,会创建一个AutoreleasePoolPage连接链表,后来的对象也会在新的page加入。

双向链表的优点
双向链表是每个节点有2个链接,一个是指向前一个节点(当此链接为第一个链接时,指向的是空值或空列表),另一个则指向后一个节点(当此链接为最后一个链接时,指向的是空值或空列表)。即一个是指向前一个节点的指针,另一个则指向后一个节点的指针。

单向链表适用于节点的增加删除,双向链表适用于需要双向查找节点值的情况。这即是AutoreleasePoolPage以双链表的方式组合的原因。缺点就是空间占用较单链表大。

假设当前线程只有一个AutoreleasePoolPage对象,对象的内存地址如下图:



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

释放时刻:

每进行一次objc_autoreleasePoolPush调用时,runtime就会将当前的AutoreleasePoolPage加入一个哨兵对象,就会变成下面结构:


自动释放池是以一个个AutoreleasePoolPage组成,而AutoreleasePoolPage以双链表形成的自动释放池。pop的时候传入边界的对象,然后再对page中的对象发送release消息。

objc_autoreleasePoolPush返回值也就是哨兵对象的地址,被objc_autoreleasePoolPop作为参数。于是:

根据传入的哨兵位置找到哨兵所对应的page
将晚于哨兵对象插入的autorelease对象都发送一个release消息,并移动next指针到正确的位置

3. MRC和ARC下的不同

ARC与MRC的autorelease的使用,如下:

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

// ARC 
@autoreleasepool { 
id obj = [NSObject alloc] init]; 
} 

在ARC下,我们如果使用@autoreleasepool{}来创建一个AutoreleasePool,随后编译器将会改成下面:

void *context = objc_autoreleasePoolPush(); 

// {}中的代码 

objc_autoreleasePoolPop(context); 

4. 子线程中Autorelease的释放

1.子线程在使用autorelease对象时,如果没有autoreleasepool会在autoreleaseNoPage中懒加载一个出来。

2.在runloop的run:beforeDate,以及一些source的callback中,有autoreleasepool的push和pop操作,总结就是系统在很多地方都差不多autorelease的管理操作。

3.就算插入没有pop也没关系,在线程exit的时候会释放资源,执行AutoreleasePoolPage::tls_dealloc,在这里面会清空autoreleasepool。

__weak id obj;
...
[NSThread detachNewThreadSelector:@selector(createAndConfigObserverInSecondThread) toTarget:self withObject:nil];

- (void)createAndConfigObserverInSecondThread{
__autoreleasing id test = [NSObject new];
NSLog(@"obj = %@", test);
obj = test;
[[NSThread currentThread] setName:@"test runloop thread"];
NSLog(@"thread ending");
}

在obj = test处设置断点使用 watchpoint set variable obj命令观察obj,可以看到obj在释放时的方法调用栈是这样的。

通过这个调用栈可以看到释放的时机在_pthread_exit。然后执行到AutorelepoolPage的tls_dealloc方法。

thread在退出时会释放自身资源,这个操作就包含了销毁autoreleasepool,在tls_delloc中,执行了pop操作。
线程在销毁时会清空autoreleasepool。但是上述这个例子中的线程并没有加入runloop,只是一个一次性的线程。现在给这个线程加入runloop来看看效果会是怎么样的。

- (void)createAndConfigObserverInSecondaryThread{
[[NSThread currentThread] setName:@"test runloop thread"];
NSRunLoop *loop = [NSRunLoop currentRunLoop];
CFRunLoopObserverRef observer;
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                   kCFRunLoopAllActivities,
                                   true,      // repeat
                                   0xFFFFFF,  // after CATransaction(2000000)
                                   YYRunLoopObserverCallBack, NULL);
CFRunLoopRef cfrunloop = [loop getCFRunLoop];
if (observer) {
    
    CFRunLoopAddObserver(cfrunloop, observer, kCFRunLoopCommonModes);
    CFRelease(observer);
}
[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(testAction) userInfo:nil repeats:YES];
[loop run];
NSLog(@"thread ending");
}

- (void)testAction{
__autoreleasing id test = [NSObject new];
obj = test;
NSLog(@"obj = %@", obj);
}

在testAction()中加上watchpoint断点,观察obj的释放时机。

释放的时机在CFRunloopRunSpecific中,也就是runloop切换状态的时候。timer在自己的callback函数里插入了释放autorelesepool的代码。对于runloop,我们知道runloop一定要有source才能保证run起来以后不立即结束,而source有三种,custom source,port source,timer。即使是我们自定义的source,执行函数中没有释放autoreleasepool的操作也不用担心,系统在各个关键入口都给我们加了这些操作。

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

推荐阅读更多精彩内容