iOS基础 - autorelease 和 autoreleasepool

Posted 你的微笑依然那样灿烂

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS基础 - autorelease 和 autoreleasepool相关的知识,希望对你有一定的参考价值。

内存管理一直是学习 Objective-C 的重点和难点之一,尽管现在已经是 ARC 时代了,但是了解 Objective-C 的内存管理机制仍然是十分必要的。其中,弄清楚 autorelease 的原理更是重中之重,只有理解了 autorelease 的原理,我们才算是真正了解了 Objective-C 的内存管理机制。

1.autorelease /autoreleasepool 基本用法及其优点

对象执行autorelease方法 或者直接在autoreleasepool中创建对象 ,会将对象添加到当前的autorelease pool中, 当自动释放池销毁时 自动释放池中 所有对象 作release操作。

2. autoreleasepool 什么时候释放

这个问题拿来做面试题,问很多人,很少能答对。很多答案都是“当前作用域大括号结束时释放”,显然没有正确理解Autorelease机制。

在没有手动加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

runloop.gif

程序运行 -> 开启事件循环 -> 发生触摸事件 -> 创建自动释放池 -> 处理触摸事件 -> 事件对象加入自动释放池 -> 一次事件循环结束, 销毁自动释放池.

苹果官方文档 :
The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event

在开始每一个事件循环之前系统会在主线程创建一个自动释放池, 并且在事件循环结束的时候把前面创建的释放池释放, 回收内存。

当然也有让autorelease提前生效的办法:自己创建Pool并进行释放

NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSArray * array = [[NSArray alloc] init] ;
[pool drain];

上面的array就会在[pool drain]执行时被释放。不过只能是在MRC下。

autorelease pool 栈

每一个线程(包括主线程)都有一个NSAutoreleasePool栈. 当一个新的池子被创建的时候, push进栈. 当池子被释放内存时, pop出栈. 对象调用autorelease方法进入栈顶的池子中. 当线程结束的时候, 它会自动地销毁掉所有跟它有关联的池子.

线程中的自动释放池栈.png

3.autoreleasepool 有什么作用

我们对@autoreleasepool 并不陌生,只要在Xcode里创建一个工程,就能看到下面这样的代码:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) 
    @autoreleasepool 
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    


main 函数的主体都被 @autoreleasepool 的Block块包在里面,也就是说,接下来所有的对象创建都在这个block里面。

那么, @autoreleasepool 的作用到底是什么呢?我们开发中可以用它来做什么呢?

可以在某些情况下,大幅度降低程序的内存占用,如下图:

autoreleasepool test.png

测试的内容:500000次循环,每次循环创建一个NSNumber实例和两个NSString实例。
图:红线表示没有用 @autoreleasepool 时的内存占用。
图:绿线表示用了 @autoreleasepool 优化后的内存占用!
效果是不是很明显!

代码Github地址: AutoReleasePoolTestExample Xcode 6, ios 8, iPhone 5模拟器.

4.什么时候用@autoreleasepool

根据 Apple的文档 ,使用场景如下:

  • 写基于命令行的的程序时,就是没有UI框架,如AppKit等Cocoa框架时。
  • 写循环,循环里面包含了大量临时创建的对象。(本文的例子)
  • 创建了新的线程。(非Cocoa程序创建线程时才需要)
  • 长时间在后台运行的任务。

个人感觉使用@autoreleasepool优化循环非常有用

//Apple 官方文档的示例代码
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. */
    

5.autoreleasepool 的是如何实现的

AutoreleasePoolPage

[NSAutoreleasePool release] 方法最终是通过调用 AutoreleasePoolPage::pop(void *) 函数来负责对 autoreleasepool 中的 autoreleased 对象执行 release 操作的。

那这里的 AutoreleasePoolPage 是什么东西呢?其实,autoreleasepool 是没有单独的内存结构的,它是通过以 AutoreleasePoolPage 为结点的双向链表来实现的。我们打开 runtime 的源码工程,在 NSObject.mm 文件的第 438-932 行可以找到 autoreleasepool 的实现源码。通过阅读源码,我们可以知道:

  • 每一个线程的 autoreleasepool 其实就是一个指针的堆栈;
  • 每一个指针代表一个需要 release 的对象或者 POOL_SENTINEL(哨兵- 对象,代表一个 autoreleasepool 的边界);
  • 一个 pool token 就是这个 pool 所对应的 POOL_SENTINEL 的内存地址。当这个 pool 被 pop 的时候,所有内存地址在 pool token 之后的对象都会被 release ;
  • 这个堆栈被划分成了一个以 page 为结点的双向链表。pages 会在必要的时候动态地增加或删除;
  • Thread-local storage(线程局部存储)指向 hot page ,即最新添加的 autoreleased 对象所在的那个 page 。

