AutoreleasePool详解和runloop的关系

AutoreleasePool详解和runloop的关系

内存管理一直是学习 Objective-C 的重点和难点之一,在实际的软件开发工作中,经常会遇见由于内存原因而导致的crash。而autorelease pool在内存管理中有着举足轻重的作用,只有理解了 autorelease pool 的原理,我们才算是真正了解了 Objective-C 的内存管理机制。下面我会从以下几个方面给大家讲解:

  • NS AutoreleasePool是什么?
  • NSAutoreleasePool的实现原理是什么?
  • NSAutoreleasePool何时释放?
  • 如何使用Autorelease Pool Blocks
  • AutoreleasePool与runloop和线程的关系

NSAutoreleasePool是什么?

NSAutoreleasePool实际上是个对象引用计数自动处理器,在官方文档中被称为是一个类。Objective-C的对象(全部继承自NSObject),就是使用引用计数的方法来管理对象的存活,众所周知,当引用计数为0时,对象就被销毁了。操作非常简单,当对象被创建时,引用计数被设成1。可以给对象发送retain消息,让对象对自己的引用计数加1。而当对象接受到release消息时,对象就会对自己的引用计数进行减1,当引用计数到了0,对象就会调用自己的dealloc处理。当对象被加入到NSAutoreleasePool中,会对其对象retain一次,当NSAutoreleasePool结束时,会对其所有对象发送一次release消息。NSAutoreleasePool可以同时有多个,它的组织是个栈,总是存在一个栈顶pool,也就是当前pool,每创建一个pool,就往栈里压一个,改变当前pool为新建的pool,然后,每次给pool发送drain消息,就弹出栈顶的pool,改当前pool为栈里的下一个pool。

NSAutoreleasePool的实现原理是什么?

@autoreleasepool {} 在编译时 @autoreleasepool {} 被转换为一个__AtAutoreleasePool ,通常这个结构体会在初始化时调用 objc_autoreleasePoolPush()方法,在析构时调用 objc_autoreleasePoolPop () 方法。而这些方法都是对AutoreleasePoolPage的简单封。AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)。我们使用的所以想深入理解AutoreleasePool必须首先了解AutoreleasePoolPage。一个空的 AutoreleasePoolPage 的内存结构如下图所示:

AutoreleasePool详解和runloop的关系_第1张图片

  1. magic 用来校验 AutoreleasePoolPage 的结构是否完整;
  2. next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
  3. thread 指向当前线程;
  4. parent 指向父结点,第一个结点的 parent 值为 nil ;
  5. child 指向子结点,最后一个结点的 child 值为 nil ;
  6. depth 代表深度,从 0 开始,往后递增 1;
  7. hiwat 代表 high water mark 。

一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,通过parent和child指针连接成链表,后来的autorelease对象在新的page加入。下面是某个线程的 autoreleasepool 堆栈的内存结构图,在这个 autoreleasepool 堆栈中总共有两个 POOL_SENTINEL (哨兵),即有两个 autoreleasepool 。该堆栈由三个 AutoreleasePoolPage 结点组成,第一个 AutoreleasePoolPage 结点为 coldPage() ,最后一个 AutoreleasePoolPage 结点为 hotPage() 。其中,前两个结点已经满了,最后一个结点中保存了最新添加的 autoreleased 对象 objr3 的内存地址。

AutoreleasePool详解和runloop的关系_第2张图片

到这里大家可能有点疑问。没关系,我下面会为大家详细讲解,首先我来介绍几个概念:

  1. POOL_SENTINEL(哨兵对象)
    你可能想要知道 POOL_SENTINEL 到底是什么,还有它为什么在栈中。首先回答第一个问题: POOL_SENTINEL 只是 nil 的别名。

    #define POOL_SENTINEL nil

    在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。

    int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
        // do whatever you want
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
    }

    上面的 atautoreleasepoolobj 就是一个 POOL_SENTINEL。

  2. push 操作

objc_autoreleasePoolPush() 函数本质上就是调用的 AutoreleasePoolPage 的 push 函数。

 
void *objc_autoreleasePoolPush(void)
{
if (UseGC) return nil;
return AutoreleasePoolPage::push();
}

