iOS面试之AutoreleasePool

原文连接git

AutoreleasePool对于iOS开发者来讲,能够说是"熟悉的陌生人"。熟悉是由于每一个iOS程序都被包围在一个autoreleasepool中,陌生是由于整个autoreleasepool是黑盒的,开发者看不到autoreleasepool中发生了什么,并且项目开发中直接用到autoreleasepool的地方很少。本文结合Runtime源码,分析一下AutoreleasePool的内部实现。github

iOS程序入口

咱们都知道,iOS程序的入口是main.m文件中的main方法。在Xcode中新建一个iOS项目,Xcode会自动生成main.m文件。main.m文件中只有一个main方法,绝大多数状况下,不须要修改main.m中的代码。面试

一个典型的main函数:bash

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
复制代码

能够看到,main函数的函数体是包含在一个autoreleasepool中的。惋惜的是,经过command + 鼠标左键,并不能看到autoreleasepool的定义。不过咱们可使用clang,将main.m文件编译成C++代码,看看autoreleasepool发生了什么。数据结构

使用命令:函数

clang -rewrite-objc main.m
复制代码

生成main.cpp文件。oop

生成的main.cpp文件很大,大概有10w行,不须要关注文件到底有多少行。将文件拖到最下面,看一下main函数变成了什么:ui

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

    }
}
复制代码

整个函数的函数体被包围在了__AtAutoreleasePoool __autoreleasepool中。并且前面有关于@autoreleasepool的注释,所以能够猜想autoreleasepool被表示成了__AtAutoreleasePool。this

在main.cpp中搜索一下,看看__AtAutoreleasePool是什么。spa

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};
复制代码

__AtAutoreleasePool是一个结构体,结构体中包含构造函数和析构函数。构造函数中调用了

atautoreleasepoolobj = objc_autoreleasePoolPush();
复制代码

析构函数中调用了

objc_autoreleasePoolPop(atautoreleasepoolobj);
复制代码

因而,关注的重点就成了objc_autoreleasePoolPush和objc_autoreleasePoolPop函数。

objc_autoreleasePoolPush和objc_autoreleasePoolPop函数在Runtime源码中能够找到,位于NSObject.mm文件中。

AutoreleasePoolPage

看一下Runtime源码中objc_autoreleasePoolPush和objc_autoreleasePoolPop函数的实现。

void * objc_autoreleasePoolPush(void)
{
    // 调用了AutoreleasePoolPage中的push方法
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt)
{
    // 调用了AutoreleasePoolPage中的pop方法
    AutoreleasePoolPage::pop(ctxt);
}
复制代码

经过源码能够看到分别调用了AutoreleasePoolPage的push方法和pop方法。

AutoreleasePoolPage的定义

AutoreleasePoolPage的定义位于NSObject.mm文件中:

// AutoreleasePoolPage的大小是4096字节
class AutoreleasePoolPage 
{
# define EMPTY_POOL_PLACEHOLDER ((id*)1)

    // 哨兵对象
# define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    // AutoreleasePoolPage的大小,经过宏定义,能够看到是4096字节
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    // 一个AutoreleasePoolPage中会存储多个对象
    // next指向的是下一个AutoreleasePoolPage中下一个为空的内存地址(新来的对象会存储到next处)
    id *next;
    // 保存了当前页所在的线程(一个AutoreleasePoolPage属于一个线程,一个线程中能够有多个AutoreleasePoolPage)
    pthread_t const thread;
    // AutoreleasePoolPage是以双向链表的形式链接
    // 前一个节点
    AutoreleasePoolPage * const parent;
    // 后一个节点
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}
复制代码

除此以外,还定义了不少方法,方法的做用及实现下面会分析。

在上面的定义中,我已经加了些注释。经过注释能够获得:

  1. 一个AutoreleasePoolPage的大小是4096字节(和操做系统中一页的大小一致)。
  2. parent指针和child指针特别有意思,指向的一样是AutoreleasePoolPage,若是对数据结构比较熟悉的话,看到相似的定义,应该能够联想到双向链表或者树结构。实际上也正是如此,下面咱们会提到AutoreleasePoolPage组成的双向链表。
  3. thread表示当前AutoreleasePoolPage所属的线程。
  4. next指针指向了下一个空的地址。一个AutoreleasePoolPage中能够存储多个对象地址,新来的对象地址会存放到next处,而后next移动到下一个地址。这样的操做有没有联想到哪一种数据结构?是否是和栈的top指针特别相似?

对AutoreleasePoolPage的定义有了基本的了解以后,来看一下push方法和pop方法。

AutoreleasePoolPage::push方法

AutoreleasePoolPage中的push方法,通过简化以后以下:

static inline void *push() 
{
    id *dest;
    // POOL_BOUNDARY其实就是nil
    dest = autoreleaseFast(POOL_BOUNDARY);
    return dest;
}
复制代码

