iOS 底层探索:内存管理 (下)

iOS 底层探索: 学习大纲 OC篇

前言

  • 紧接上一篇,这篇开始分析autorelease ,在ARC时代程序员基本不用操作Autorelease,只知道有autoreleasepool自动释放池,但是在面试的过程中很大概率会被问到,并且很难清晰的回答,今天我们就深入的探索下autorelease。

一、autorelease 的基本概念

autorelease机制是在iOS内存管理中的一员。

  • 在MRC中,是通过调用[obj autorelease]来延迟内存释放;
  • 在ARC中,我们已经完全不需要知道Autorelease就能很好地管理好内存。
autorelease的使用举例:
// MRC
NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool drain];

// ARC
@autoreleasepool {
  id obj = [NSObject alloc] init];
}
ARC中 @autoreleasepool 做了什么?
  • 在MRC时代,如果我们想先retain一个对象,但是并不知道在什么时候可以release它,我们可以像下面这么做:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

NSString* str = [[[NSString alloc] initWithString:@"XXXXXX"] autorelease];
//use str...

[pool release];

就是说,我们可以在创建对象的时候给对象发送autorelease消息,然后当NSAutoreleasePool结束的时候,标记过autorelease的对象都会被release掉,也就是会被释放掉。

  • 但是在ARC时代,我们不用手动发送autorelease消息,ARC会自动帮我们加。而这个时候,@autoreleasepool做的事情,跟NSAutoreleasePool就一模一样了。

在开发中,main函数里面:

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);
}

main函数的主体都被@autoreleasepool的Block块包在里面,也就是说,接下来所有的对象创建都在这个block里面。那么其中 @autoreleasepool他的具体实现原理是什么呢?抱着这个问题,我们开始其底层探索。

二、@autoreleasepool 的底层原理分析

通过 Clang 探索编译的源码 :

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
       .......
    }

看到这个__AtAutoreleasePool, 先全局搜一下:

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

得到一个由两个方法组成的结构体:

  • 构造函数:objc_autoreleasePoolPush()
  • 析构函数:objc_autoreleasePoolPop()

通过 断点调试查看堆栈和汇编探索:


说明在@autoreleasepool{} 执行了两个函数。那么这两个函数到底做了什么呢?
这个时候不知道怎么分析了。不知道它怎么执行的。这个时候我们去源码中搜索看看。找到 Autorelease pool implementation 实现

/*
 * NSObject-internal.h: Private SPI for use by other system frameworks.
 */

/***********************************************************************
   Autorelease pool implementation
   A thread's autorelease pool is a stack of pointers.
   Each pointer is either an object to release, or POOL_BOUNDARY which is
     an autorelease pool boundary.
   A pool token is a pointer to the POOL_BOUNDARY for that pool. When
     the pool is popped, every object hotter than the sentinel is released.
   The stack is divided into a doubly-linked list of pages. Pages are added
     and deleted as necessary.
   Thread-local storage points to the hot page, where newly autoreleased
     objects are stored.
**********************************************************************/
/*
谷歌翻译:

    自动释放池的实现

    线程的自动释放池是指针的栈。
    每个指针都是要释放的对象,或者是POOL_BOUNDARY(哨兵或者边界),它是自动释放池的边界。
    池令牌是指向该池的POOL_BOUNDARY(哨兵或者边界)的指针。 弹出池后,将释放比哨点更热的每个对象。
    栈分为doubly-linked list (双向链接的页面)列表。 根据需要添加和删除页面。
    线程本地存储指向热页面,该页面存储新自动释放的对象。
*/
// structure version number. Only bump if ABI compatability is broken
#define AUTORELEASEPOOL_VERSION 1

捕获的信息 :基本理解就是通过双向链接的页面(双向链表)的栈的结构,自动释放池里面管理着线程。

打开源码开始验证

搜索objc_autoreleasePoolPush如下:

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push(); // c++中 ::  表示域操作符,就是调用方法的意思
}

NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

可以看到它的核心AutoreleasePoolPage,通过pushpop 来操作的。

进入AutoreleasePoolPage

class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    friend struct thread_data_t;
public:
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // 页的最大值4096 
#else
        PAGE_MIN_SIZE;  // size and alignment, power of 2
#endif
    
private:
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const COUNT = SIZE / sizeof(id);

    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil
......省略......

我们会发现AutoreleasePoolPage 它继承自 AutoreleasePoolPageData,进入AutoreleasePoolPageData

class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
    magic_t const magic;  // 用来校验AutoreleasePoolPage的结构是否完整
    __unsafe_unretained id *next; //栈顶地址,只想最新添加的autoreleased对象的下一个位置,初始化时的方向begin()
    pthread_t const thread; // 当前所属的线程
    AutoreleasePoolPage * const parent; //指向父节点,第一个节点的parent 值 为nil
    AutoreleasePoolPage *child; // 指向子节点,最后一个节点的child值为nil
    uint32_t const depth; //代表深度,从0开始,往后递增1
    uint32_t hiwat; //表示high water mark ? 

