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

MRC下和ARC下Autorelease的异同_第1张图片

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

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

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

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

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

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

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


MRC下和ARC下Autorelease的异同_第2张图片

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

释放时刻:

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


MRC下和ARC下Autorelease的异同_第3张图片

自动释放池是以一个个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的操作也不用担心,系统在各个关键入口都给我们加了这些操作。

你可能感兴趣的:(MRC下和ARC下Autorelease的异同)