push方法中主要调用了autoreleaseFast方法,所传入的参数是POOL_BOUNDARY,也就是nil。看你一下autoreleaseFast方法的实现。

static inline id *autoreleaseFast(id obj)
{
    // hotPage就是当前正在使用的AutoreleasePoolPage
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        // 有hotPage且hotPage不满,将对象添加到hotPage中
        return page->add(obj);
    } else if (page) {
        // 有hotPage可是hotPage已满
        // 使用autoreleaseFullPage初始化一个新页,并将对象添加到新的AutoreleasePoolPage中
        return autoreleaseFullPage(obj, page);
    } else {
        // 无hotPage
        // 使用autoreleaseNoPage建立一个hotPage,并将对象添加到新建立的page中
        return autoreleaseNoPage(obj);
    }
}
复制代码

我在代码中已经加入了注释,再来看一下里面涉及到的一些方法。

hotPage方法
// 获取正在使用的AutoreleasePoolPage
static inline AutoreleasePoolPage *hotPage() 
{
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
    if (result) result->fastcheck();
    return result;
}
复制代码

hotPage能够理解成当前正在使用的page。上面也提到了,AutoreleasePoolPage中有parent和child指针,实际上AutoreleasePool就是由一个个AutoreleasePoolPage组成的双向链表。这里获得的hotPage能够理解成链表最末尾的结点。

获取hotPage的方法是tls_get_direct(key),key是AutoreleasePoolPage结构中定义的

static pthread_key_t const key = AUTORELEASE_POOL_KEY;
复制代码
setHotPage方法
static inline void setHotPage(AutoreleasePoolPage *page) 
{
    if (page) page->fastcheck();
    tls_set_direct(key, (void *)page);
}
复制代码

将某个page设置成hotPage。

full方法
// 是否已满
bool full() { 
    return next == end();
}
复制代码

判断当前的AutoreleasePoolPage是否已满。判断标准是next等于AutoreleasePoolPage的尾地址。上面已经提到了,AutoreleasePoolPage的大小是4096字节,既然大小是固定的,那么确定有满的一刻,full方法就是用来作这个得。

add方法
// 将对象添加到AutoreleasePoolPage中
id *add(id obj)
{
    id *ret = next;  // faster than `return next-1` because of aliasing
    // next = obj; next++;
    // 也就是将obj存放在next处,并将next指向下一个位置
    *next++ = obj;
    return ret;
}
复制代码

add方法所作的操做也比较简单,就是将当前对象存放在next指向的位置,而且将next指向下一个位置。能够理解成一个栈,next指针相似于栈的top指针。

autoreleaseFullPage方法
// 新建一个AutoreleasePoolPage,并将obj添加到新的AutoreleasePoolPage中
// 参数page是新AutoreleasePoolPage的父节点
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    do {
        // 若是page->child存在,那么使用page->child
        if (page->child) page = page->child;
        // 不然的话,初始化一个新的AutoreleasePoolPage
        else page = new AutoreleasePoolPage(page);
    } while (page->full());
    // 将找到的合适的page设置成hotPage
    setHotPage(page);
    // 将对象添加到hotPage中
    return page->add(obj);
}
复制代码

autoreleaseFullPage所作的操做有三步:

  1. 首先找到一个合适的AutoreleasePoolPage,这里合适的page指的是不满的page。具体找的过程是从传过来的参数page的child开始找,若是page->child存在,则判断page->child是不是合适的page;若是page->child不存在,则初始化一个新的AutoreleasePoolPage,这里使用的是AutoreleasePoolPage的构造函数,传入的page是新的AutoreleasePoolPage的父节点。
  2. 将找到的AutoreleasePoolPage对象设置成hotPage
  3. 调用add方法,将对象添加到找到的page中
autoreleaseNoPage方法
// AutoreleasePool中尚未AutoreleasePoolPage
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    // 初始化一个AutoreleasePoolPage
    // 当前内存中不存在AutoreleasePoolPage,则从头开始构建AutoreleasePoolPage,也就是其parent为nil
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    // 将初始化的AutoreleasePoolPage设置成hotPage
    setHotPage(page);
    
    // Push a boundary on behalf of the previously-placeholder'd pool. // 添加一个边界对象(nil) if (pushExtraBoundary) { page->add(POOL_BOUNDARY); } // Push the requested object or pool. // 将对象添加到AutoreleasePoolPage中 return page->add(obj); } 复制代码

autoreleaseNoPage方法处理的是当前autoreleasePool中尚未autoreleasePoolPage的状况。既然没有,须要新建一个AutoreleasePoolPage,且该page的父指针指向空,而后将该page设置成hotPage。以后向该page中先是添加了POOL_BOUNDARY,而后在把对象obj添加到page中。