// 很明显这个就是对外的构造方法
    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

继续搜索看看AutoreleasePoolPageData()调用

AutoreleasePoolPageData(begin(),
                                objc_thread_self(),
                                newParent,
                                newParent ? 1+newParent->depth : 0,
                                newParent ? newParent->hiwat : 0)

我们发现这里传入了begin()

 id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));  
    }

根据以往经验看到 + sizeof(*this) 是不是就想到了内存平移呢?

断点调试验证一下:

看到AutoreleasePoolPageData 这个类 我们计算一下它的属性的大小:

struct AutoreleasePoolPageData
{
    magic_t const magic;  // 16 
    __unsafe_unretained id *next; //8
    pthread_t const thread; // 8
    AutoreleasePoolPage * const parent; //8
    AutoreleasePoolPage *child; //8
    uint32_t const depth; //4
    uint32_t hiwat; //4
  ....
}
struct magic_t {
    static const uint32_t M0 = 0xA1A1A1A1;  //static 静态变量的内存不在结构体内
    static const size_t M1_len = 12;
    uint32_t m[4]; // 数组 4 x 4  = 16个字节
}

经过计算刚好56个字节。

再通过一个例子来研究下:

注:这里方便研究使用了MRC ,autorelease ,其实ARC里自动调用了autorelease的底层autoreleaseFast,这个可以进源码看的,下面也会分析到的,暂不说明。

从这个图中我们就能大概了解到自动释放池的结构信息了。 在栈顶是一个哨兵对象,下面是pool里的对象信息。它的大概结构如图:

Autorelease pool implementation里的内容提到过 Autorelease pool 的数据结构是一个双向链接(双向链表)页面的栈的结构。 并且AutoreleasePoolPage的PAGE_MAX_SIZE为4096 。

我们可以计算一下它能存多少个nsobject的无属性的对象,首先明确一点对象至少16个字节,但是Autorelease pool 里面管理的事对象的指针占8个字节,包括一个哨兵对象8个字节,出去本身的属性占56个字节。那么Autorelease pool 管理的对象的总数计算为:( 4096 - 56 )/ 8 - 1= 504

  • 验证一下: 如果放505个对象是什么情况:

现在又要问了?这些对象怎么进去的?怎么压栈的,这些页是怎么关联的呢?

objc_autoreleasePoolPush()

回到最初的构造函数:objc_autoreleasePoolPush(),进入源码分析:

static inline void *push() 
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) { //DEBUG
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);  //来到这里
        }
        return dest;
    }

现在看到@autoreleasepool{} 的底层push ,它到了autoreleaseFast这个函数,上面提到MRC下测试使用autorelease 其实他的底层最后也用了autoreleaseFast这个方法,查找方式如下:

-(id) autorelease
{
    return _objc_rootAutorelease(self);
}
_objc_rootAutorelease(id obj)
{
    ASSERT(obj);
    return obj->rootAutorelease();
}
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

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

 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;
    }

最后也走到了这个函数autoreleaseFast,所以 逐个分析这个函数:


// 对象进来了
  static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {

         //当前page存在,且不满,则add
            return page->add(obj);
        } else if (page) {
         //当前page存在,且满
            return autoreleaseFullPage(obj, page);
        } else {
      //当前page不存在
            return autoreleaseNoPage(obj);
        }
    }

当前page不存在

static __attribute__((noinline)) id *autoreleaseNoPage(id obj)
{
    assert(!hotPage());
    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) { //是否存在未使用的空池
        pushExtraBoundary = true;
    } else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        //obj 不是哨兵对象时, 说明上一次的 push() 没有成功, 如果打开了丢失 pools 的调试
        _objc_inform(...); //输出一些调试信息
        objc_autoreleaseNoPool(obj); //这个函数估计在llvm里了
        return nil;
    } else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        //如果 obj 是哨兵对象, 并且没有开启 pool 内存申请的调试
        return setEmptyPoolPlaceholder(); //将本自动释放池标记为未使用的空池
    }
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); //创建一个 page 节点, 这里会调用构造函数
    setHotPage(page); //将该节点设置为 hotPage
    if (pushExtraBoundary) { //如果本自动释放池是一个未使用的空池
        page->add(POOL_BOUNDARY); //加入哨兵对象
    }
    return page->add(obj); //将 obj 加入自动释放池
}

static inline bool haveEmptyPoolPlaceholder()
{
     id *tls = (id *)tls_get_direct(key); //取出本线程对应的 hotPage
     return (tls == EMPTY_POOL_PLACEHOLDER); //如果为空池标识符则返回 true
}

当前page存在,且不满,则add

id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing 由于折叠,比“return next-1”快
        *next++ = obj;  // 这里就可以看出指针不断的压栈
        protect();
        return ret;
    }

当前page存在,且满

id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
        // The hot page is full. 
        // Step to the next non-full page, adding a new page if necessary.
        // Then add the object to that page.
      //热页面已满。
      //步骤到下一个非完整页面,如果需要,添加一个新页面。
      //然后将对象添加到该页面。
        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page); // 这里就开始创建添加新页面了
        } while (page->full());  

        setHotPage(page);
        return page->add(obj);
    }

