聊聊iOS autoreleasepool里的数据结构

概述

这篇博客的主要目的是解析autoreleasepool的具体实现,因为底层的实现其实都是基于我们的数据结构的,最终我们的目的就是学习这些之前学习到的数据结构是怎么用于实际项目当中的,而不是仅仅限于书本的知识。该篇文章内容是参考iOS - 聊聊 autorelease 和 @autoreleasepool。有不对或者说的不够全面的点,欢迎留言大家补充。

一、iOS中的内存管理技术

内存管理是程序员在写bug的过程中必然会涉及到的主题,比如Java的垃圾回收机制,C++基于栈和析构函数的RAII(Resource Acquisition Is Initialization)。在iOS中,使用的是引用计数的技术(Reference count)。

在iOS的不断发展过程中,又分为手动引用计数(MRC)和自动引用计数(ARC)。ARC在iOS 5之后引入,通过LLVM编译器和Runtime协作来进行自动管理内存。LLVM编译器会在编译时在合适的地方为 OC 对象插入retainreleaseautorelease代码,省去了在MRC(Manual Reference Counting)手动引用计数下手动插入这些代码的工作,减轻了开发者的工作量。Reference.

注:retain可以简单理解为引用计数加一,releaseautorelease是使引用计数减一。

在MRC下,当我们不需要一个对象的时候,要调用releaseautorelease方法来释放它。调用release会立即让对象的引用计数减 1 ,如果此时对象的引用计数为 0,对象就会被销毁。调用autorelease会将该对象添加进自动释放池(autoreleasepool)中,它会在一个恰当的时刻自动给对象调用release,所以autorelease相当于延迟了对象的释放。
在ARC下,autorelease方法已被禁用,我们可以使用__autoreleasing修饰符修饰对象将对象注册到自动释放池中。详情请参阅《iOS - 老生常谈内存管理(三):ARC 面世 —— 所有权修饰符》

二、autoReleasePool 自动释放池

2.1 autoReleasePool定义

截取两段Apple官方的定义是:

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. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.

从以上定义的两个片段可以得到:最开始的时候,主线程会创健一个autoReleasePool,这是默认的autoReleasePool,但是每个thread都会维护自己的autoReleasePoolautoReleasePool也是和runloop一一对应的。线程的 observer 观察到 RunLoop 即将开始和休眠/结束,会调用 autoreleasePoolpushpop进行相关的操作。

2.2 创建autoReleasePool

  • 在MRC下,可以使用NSAutoreleasePool或者@autoreleasepool。建议使用@autoreleasepool,苹果说它比NSAutoreleasePool快大约六倍。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release];
  • 在ARC下,已经禁止使用NSAutoreleasePool类创建自动释放池,只能使用@autoreleasepool
@autoreleasepool {
    // Code benefitting from a local autorelease pool.
}

三、autoReleasePool 原理分析

3.1 总体概述

在xcode中新建一个项目,选择oc语言,然后直接进入到改项目的源文件目录下,然后使用

xcrun -sdk iphonesimulator clang -rewrite-objc main.m // 对于包含了apple头文件的代码,需要加入前缀xcrun -sdk iphonesimulator来指定边缘的平台。

将默认生成的main.m文件转换为main.cpp

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

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

我们先不管其他地方的代码,只截取其中关于autoreleasepool的一部分代码:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
    }
    return 0;
}

从以上的cpp代码可以看出:

  • @autoreleasepool底层是创建了一个__AtAutoreleasePool结构体对象;
  • 在创建__AtAutoreleasePool结构体时会在构造函数中调用objc_autoreleasePoolPush()函数,并返回一个atautoreleasepoolobj(POOL_BOUNDARY存放的内存地址,下面会讲到);
  • 在释放__AtAutoreleasePool结构体时会在析构函数中调用objc_autoreleasePoolPop()函数,并将atautoreleasepoolobj传入。

objc_autoreleasePoolPush()和objc_autoreleasePoolPop()两个函数其实是调用了AutoreleasePoolPage类的两个类方法push()和pop()。所以@autoreleasepool底层就是使用AutoreleasePoolPage类来实现的。
AutoreleasePoolPage的定义如下,就是一个双向链表节点的定义:

class AutoreleasePoolPage 
{
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)  // EMPTY_POOL_PLACEHOLDER:表示一个空自动释放池的占位符
#   define POOL_BOUNDARY nil                // POOL_BOUNDARY:哨兵对象
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;   // 用来标记已释放的对象
    static size_t const SIZE =              // 每个 Page 对象占用 4096 个字节内存
