美文网首页iOS开发技术
iOS 内存管理之AutoReleasePool

iOS 内存管理之AutoReleasePool

作者: huxinwen | 来源:发表于2020-06-02 17:31 被阅读0次

    背景

    自从苹果推出了ARC管理内存后,对于iOS开发这而言,内存管理就变得so easy了,只要正确使用相关规则,再也不用担心double release,野指针的等问题了,而ARC的背后,除了强大的编译器之外,还要得益于运行时起作用的AutoReleasePool。

    研究AutoReleasePool

    iOS的项目中,除了特别需求外,整个项目就一个地方明确写了autoReleasePool的代码了,就是main函数:

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

    autoreleasepool做了什么?

    我们知道oc代码在编译期间都会转化为c/c++代码,然后转化为汇编,最终转化为对应的架构的二进制文件;也可以这么说,oc的底层实现就是c/c++,既然这样,我们把他转化为对应的c/c++代码应该就可以窥探到其中的密码了:
    转化为c/c++代码,Xcode有自带的工具,打开命令行,输入一下命令就可以:

    xcrun -sdk iphoneos   clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
    

    为了减少代码量,重新建了一个macOS命令行项目:

    #import <Foundation/Foundation.h>
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            NSLog(@"Hello, World!");
        }
        return 0;
    }
    

    转化为cpp文件,看下对应的代码:

    struct __AtAutoreleasePool {
      __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
      ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
      void * atautoreleasepoolobj;
    };
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_k5_h0x40m15075dxn_z956dk2500000gn_T_main_6e2ecd_mi_0);
        }
        return 0;
    }
    

    从上面的C++源码可以发现@autoreleasepool {}最后变成了:

    { __AtAutoreleasePool __autoreleasepool;//定义了一个__AtAutoreleasePool结构体变量
    。。。
     }
    

    我们分析下流程:
    1、进入大括号,定义了一个__AtAutoreleasePool的结构体局部变量;
    2、定义这个结构体变量的时候,会走结构体的构造方法,间接的会调用objc_autoreleasePoolPush函数:

    __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    

    3、当走出大括号是,局部变量__autoreleasepool,会被销毁,因此会走结构体的析构函数,间接就会调用objc_autoreleasePoolPop函数:

      ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
    

    从上面的分析,我们发现了两个重要的函数objc_autoreleasePoolPush和objc_autoreleasePoolPop,这两个函数是全局函数,而且是以objc开头的,应该是在objc的源码中,下载objc源码的地址,macOS 最新系统下面的objc4。

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

    他们调用的是AutoreleasePoolPage类对应的push跟pop两个静态函数,那么我们就要研究下AutoreleasePoolPage这个类了。

    研究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;
    ...
    }
    

    从AutoreleasePoolPage的成员变量可以分析出,AutoreleasePoolPage是一个双向链表的结构,每个实例都会存在一个parent实例指针,跟一个child实例指针。其他成员变量暂时不知道表示什么意思,只能继续研究AutoreleasePoolPage的实现逻辑了,还是从push跟pop函数入手:

    AutoreleasePoolPage 的push函数:

      static inline void *push() 
        {
            id *dest;
            if (DebugPoolAllocation) {
                // Each autorelease pool starts on a new pool page .debug模式新建一个page对象,实际不需要关注
                dest = autoreleaseNewPage(POOL_BOUNDARY);
            } else {//实际走这里
                dest = autoreleaseFast(POOL_BOUNDARY);
            }
            assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
            return dest;
        }
    

    追根溯源autoreleaseFast:

    static inline id *autoreleaseFast(id obj)
        {
            AutoreleasePoolPage *page = hotPage();
    //拿到当前正在被使用的page,因为每个page都是有对象obj数量限
    //制的,当page放满了,就会创建一个child page来继续放。
            if (page && !page->full()) {//没满,直接添加到当前的page上
                return page->add(obj);//添加obj
            } else if (page) {//full,满了
                return autoreleaseFullPage(obj, page);//将obj添加到对应的未满的child page里面,并将其设置为hot page
            } else {//没有page
                return autoreleaseNoPage(obj);
            }
        }
    
     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;
        }
    
     static __attribute__((noinline))
        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.
            assert(page == hotPage());
            assert(page->full()  ||  DebugPoolAllocation);
    
            do {
                if (page->child) page = page->child;//若有child page,将当前的指针指向child page
                else page = new AutoreleasePoolPage(page);//new一个新的page,并将其赋值给child page;
            } while (page->full());//page是否满了,没满,跳出
    
            setHotPage(page);//将page设置为当前hotpage
            return page->add(obj);//添加obj到page
        }
    
        id *autoreleaseNoPage(id obj)
        {
            // "No page" could mean no pool has been pushed
            // or an empty placeholder pool has been pushed and has no contents yet
            assert(!hotPage());
    
            bool pushExtraBoundary = false;
            if (haveEmptyPoolPlaceholder()) {
                // We are pushing a second pool over the empty placeholder pool
                // or pushing the first object into the empty placeholder pool.
                // Before doing that, push a pool boundary on behalf of the pool 
                // that is currently represented by the empty placeholder.
                pushExtraBoundary = true;
            }
            else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
                // We are pushing an object with no pool in place, 
                // and no-pool debugging was requested by environment.
                _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                             "autoreleased with no pool in place - "
                             "just leaking - break on "
                             "objc_autoreleaseNoPool() to debug", 
                             pthread_self(), (void*)obj, object_getClassName(obj));
                objc_autoreleaseNoPool(obj);
                return nil;
            }
            else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
                // We are pushing a pool with no pool in place,
                // and alloc-per-pool debugging was not requested.
                // Install and return the empty pool placeholder.
                return setEmptyPoolPlaceholder();//设置一个占位的空的page
            }
    
            // We are pushing an object or a non-placeholder'd pool.
    
            // Install the first page.第一次创建一个page
            AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
            setHotPage(page);//并将它设置为hotpage
            
            // Push a boundary on behalf of the previously-placeholder'd pool.
            if (pushExtraBoundary) {
                page->add(POOL_BOUNDARY);//添加哨兵对象
            }
            
            // Push the requested object or pool.
            return page->add(obj);//添加obj
        }
    
       id * begin() {
            return (id *) ((uint8_t *)this+sizeof(*this));//page的起始地址+成员变量的大小
        }
    
        id * end() {
            return (id *) ((uint8_t *)this+SIZE);///一个page的大小是size,4096字节
        }
    
        bool empty() {
            return next == begin();
        }
    
        bool full() { 
            return next == end();
        }
    
      id *add(id obj)
        {
            assert(!full());
            unprotect();
            id *ret = next;  // faster than `return next-1` because of aliasing
            *next++ = obj;//将obj的指针赋值给next所在的位置,然后将next指向下一个位置
            protect();
            return ret;//返回前一个obj
        }
    

    总结一下:

    1. push操作从取出当前的hotpage,然后将一个哨兵对象(其实是nil)放入到page的next位置,并将next指向的位置向下+1;
    2. 取出的hotpage存在,但是hotpage是一个fullpage(一个page,PAGE_MAX_SIZE = 4096字节大小,除了存放内部成员变量的值之外,其他的都用来存放autorelease对象的地址),这时就会循环查找他的child指向的page对象,知道找到没使用完的child page,如果没有child page,则创建一个,将找到的child page并设置为hotpage,然后将一个哨兵对象添加进去;
    3. 取出的当前hotpage不存在,则通过autoreleaseNoPage创建一个新的page,并设置为hotpage,然后将然后将一个哨兵对象添加进去;
    4. 这样做的结果,除了第一层page(没有parent page),每次push返回的obj的都是哨兵对象,最开始的push返回的是第一层page最开始的位置page.begin(),后面的pop会用到这个返回值。

    AutoreleasePoolPage 的pop函数:

        static inline void pop(void *token) 
        {//token就是对应push返回值,上面提到过要么是哨兵对象,要么是第一层page最开始的位置
            AutoreleasePoolPage *page;
            id *stop;
    
            if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
                // Popping the top-level placeholder pool.
                if (hotPage()) {
                    // Pool was used. Pop its contents normally.
                    // Pool pages remain allocated for re-use as usual.
                    pop(coldPage()->begin());
                } else {
                    // Pool was never used. Clear the placeholder.
                    setHotPage(nil);
                }
                return;
            }
    
            page = pageForPointer(token);
            stop = (id *)token;
            if (*stop != POOL_BOUNDARY) {
                if (stop == page->begin()  &&  !page->parent) {
                    //要么是第一层page最开始的位置
                    // Start of coldest page may correctly not be POOL_BOUNDARY:
                    // 1. top-level pool is popped, leaving the cold page in place
                    // 2. an object is autoreleased with no pool
                } else {//其他情况不存在,坏的page
                    // Error. For bincompat purposes this is not 
                    // fatal in executables built with old SDKs.
                    return badPop(token);
                }
            }
    
            if (PrintPoolHiwat) printHiwat();
    
            page->releaseUntil(stop);//释放存放在page的指针所指的对象,直到遇到哨兵对象或者全部释放完成
    
            // memory: delete empty children
            if (DebugPoolAllocation  &&  page->empty()) {
                // special case: delete everything during page-per-pool debugging
                AutoreleasePoolPage *parent = page->parent;
                page->kill();
                setHotPage(parent);
            } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
                // special case: delete everything for pop(top) 
                // when debugging missing autorelease pools
                page->kill();
                setHotPage(nil);
            } 
            else if (page->child) {//将page对象释放掉
                // hysteresis: keep one empty child if page is more than half full
                if (page->lessThanHalfFull()) {
                    page->child->kill();
                }
                else if (page->child->child) {
                    page->child->child->kill();
                }
            }
        }
    
        void releaseUntil(id *stop) 
        {
            // Not recursive: we don't want to blow out the stack 
            // if a thread accumulates a stupendous amount of garbage
            
            while (this->next != stop) {//循环释放对象,直到遇到哨兵对象或者全部释放完
                // Restart from hotPage() every time, in case -release 
                // autoreleased more objects
                AutoreleasePoolPage *page = hotPage();
    
                // fixme I think this `while` can be `if`, but I can't prove it
                while (page->empty()) {//当前page释放完,还没遇到哨兵对象,拿到parent page,并设置为hotpage,继续释放,直到遇到哨兵对象或者全部释放完
                    page = page->parent;
                    setHotPage(page);
                }
    
                page->unprotect();
                id obj = *--page->next;//拿到obj对象,并将next指针指向上一个位置
                memset((void*)page->next, SCRIBBLE, sizeof(*page->next));//清空next位置,这里是设置为0x3A
                page->protect();
    
                if (obj != POOL_BOUNDARY) {
                    objc_release(obj);//释放掉对象
                }
            }
    
            setHotPage(this);//释放完后,将当前page设置为hotpage
    
    #if DEBUG
            // we expect any children to be completely empty
            for (AutoreleasePoolPage *page = child; page; page = page->child) {
                assert(page->empty());
            }
    #endif
        }
    

    pop函数总结几点:

    1. 会拿到最近一次push函数(pop函数与push一一对应)返回的哨兵对象,作为pop函数的入参;
    2. 遍历hotpage的next指针指向的对象,并释放,直到遇到入参的哨兵对象;
    3. 如果当前page释放完了,还没遇到哨兵对象,就会往parent page遍历,直到遇到哨兵对象,一次类推;
    4. 最后释放掉为空(empty)的page对象,但是需要注意的是:当他的parent的使用空间超过了1/2,保留它对应的child page。

    从上面的分析大致了解了AutoreleasePoolPage的工作流程,在程序运行的时候是怎么样工作的呢?
    我们知道,在MRC时代,需要程序员手写对象的retain和release,后面最智能的就是new一个对象的时候,我们需要带上autorelease代码:

    [[[NSObject alloc] init] autorelease];//MRC手动管理内存
    

    到了ARC,我们不需要这样写,因为编译器在编译的时候,会自动帮忙加上这些代码,所以说不管是ARC还是MRC时期,oc对象的内存管理入口都是autorelease方法:

    autorelease方法的研究:

    - (id)autorelease {
        return ((id)self)->rootAutorelease();
    }
    
    inline id 
    objc_object::rootAutorelease()
    {
        if (isTaggedPointer()) return (id)this;//tagPointer 不需要
        if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
    
        return rootAutorelease2();
    }
    
    __attribute__((noinline,used))
    id 
    objc_object::rootAutorelease2()
    {
        assert(!isTaggedPointer());
        return AutoreleasePoolPage::autorelease((id)this);//调用的是AutoreleasePoolPage的autorelease函数
    }
    
    static inline id autorelease(id obj)
        {
            assert(obj);
            assert(!obj->isTaggedPointer());
            id *dest __unused = autoreleaseFast(obj);//调用autoreleaseFast,上面提到过
            assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
            return obj;
        }
    

    就是说,每个oc对象创建的时候(alloc/new/copy/mutableCopy),都是通过autorelease方法添加到page对象中的。pop那具体的调用时机又是什么呢?我们知道,项目只在main函数有一个autoreleasepool,其他的地方除非程序员自己手动添加,就不会有了,也就是说:我们暂且认为编译器帮忙添加的autorelease,那也只是添加进page,pop函数还是只有一个,而且是程序退出的时候调用,如果是这种情况的话,程序整个运行期间,内存得不停的增长,因为只有申请,没有释放。显然这种做法是行不通的 。

    那么系统是怎么做的呢?直接说结论:我们知道程序运行期间是通过runloop维持的,而runloop就是不停的监听事件和timer,处理事件和timer,没有事件和timer的时候就进入休眠,系统会在runloop里面添加了两个autorelease相关的observers: autorelease相关的observers.png
    RunLoop& AutoReleasePool关系几点说明:
    1. App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
    2. 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
    3. 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

    总结

    通过上面的源码及流程的分析,我们对于autoreleasepool的工作原理及流程有了充分的了解:

    1. autoreleasepool底层的数据结构是一个autoreleasepage的双向链表,每个page的大小为4096字节,除了存储成员变量的大小,其他的位置都用来存储autorelease对象的地址,next变量永远指向下一个可以存放autorelease对象地址的地址空间,具体结构如下: 数据结构
    2. 当一个page1存储空间用完后,会创建一个新的page2,新的page2的parent指针指向满了的page1,page1的child指针会指向page2;
    3. 程序启动就会创建一个autoreleasepool,会调用push,程序结束时,最后会调用pop,回收所有autorelease对象的内存;
    4. 程序运行期间,同过监听runloop的休眠状态,调用push/pop方法,管理autorelease对象;
    5. 每次push的时候,会往page里面添加一个哨兵对象,这个哨兵对象作为下次pop函数的入参,遇到哨兵对象,说明这次runloop循环添加到page的autorelease对象release完毕。

    相关文章

      网友评论

        本文标题:iOS 内存管理之AutoReleasePool

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