因此,我们接下来看看 AutoreleasePoolPage 的 push 函数的作用和执行过程。一个 push 操作其实就是创建一个新的 autoreleasepool ,对应 AutoreleasePoolPage 的具体实现就是往 AutoreleasePoolPage 中的 next 位置插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址,在执行 pop 操作的时候作为函数的入参。


static inline void *push()
{
    id *dest = autoreleaseFast(POOL_SENTINEL);
    assert(*dest == POOL_SENTINEL);
    return dest;
}

push 函数通过调用 autoreleaseFast 函数来执行具体的插入操作。重点内容

static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}

autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:

  • 当前 page 存在且没有满时,直接将对象添加到当前 page 中,即 next 指向的位置;
  • 当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中;
  • 当前 page 不存在时,即还没有 page 时,创建第一个 page ,并将对象添加到新创建的 page 中。

每调用一次 push 操作就会创建一个新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址。

3.autorelease 操作
通过 NSObject.mm 源文件,我们可以找到 -autorelease 方法的实现

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

通过查看 ((id)self)->rootAutorelease() 的方法调用,我们发现最终调用的就是 AutoreleasePoolPage 的 autorelease 函数。


id objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
} 

AutoreleasePoolPage 的 autorelease 函数的实现对我们来说就比较容量理解了,它跟 push 操作的实现非常相似。只不过 push 操作插入的是一个 POOL_SENTINEL ,而 autorelease 操作插入的是一个具体的 autoreleased 对象。


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

4.POP操作

同理,前面提到的 objc_autoreleasePoolPop(void *) 函数本质上也是调用的 AutoreleasePoolPage 的 pop 函数


void objc_autoreleasePoolPop(void *ctxt)
{
    if (UseGC) return;
    // fixme rdar://9167170
    if (!ctxt) return;
    AutoreleasePoolPage::pop(ctxt);
}

pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL 的内存地址,就是pool token 。当执行 pop 操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。直到 pool token 所在 page 的 next 指向 pool token 为止。

AutoreleasePool详解和runloop的关系_第3张图片

对照之前的图,其实整个过程可以总结为:每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),当需要release时,objc_autoreleasePoolPop(哨兵对象)作为入参,根据传入的哨兵对象地址找到哨兵对象所处的page。在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置

此时,如果执行 pop(token1) 操作,那么该 autoreleasepool 堆栈的内存结构将会变成如下图所示:
AutoreleasePool详解和runloop的关系_第4张图片

NSAutoreleasePool何时释放?

当别人问你NSAutoreleasePool何时释放?你回答“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

小实验。

- (void)viewDidLoad { 
[super viewDidLoad];
// 场景 1
NSString *string = [NSString stringWithFormat:@"1234567890"];
self.string_weak = string;
//场景 2
// @autoreleasepool {
// NSString *string = [NSString stringWithFormat:@"1234567890"];
// _string_weak = string;
// }
// NSLog(@"string: %@",_string_weak);
// 场景 3

// NSString *string = nil;
// @autoreleasepool {
// string = [NSString stringWithFormat:@”1234567890”];
//
// _string_weak = string;
// }
NSLog(@”string: %@”,self.string_weak);

}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@”string: %@”, self.string_weak);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@”string: %@”, self.string_weak);
}

思考得怎么样了?相信在你心中已经有答案了。那么让我们一起来看看 console 输出:

 //情景1
2016-10-27 20:29:06.069 AutoreleasePoolTest[974:138797] string: 1234567890
2016-10-27 20:29:06.070 AutoreleasePoolTest[974:138797] string: 1234567890
2016-10-27 20:29:06.076 AutoreleasePoolTest[974:138797] string: (null)
 //情景2
2016-10-27 20:31:58.836 AutoreleasePoolTest[1003:141350] string: (null)
2016-10-27 20:31:58.837 AutoreleasePoolTest[1003:141350] string: (null)
2016-10-27 20:31:58.844 AutoreleasePoolTest[1003:141350] string: (null)
 //情景3