关于为何须要添加POOL_BOUNDARY的缘由,后面会说到。

如今已经把autoreleaseFast方法中涉及到的方法都弄明白了,再来看一下autoreleaseFast方法作的操做。

static inline id *autoreleaseFast(id obj)
{
    // hotPage就是当前正在使用的AutoreleasePoolPage
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        // 有hotPage且hotPage不满,将对象添加到hotPage中
        return page->add(obj);
    } else if (page) {
        // 有hotPage可是hotPage已满
        // 使用autoreleaseFullPage初始化一个新页,并将对象添加到新的AutoreleasePoolPage中
        return autoreleaseFullPage(obj, page);
    } else {
        // 无hotPage
        // 使用autoreleaseNoPage建立一个hotPage,并将对象添加到新建立的page中
        return autoreleaseNoPage(obj);
    }
}
复制代码

autoreleaseFast方法首先找到hotPage,也就是当前的page,以后分为三种状况:

  1. 若是hotPage存在,且hotPage还不满,则将对象添加到hotPage中
  2. 若是hotPage存在,可是hotPage已满,则调用autoreleaseFullPage方法 autoreleaseFullPage方法上面已经说了,作的操做就是从page开始找,找到一个不满的page,将找到的page设置成hotPage,而且将对象添加到找到的page中。
  3. 若是hotPage不存在,则调用autoreleaseNoPage方法 autoreleaseNoPage方法上面说了,作的操做就是新建一个AutoreleasePoolPage,而且将对象添加到新建的AutoreleasePoolPage中。

至此,AutoreleasePoolPage的push方法介绍完毕。

AutoreleasePoolPage::pop方法

AutoreleasePoolPage::pop方法的代码通过简化以后以下:

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;
    page = pageForPointer(token);
    stop = (id *)token;
    page->releaseUntil(stop);
}
复制代码

同理,仍是先看一下里面调用到的方法的实现。不过,在介绍pop内部调用的方法以前,先来看一下pop方法中的参数究竟是什么。

在文章开始处,咱们是从clang重写以后的main.cpp文件引入到Runtime源码的,如今再回过去看一下main.cpp文件中的__AtAutoreleasePool结构体:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};
复制代码

objc_autoreleasePoolPop中的参数是atautoreleasepoolobj,而atautoreleasepoolobj是objc_autoreleasePoolPush方法返回的。也就是说,AutoreleasePoolPage中pop方法的参数是AutoreleasePoolPage中push方法返回的,比较拗口,能够多理解一下。那么,AutoreleasePoolPage中push方法返回的是什么呢?

上面已经介绍过push方法了,push方法内部分为了三种状况,不管哪一种状况,最终都调用了add方法,而且返回了add方法的返回值。add方法的实现以下:

id *add(id obj)
{
    id *ret = next;  // faster than `return next-1` because of aliasing
    // next = obj; next++;
    // 也就是将obj存放在next处,并将next指向下一个位置
    *next++ = obj;
    return ret;
}
复制代码

add方法返回的就是所要添加对象在AutoreleasePoolPage中的地址。

而在push方法中,添加的对象是哨兵对象POOL_BOUNDARY,因此,在pop方法中,参数也是哨兵对象POOL_BOUNDARY。

pageForPointer方法

pageForPointer的代码以下:

static AutoreleasePoolPage *pageForPointer(const void *p) 
{
    // 调用了pageForPointer方法
    return pageForPointer((uintptr_t)p);
}

// 根据内存地址,获取指针所在的AutoreleasePage的首地址
static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    // 偏移量
    uintptr_t offset = p % SIZE;
    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();
    return result;
}
复制代码

pageForPointer方法的做用是根据指针位置,找到该指针位于哪一个AutoreleasePoolPage,并返回找到的AutoreleasePoolPage(以前已经提到了,AutoreleasePool是由一个个AutoreleasePoolPage组成的双向链表,不止一个AutoreleasePoolPage)。

releaseUntil方法

releaseUntil方法的代码以下:

