Autorelease学习笔记

前言

本文收集了一些大神的博客,加上自己平时的一些理解,特此记录学习笔记。
retainrelease 是 MFC 模式开发下,每个初级 iOSer 的心中痛,多少次 bug 都是因为内存管理不当引起的。
Autorelease 有时是一个"神器",只要用[obj autorelease] ,很多 bug 迎刃而解。
Autorelease 背后的机制是什么?是如何管理内存的?编译器又做了哪些操作?

在苹果一些新的硬件设备上,autoreleasepool 和 runloop 的表现发生了变化,这一点需要详细查资料。

Autorelease对象什么时候释放?

表面上看答案是:“当前作用域大括号结束时释放”。

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

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

在主线程的 NSRunLoop 对象(在系统级别的其他线程中应该也是如此,比如通过 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 获取到的线程)的每个 event loop 开始前,系统会自动创建一个 autoreleasepool ,并在 event loop 结束时 drain

autoreleasepool 是与线程紧密相关的,每一个 autoreleasepool 只对应一个线程。

autoreleasepool

编译器会将 @autoreleasepool{} 改写成:

void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);

这两个函数都是对 AutoreleasePoolPage 的简单封装:

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

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

释放

每当进行一次 objc_autoreleasePoolPush 调用时,runtime向当前的 AutoreleasePoolPage 中add进一个哨兵对象,值为0(也就是个nil)

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

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

使用

Apple 官方文档 "Using Autorelease Pool Blocks" 中提到了三个场景需要使用 @autoreleasepool

three occasions when you might use your own autorelease pool blocks:
If you are writing a program that is not based on a UI framework, such as a command-line tool.
If you write a loop that creates many temporary objects.
If you spawn a secondary thread

  1. 写非UI框架项目时
  2. 在一个循环中创建大量的临时对象
  3. 大量使用辅助线程

所以,如下代码会产生很大的内存占用,甚至导致内存警告。

for (int i=0; i<100000; i++) {
    UIImage *image = [UIImage imageNamed:@"pic"];
}

在for循环中大量创建临时变量时,需要用@autoreleasepool来优化:

for (int i=0;i<100000;i++) {
    @autoreleasepool {
        UIImage *image = [UIImage imageNamed:@"pic"];
    }
}

特别

使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 这里被一个局部@autoreleasepool包围着
}];

参考

  1. 黑幕背后的Autorelease - sunnyxx
  2. Objective-C Autorelease Pool 的实现原理
  3. Using Autorelease Pool Blocks

你可能感兴趣的:(Autorelease学习笔记)