一个空的 AutoreleasePoolPage 的内存结构如下图所示:

autoreleasePoolPage 内存结构.png

  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 。

另外,当 next == begin() 时,表示 AutoreleasePoolPage 为空;当 next == end() 时,表示 AutoreleasePoolPage 已满。

Autorelease Pool Blocks

我们使用 clang -rewrite-objc 命令将下面的 Objective-C 代码重写成 C++ 代码:

@autoreleasepool 

将会得到以下输出结果(只保留了相关代码):

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool 
  __AtAutoreleasePool() atautoreleasepoolobj = objc_autoreleasePoolPush();
  ~__AtAutoreleasePool() objc_autoreleasePoolPop(atautoreleasepoolobj);
  void * atautoreleasepoolobj;
;
/* @autoreleasepool */  __AtAutoreleasePool __autoreleasepool;

不得不说,苹果对 @autoreleasepool 的实现真的是非常巧妙,真正可以称得上是代码的艺术。苹果通过声明一个 __AtAutoreleasePool 类型的局部变量 __autoreleasepool 来实现 @autoreleasepool 。当声明 __autoreleasepool 变量时,构造函数 __AtAutoreleasePool() 被调用,即执行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;当出了当前作用域时,析构函数 ~__AtAutoreleasePool() 被调用,即执行 objc_autoreleasePoolPop(atautoreleasepoolobj); 。也就是说 @autoreleasepool 的实现代码可以进一步简化如下:

/* @autoreleasepool */ 
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    // 用户代码,所有接收到 autorelease 消息的对象会被添加到这个 autoreleasepool 中
    objc_autoreleasePoolPop(atautoreleasepoolobj);

因此,单个 autoreleasepool 的运行过程可以简单地理解为 objc_autoreleasePoolPush()、[对象 autorelease] 和 objc_autoreleasePoolPop(void *) 三个过程。

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 的内存地址。这个地址也就是我们前面提到的 pool token ,在执行 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 的内存地址。

autorelease 操作

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

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

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

__attribute__((noinline,used))
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;

pop 操作

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

![Uploading 1_188894.jpg . . .]

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 堆栈的内存结构图,在这个 autoreleasepool 堆栈中总共有两个 POOL_SENTINEL ,即有两个 autoreleasepool 。该堆栈由三个 AutoreleasePoolPage 结点组成,第一个 AutoreleasePoolPage 结点为 coldPage() ,最后一个 AutoreleasePoolPage 结点为 hotPage() 。其中,前两个结点已经满了,最后一个结点中保存了最新添加的 autoreleased 对象 objr3 的内存地址。

1.jpg

此时,如果执行 pop(token1) 操作,那么该 autoreleasepool 堆栈的内存结构将会变成如下图所示:

 

2.jpg

NSThread、NSRunLoop 和 NSAutoreleasePool

根据苹果官方文档中对 NSRunLoop 的描述,我们可以知道每一个线程,包括主线程,都会拥有一个专属的 NSRunLoop 对象,并且会在有需要的时候自动创建。

Each NSThread object, including the application’s main thread, has an NSRunLoop object automatically created for it as needed.

同样的,根据苹果官方文档中对 NSAutoreleasePool 的描述,我们可知,在主线程的 NSRunLoop 对象(在系统级别的其他线程中应该也是如此,比如通过 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 获取到的线程)的每个 event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain 。

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

另外,NSAutoreleasePool 中还提到,每一个线程都会维护自己的 autoreleasepool 堆栈。换句话说 autoreleasepool 是与线程紧密相关的,每一个 autoreleasepool 只对应一个线程。

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.

弄清楚 NSThread、NSRunLoop 和 NSAutoreleasePool 三者之间的关系可以帮助我们从整体上了解 Objective-C 的内存管理机制,清楚系统在背后到底为我们做了些什么,理解整个运行机制等。

参考文案:
黑幕背后的Autorelease 
@autoreleasepool-内存的分配与释放

以上是关于iOS基础 - autorelease 和 autoreleasepool的主要内容,如果未能解决你的问题,请参考以下文章

iOS 基础知识整理(不间断更新)

iOS 过早释放对象 - 可能与 Autorelease 和 Copy 混淆

下面的 iOS 代码安全吗? (__autoreleasing 语义)

iOS之内存管理(ARC)

75. Autorelease机制及释放时机

ios 不兼容的指针类型从 NSError _autoreleasing 分配给 NSError _strong