#if PROTECT_AUTORELEASEPOOL                 // PAGE_MAX_SIZE = 4096
        PAGE_MAX_SIZE;  // must be muliple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);  // Page 的个数

    magic_t const magic;                // 用来校验 Page 的结构是否完整
    id *next;                           // 指向下一个可存放 autorelease 对象地址的位置,初始化指向 begin()
    pthread_t const thread;             // 指向当前线程
    AutoreleasePoolPage * const parent; // 指向父结点,首结点的 parent 为 nil
    AutoreleasePoolPage *child;         // 指向子结点,尾结点的 child  为 nil
    uint32_t const depth;               // Page 的深度,从 0 开始递增
    uint32_t hiwat;
    ......
}

整个程序运行过程中,可能会有多个AutoreleasePoolPage对象。从它的定义可以得知:

  • 自动释放池(即所有的AutoreleasePoolPage对象)是通过双向链表的形式组合在一起,内部的对象地址是按照栈的方式存储;
  • 自动释放池与线程一一对应;
  • 每个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放它内部的成员变量,剩下的空间(4040个字节)用来存放autorelease对象的地址。

3.2 AutoreleasePoolPage

AutoreleasePoolPage之前说过其实就是双向链表的一个节点,那么这个节点的构造函数是如何的呢,我们来看一下源码:

    AutoreleasePoolPage(AutoreleasePoolPage *newParent) 
        : magic(), next(begin()), thread(pthread_self()),
          parent(newParent), child(nil), 
          depth(parent ? 1+parent->depth : 0), 
          hiwat(parent ? parent->hiwat : 0)
    { 
        if (parent) {
            parent->check();
            assert(!parent->child);
            parent->unprotect();
            parent->child = this;
            parent->protect();
        }
        protect();
    }

从以上构造函数可以看出,这就是一个双向链表节点的构造(如下图所示):

  • AutoreleasePoolPage()方法的参数为parentPage
  • next指针指向该page中可以存储对象地址的开始位置
  • child指针为nil
  • 新创建的Page的depth加一
  • 将新创建的Page的parent指针指向parentPage
  • 将parentPage的child指针指向自己。
    autoreleasepool

3.2 POOL_BOUNDARY

  • POOL_BOUNDARY的前世叫做POOL_SENTINEL,称为哨兵对象或者边界对象;
  • POOL_BOUNDARY用来区分不同的自动释放池,以解决自动释放池嵌套的问题;
  • 每当创建一个自动释放池,就会调用push()方法将一个POOL_BOUNDARY入栈,并返回其存放的内存地址;
  • 当往自动释放池中添加autorelease对象时,将autorelease对象的内存地址入栈,它们前面至少有一个POOL_BOUNDARY
  • 当销毁一个自动释放池时,会调用pop()方法并传入一个POOL_BOUNDARY,会从自动释放池中最后一个对象开始,依次给它们发送release消息,直到遇到这个POOL_BOUNDARY

3.3 autoReleasePool的操作

3.3.1 push
    static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) { // 出错时进入调试状态
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);  // 传入 POOL_BOUNDARY 哨兵对象
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

从代码中看出,当创建一个自动释放池时,会调用push()方法。push()方法中调用了autoreleaseFast()方法并传入了POOL_BOUNDARY哨兵对象。

    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();     // 双向链表中的最后一个 Page
        if (page && !page->full()) {        // 如果当前 Page 存在且未满
            return page->add(obj);                 // 将 autorelease 对象入栈,即添加到当前 Page 中;
        } else if (page) {                  // 如果当前 Page 存在但已满
            return autoreleaseFullPage(obj, page); // 创建一个新的 Page,并将 autorelease 对象添加进去
        } else {                            // 如果当前 Page 不存在,即还没创建过 Page
            return autoreleaseNoPage(obj);         // 创建第一个 Page,并将 autorelease 对象添加进去
        }
    }