// 释放对象
// 这里须要注意的是,由于AutoreleasePool实际上就是由AutoreleasePoolPage组成的双向链表
// 所以,*stop可能不是在最新的AutoreleasePoolPage中,也就是下面的hotPage,这时须要从hotPage
// 开始,一直释放,直到stop,中间所通过的全部AutoreleasePoolPage,里面的对象都要释放
void releaseUntil(id *stop) 
{
    // 释放AutoreleasePoolPage中的对象,直到next指向stop
    while (this->next != stop) {
        // hotPage能够理解为当前正在使用的page
        AutoreleasePoolPage *page = hotPage();

        // fixme I think this `while` can be `if`, but I can't prove it // 若是page为空的话,将page指向上一个page // 注释写到猜想这里可使用if,我感受也可使用if // 由于根据AutoreleasePoolPage的结构,理论上不可能存在连续两个page都为空 while (page->empty()) { page = page->parent; setHotPage(page); } // obj = page->next; page->next--; id obj = *--page->next; memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); // POOL_BOUNDARY为nil,是哨兵对象 if (obj != POOL_BOUNDARY) { // 释放obj对象 objc_release(obj); } } // 从新设置hotPage setHotPage(this); } 复制代码

代码中我已经加了注释,releaseUntil作的操做就是持续释放AutoreleasePoolPage中的对象,直到next = stop。

回过头来再来看一下pop方法:

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;
    page = pageForPointer(token);
    stop = (id *)token;
    page->releaseUntil(stop);
}
复制代码

pop方法中主要作了两步:

  1. 根据token,也就是哨兵对象找到该哨兵对象所处的page
  2. 从hotPage开始,一直删除到第一步找到的page.next==stop(哨兵对象)的位置

至此,关于AutoreleasePoolPage以及其中的关键方法就所有介绍完毕了。若是到这里,关于AutoreleasePool、AutoreleasePoolPage、哨兵对象还有点蒙的话,不要着急,继续往下看。

AutoreleasePool和AutoreleasePoolPage的关系

实际上,Runtime中并无AutoreleasePool结构的定义,AutoreleasePool是由AutoreleasePoolPage组成的双向链表,以下图:

image

在autoreleasepool的开始处,会调用AutoreleasePoolPage的push方法;在autoreleasepool的结束处,会调用AutoreleasePoolPage的pop方法。在AutoreleasePoolPage的push方法中,会往AutoreleasePoolPage中插入哨兵对象,以后的对象依次插入到AutoreleasePoolPage中。以下表示:

AutoreleasePoolPage::push(); // 这里会向AutoreleasePoolPage中插入哨兵对象
*** // 开发者本身写的代码,代码中的对象会依次插入到AutoreleasePoolPage中
***
AutoreleasePoolPage::pop(nil);
复制代码

当AutoreleasePoolPage满以后,会新建一个AutoreleasePoolPage,继续将对象添加到新的AutoreleasePoolPage中。

经过上面的介绍,能够知道,AutoreleasePool由多个AutoreleasePoolPage组成,且AutoreleasePool的开始处一定是一个哨兵对象。到这里,哨兵对象的做用也就清楚了,哨兵对象是用来分隔不一样的AutoreleasePool的。

当调用AutoreleasePoolPage::pop(nil)方法时,会从某个autoreleasepool开始,一直释放到参数哨兵对象所属的autoreleasepool。能够是同一个autoreleasepool,也能够不是同一个autoreleasepool,当不是同一个autoreleasepool时,能够理解成是嵌套autoreleasepool释放。

到这里,AutoreleasePool、AutoreleasePoolPage、哨兵对象之间的关系应该就理解了。

关于AutoreleasePool的一些面试题

AutoreleasePool在面试中出现的频率也很是高,接下来分享几道关于AutoreleasePool的面试题。

AutoreleasePool和线程的关系

确切地说,应该是AutoreleasePoolPage和线程的关系。AutoreleasePool是由AutoreleasePoolPage组成的双向链表,根据AutoreleasePoolPage的定义,每个AutoreleasePoolPage都属于一个特定的线程。也就是说,一个线程能够有多个AutoreleasePoolPage,可是一个AutoreleasePoolPage只能属于一个线程。

AutoreleasePool和Runloop的关系

Runloop,即运行循环。从直观上看,RunLoop和AutoreleasePool彷佛没什么关系,其实否则。在一个完整的RunLoop中,RunLoop开始的时候,会建立一个AutoreleasePool,在RunLoop运行期间,autorelease对象会加入到自动释放池中。在RunLoop结束以前,AutoreleasePool会被销毁,也就是调用AutoreleasePoolPage::pop方法,在该方法中,自动释放池中的全部对象会收到release消息。正常状况下,AutoreleasePool中的对象发送完release消息后,引用计数应该为0,会被释放,若是引用计数不为0,则发生了内存泄露。

AutoreleasePool中对象何时释放?

其实上面已经说过了,AutoreleasePool销毁时,AutoreleasePool中的全部对象都会发送release消息,对象会释放。那么,AutoreleasePool何时销毁呢?分两种状况:

  1. 一种状况就是上面提到的,当前RunLoop结束以前,AutoreleasePool会销毁。这种状况适用于系统自动生成的AutoreleasePool。
  2. 第二种状况是开发者本身写的AutoreleasePool,常见于for循环中,将循环体包在一个AutoreleasePool中。这种状况下,在AutoreleasePool做用域以后(也就是大括号),AutoreleasePool会销毁。
相关文章
相关标签/搜索