2016-10-27 20:33:21.699 AutoreleasePoolTest[1014:142465] string: 1234567890
2016-10-27 20:33:21.699 AutoreleasePoolTest[1014:142465] string: (null)
2016-10-27 20:33:21.703 AutoreleasePoolTest[1014:142465] string: (null)
是不是和你所想一样?我们一起来分析下为什么会得到这样的结果。
分析1:当使用  [NSString stringWithFormat:@"1234567890"]创建一个对象时,这个对象的引用计数为 1 ,并且这个对象被系统自动添加到了当前的主线程的 autoreleasepool 中,autoreleasepool和strong同时拥有这个对象,所以引用计数为2。(如果这里直接输出  _string_weak的引用计数为3,因为 weak 的 retainCount 管理是独立的)。当程序执行完 - (void)viewDidLoad 时,string 释放,字符串对象引用计数-1。当执行- (void)viewWillAppear方法时,字符串对象引用计数为1。主线程的runloop一次迭代并没有结束,字符引用计数大于0,所以此时输出仍然有字符。而执行- (void)viewDidAppear时。runloop结束,autoreleasepool释放。字符串对象引用计数为0;所以此时字符串为空。
情景2:
当使用  [NSString stringWithFormat:@"1234567890"]创建一个对象时,string和autoreleasepool块同时持有,引用计数为2,当出来代码块后,string和autoreleasepool都会被释放。引用计数-2为0.字符串被释放,所以下面的输出全为空。
情景3:
申请字符串后,同时被string局部变量和代码块共有,引用计数为2,当出了autoreleasepool代码块的作用域时,字符串引用计数-1;由于string作用域为- (void)viewDidLoad 。所以第一次输出时字符串引用计数为1;当出了- (void)viewDidLoad 引用计数-1为0;此时字符串被释放。接下来两次输出都会为空。

如何使用Autorelease Pool Blocks

苹果文档介绍:很多程序会创造大量的临时对象,这些对象一直占用内存block的结束。大多情况下临时对象会一直在内存中聚集,直到当前的一次runloop迭代结束这样对内存造成很大的负担。苹果用了一段代码举例说明

NSArray *urls = <# An array of file URLs #>; 
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:url
encoding:NSUTF8StringEncoding error:&error];
/* Process the string, creating and autoreleasing more objects. */
}
}

每次循环代码块结束,里面的临时对象也会释放,这样就很好的解决了内存占用的问题。根据苹果官方文档中对 Using Autorelease Pool Blocks 的描述,我们知道在下面三种情况下是需要我们手动添加 autoreleasepool 的:

  1. 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
  2. 如果你编写的循环中创建了大量的临时对象;
  3. 如果你创建了一个辅助线程。

AutoreleasePool与runloop和线程的关系

根据苹果官方文档中对 NSRunLoop 的描述,我们可以知道每一个线程,包括主线程,都会拥有一个专属的 NSRunLoop 对象,并且会在有需要的时候自动创建。子线程的runloop需要自己手动创建,如果子线程的runloop没有任何事件,runloop会马上退出。(曾经我在写异步网络请求时想利用runloop来暂停线程来执行回调,但是由于没有添加任何事件源,导致runloop马上结束。网络请求失败)如果在每个 event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain 。我们上面提到的场景 1 中创建的 autoreleased 对象就是被系统添加到了这个自动创建的 autoreleasepool 中,并在这个 autoreleasepool 被 drain 时得到释放。另外,NSAutoreleasePool 中还提到,每一个线程都会维护自己的 autoreleasepool 堆栈。换句话说 autoreleasepool 是与线程紧密相关的,每一个 autoreleasepool 只对应一个线程。

本人第一次写博客。以后还会坚持下去。如有错误,欢迎大家来讨论。

相关链接:
Objective-C Autorelease Pool 的实现原理
自动释放池的前世今生 —- 深入解析 Autoreleasepool
官网文档
ARC的原理详解

你可能感兴趣的:(AutoreleasePool详解和runloop的关系)