autoreleaseFast()中先是调用了hotPage()方法获得未满的Page,从AutoreleasePoolPage类的定义可知,每个Page的内存大小为4096个字节,每当Page满了的时候,就会创建一个新的Page。hotPage()方法就是用来获得这个新创建的未满的Page。
autoreleaseFast()在执行过程中有三种情况:

  • ① 当前Page存在且未满时,通过page->add(obj)autorelease对象添加到Page中的next指针所指向的位置,并将next指针指向这个对象的下一个位置,然后将该对象的位置返回。
  • ② 当前Page存在但已满时,通过autoreleaseFullPage(obj, page)创建一个新的Page,并将autorelease对象添加进去;
  • ③ 当前Page不存在,通过autoreleaseNoPage(obj)创建第一个Page,并将autorelease对象添加进去。该方法会判断是否有空的自动释放池存在,如果没有会通过setEmptyPoolPlaceholder()生成一个占位符,表示一个空的自动释放池。接着创建第一个Page,设置它为hotPage。最后将一个POOL_BOUNDARY添加进Page中,并返回POOL_BOUNDARY的下一个位置。

小节总结:以上就是push操作的实现,往自动释放池中添加一个POOL_BOUNDARY,并返回它存放的内存地址。接着每有一个对象调用autorelease方法,会将它的内存地址添加进自动释放池中。

3.3.2 pop

pop()方法的传参token即为POOL_BOUNDARY对应在Page中的地址。当销毁自动释放池时,会调用pop()方法将自动释放池中的autorelease对象全部释放(实际上是从自动释放池的中的最后一个入栈的autorelease对象开始,依次给它们发送一条release消息,直到遇到这个POOL_BOUNDARY)。pop()方法的执行过程如下:

  • ① 判断token是不是EMPTY_POOL_PLACEHOLDER,是的话就清空这个自动释放池;
  • ② 如果不是的话,就通过pageForPointer(token)拿到token所在的Page(自动释放池的首个Page);
  • ③ 通过page->releaseUntil(stop)将自动释放池中的autorelease对象全部释放,传参stop即为POOL_BOUNDARY的地址;
  • ④ 判断当前Page是否有子Page,有的话就销毁。
3.3.3 autoRelease

可以看到,调用了autorelease方法的对象,也是通过以上解析的autoreleaseFast()方法添加进Page中。

    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;
    }
3.4 总结
  • push操作是往自动释放池中添加一个POOL_BOUNDARY,并返回它存放的内存地址;
  • 接着每有一个对象调用autorelease方法,会将它的内存地址添加进自动释放池中。
  • pop操作是传入一个POOL_BOUNDARY的内存地址,从最后一个入栈的autorelease对象开始,将自动释放池中的autorelease对象全部释放(实际上是给它们发送一条release消息),直到遇到这个POOL_BOUNDARY

四、RunLoop 与 @autoreleasepool

学习这个知识点之前,需要先搞懂RunLoop的事件循环机制以及它的6种活动状态,参考文章:
《深入浅出 RunLoop(二):数据结构》
《深入浅出 RunLoop(三):事件循环机制》

iOS在主线程的RunLoop中注册了两个Observer:

  • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush();
  • 第2个Observer:
    ① 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush();
    ② 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()。
    runloop和autorelease 关系

五、Q&A

Q:释放NSAutoreleasePool对象,使用[pool release][pool drain]的区别?
A: Objective-C 语言本身是支持 GC 机制的,但有平台局限性,仅限于 MacOS 开发中,iOS 开发用的是 RC 机制。在 iOS 的 RC 环境下[pool release]和[pool drain]效果一样,但在 GC 环境下drain会触发 GC 而release不做任何操作。使用[pool drain]更佳,一是它的功能对系统兼容性更强,二是这样可以跟普通对象的release区别开。(注意:苹果在引入ARC时称,已在 OS X Mountain Lion v10.8 中弃用GC机制,而使用ARC替代)

Q: 子线程的autoReleasePool和主线程有什么关系呢?
A: 子线程默认不自动创建pool,不过子线程一些临时对象如果显式调用了autorelease方法,内部会创建pool并把对象加入pool的(pool存储在对应线程的私有存储区里),不过pool里的对象释放时机却是等到线程退出才进行清理;所以线程内部建议我们手动@autoreleasepool{}这样释放时机控制在作用域内。

Q: ARC情况下,什么时候需要手动添加autoReleasePool?
A: 苹果给出了三种需要手动添加@autoreleasepool的情况:

  • 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
  • 如果你编写的循环中创建了大量的临时对象;
    你可以在循环内使用@autoreleasepool在下一次迭代之前处理这些对象。在循环中使用@autoreleasepool有助于减少应用程序的最大内存占用。
  • 如果你创建了辅助线程。
    一旦线程开始执行,就必须创建自己的@autoreleasepool;否则,你的应用程序将存在内存泄漏。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值