美文网首页iOS 底层探索之路
iOS 底层探索:内存管理 (下)

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

作者: 欧德尔丶胡 | 来源:发表于2020-11-26 19:04 被阅读0次

    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寻找答案。

    相关文章

      网友评论

        本文标题:iOS 底层探索:内存管理 (下)

        本文链接:https://www.haomeiwen.com/subject/aslaiktx.html