AutoreleasePoolPage 创建新page: 可以发现又重新调用了AutoreleasePoolPageData的方法。再次begin()等等

AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),
                                objc_thread_self(),
                                newParent,
                                newParent ? 1+newParent->depth : 0,
                                newParent ? newParent->hiwat : 0)
    {
        if (parent) {
            parent->check();
            ASSERT(!parent->child);
            parent->unprotect();
            parent->child = this; //赋值子节点,链表就连上了
            parent->protect();
        }
        protect();
    }

分析到这里,就可以看出对象指针压栈到表的过程了。

整个过程如下图所示:
objc_autoreleasePoolPop()

栈是先进后出(FILO—First-In/Last-Out),可以想到objc_autoreleasePoolPop(出栈)的过程和objc_autoreleasePoolPush(压栈)相反。

进入源码:

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

可以看到这里有个*ctxt,通过clang下的代码:

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

可以发现objc_autoreleasePoolPop(atautoreleasepoolobj) ,其中atautoreleasepoolobj这个东西就是传给*ctxt,它来自于atautoreleasepoolobj = objc_autoreleasePoolPush() ,这样压栈得出的结果就和出栈联系在一起了,也不会混乱了。

进入pop源码:

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) { //如果 token 为空池标志
        if (hotPage()) { //如果有 hotPage, 即池非空
            pop(coldPage()->begin()); //将整个自动释放池销毁
        } else {
            setHotPage(nil); //没有 hotPage, 即为空池, 设置 hotPage 为 nil
        }
        return;
    }
    page = pageForPointer(token); //根据 token 找到所在的 节点
    stop = (id *)token; //token 转换给 stop
    if (*stop != POOL_BOUNDARY) { //如果 stop 中存储的不是哨兵节点
        if (stop == page->begin()  &&  !page->parent) {
            //存在自动释放池的第一个节点存储的第一个对象不是哨兵对象的情况, 有两种情况导致:
            //1. 顶层池呗是否, 但留下了第一个节点(有待深挖)
            //2. 没有自动释放池的 autorelease 对象(有待深挖)
        } else {
            //非自动释放池的第一个节点, stop 存储的也不是哨兵对象的情况
            return badPop(token); //调用错误情况下的 badPop()
        }
    }
    return popPage<false>(token, page, stop);
}

static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop){
    page->releaseUntil(stop); //将自动释放池中 stop 地址之后的所有对象释放掉
    if (...) {
        //这一段代码都是调试用代码
    } else if (page->child) { //如果 page 有 child 节点
        if (page->lessThanHalfFull()) { //如果 page 已占用空间少于一半
            page->child->kill(); //kill 掉 page 的 child 节点
        } else if (page->child->child) { //如果 page 的占用空间已经大于一半, 并且 page 的 child 节点有 child 节点
            page->child->child->kill(); //kill 掉 child 节点的 child 节点
        }
    }
}

void releaseUntil(id *stop)
{
    // 释放当前page中的autorelease对象
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();

        // 如果当前page都释放完了还没遇到POOL_BOUNDARY,就继续释放上一个page的
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        // 依次取出autorelease对象
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        // 释放autorelease对象
        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }

    setHotPage(this);
}

过程:不断的pop指针对象去objc_release,直到哨兵对象,再kill(),然后根据父子节点,找到父节点,再去pop指针对象,循环直到POOL_BOUNDARY ,这里就体现了双向链表的数据结构。

POOL_BOUNDARY
  • POOL_BOUNDARY会在建立新的自动释放池时作为第一个对象加入到池中, 被称为哨兵对象, 哨兵对象是自动释放池中非常巧妙而且重要的一环。
  • 我们已经知道 @autoreleasepool {}是在作用域的开始使用push()方法来创建自动释放池, 在作用域结束时, 使用 pop() 方法来销毁自动释放池。
  • 在嵌套结构中push()方法不一定会创建新的 page 节点, 如果当前节点未满则会直接插入一个哨兵对象, 如果当前节点已满则创建一个新的 page 节点并且插入一个哨兵对象, push()函数的返回值就是这个哨兵对象的地址(哨兵对象的值是 nil, 但哨兵对象的地址不为 nil), 然后在pop()方法调用时, 传入这个哨兵对象的地址, 对这个地址之后的 autorelease 对象发送release方法。

三 、拓展 autorelease面试题

autorelease对象什么时候释放呢?

  • 错误回答:当前作用域也就是大括号结束时释放
  • 正确回答:在如果没有手动加Autorelease Pool情况下,autorelease对象是在当前runloop迭代结束(休眠之前)之后才会释放,释放的原因是因为系统在每个runloop迭代中都已经加入了自动释放池进行push和pop。

我们知道,主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理。 那么多线程、Runloop和Autoreleasepool 之间有什么关系呢?接下来就去分析Runloop,在Runloop寻找答案。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容