iOS内存管理4-autorelease自动释放池

临时变量什么时候释放
自动释放池的原理
自动释放池能否嵌套使用

使用xcode创建一个project在创建的时候有会生成一个main.m文件,其中@autoreleasePool的注解
可以通过clang将main.m转义成C++文件,找到源码切入

xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m

打开生成的main.cpp文件


int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    //自动释放池的作用域
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}


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

通过转义后的代码autorelease是一个结构体,结构体变量atautoreleasepoolobj是通过objc_autoreleasePoolPush创建而来,他的作用域在一个{}内。

另外有可以在在@autorelease处打断点,进入汇编调试界面,可以看到注释调用了objc_autoreleasePoolPush方法。
上面我们通过代码转义和汇编两个方式找到进入@autorelease源码的切入点

Autorelease的官方说明

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.

1.指针->栈的结构

  1. 释放对象+POOL_BOUNDARY(边界或者叫哨兵)
  2. 页的结构 + 双向链表
  3. 线程相关

自动释放池使用页,当前正在添加待释放对象的page称为hotpage,已经添加满了的page称为 coldpage。

push

通过objc4-781源码
objc_autoreleasePoolPush->AutoreleasePoolPage::push()
其中AutoreleasePoolPage是继承自 AutoreleasePoolPageData的类

struct AutoreleasePoolPageData
{
    magic_t const magic;   //用来校验AutoreleasePoolPage的结构是否完整 ,16字节(静态变量不占用结构体内存,它的大小是 uint32_5 m[4] = 4*4 的大小)
    __unsafe_unretained id *next;    //指向最新添加的autoreleased对象的下一个位置,初始化时指向begin() ,8字节
    pthread_t const thread;    //指向当前线程 //8字节
    AutoreleasePoolPage * const parent;  //指向父节点,第一个节点的parent值为nil ,8字节
    AutoreleasePoolPage *child;    //最后一个节点的child值为nil,8字节
    uint32_t const depth;      //深度,从0开始,4字节
    uint32_t hiwat;          //代表high water mark 最大入栈数量标记,4字节

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

AutoreleasePoolPage对象初始化

    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),
                                objc_thread_self(),
                                newParent,
                                newParent ? 1+newParent->depth : 0,
                                newParent ? newParent->hiwat : 0)
    {
        if (objc::PageCountWarning != -1) {
            checkTooMuchAutorelease();
        }

        if (parent) {
            parent->check();
            ASSERT(!parent->child);
            parent->unprotect();
            parent->child = this;
            parent->protect();
        }
        protect();
    }

其中begin()是当前对象的起始地址+ 56个字节,56个字节是AutoreleasePoolPageData中的变量已经占用了这么多,那么一个存放对象的地址应该是(id *) ((uint8_t *)this+sizeof(*this));

通过源码调试,需要切换到MRC环境

#import <Foundation/Foundation.h>
//自动释放池的打印方法
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
      
        for (int i = 0; i<10; i++) {
            NSObject *objc = [[NSObject alloc] autorelease];
        }
        _objc_autoreleasePoolPrint();
    }
    return 0;
}

输出


image.png

在释放池中添加505个对象

image.png

自动释放池创建了新的一页,在0x10180d038位开始的是NSObject对象,不是哨兵对象。
在x86架构下,第一页结构是page的原始结构+504个释放对象 +哨兵对象,大小是(504 + 1)*8 +56 = 4096 = 4kb
从第二页开始没有哨兵对象,一共可以存放505个释放对象

AutoreleasePoolPage::push()源码

    static inline void *push() 
    {
        id *dest;
        //执行slowpath概率很小
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

autoreleaseNewPage()创建一个新的page

    static __attribute__((noinline))
    id *autoreleaseNewPage(id obj)
    {  
        //1.获取聚焦page
        AutoreleasePoolPage *page = hotPage();
        //2.存在hotpage就添加
        if (page) return autoreleaseFullPage(obj, page);
        //3.没有hotpage就表明需要创建一个page
        else return autoreleaseNoPage(obj);
    }

autoreleaseFast源码

    static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
      //1.有hotpage,切没有满,直接添加
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            //2.如果页满了
            return autoreleaseFullPage(obj, page);
        } else {
          //3.没有获取到page
            return autoreleaseNoPage(obj);
        }
    }

push的流程图如下

image.png

但是由于slowpath的关系,执行autoreleaseNewPage概率很小。

autoreleaseFullPage 页满了的处理情况

    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);
        //如果hotpage满了,获取下一个非full的页给当前page,跳出循环后,设置当前page为hotpage,并将随访对象添加
        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }
  • 1.如果hotpage满了,判断是否有child,
    • 1.1没有child就创建一个page,复制给当前page,并退出当前会还
    • 1.2有就将child复制给当前page,继续while循环判断是否满了
  • 2.page没有满,跳出循环,设置当前page为hotpage,
  • 3.将释放对象add到page中

autoreleaseNoPage没有页的情况


    static __attribute__((noinline))
    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
      //No page 可能是没有被东西被压入栈中,或者只有一个占位的哨兵
        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", 
                         objc_thread_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();
        }

        // We are pushing an object or a non-placeholder'd pool.

        // Install the first page. 初始化第一页
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.  传入哨兵
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
         //将释放对象加入page中
        return page->add(obj);
    }
  • 1.判断是否是空占位符,如果是,则压栈哨兵标识符置为YES
  • 2.如果对象不是哨兵对象,且没有Pool,则报错
  • 3.如果对象是哨兵对象,且没有申请自动释放池内存,则设置一个空占位符存储在tls中,其目的是为了节省内存
  • 4.初始化第一页,并设置为hotpage
  • 5.压栈哨兵的标识符为YES,则压栈哨兵对象
  • 6.压栈对象
    add方法
    id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        //将obj压栈到next指针位置,然后next进行++,即下一个对象存储的位置
        *next++ = obj;
        protect();
        return ret;
    }

autolease:pop

当程序运行出自动释放池的作用域范围会__AtAutoreleasePool调用c++的析构函数,将pop出栈方法写入析构函数,开始释放。

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

pop->popPage->releaseUntil

static inline void
    pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            page = hotPage();
            if (!page) {
                // Pool was never used. Clear the placeholder.
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            page = coldPage();
            token = page->begin();
        } else {
            page = pageForPointer(token);
        }

        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            //
            if (stop == page->begin()  &&  !page->parent) {
                // 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 {
         
                return badPop(token);
            }
        }

        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }

        return popPage<false>(token, page, stop);
    }

popPage之前的代码都是在进行容错处理,page = pageForPointer(token);是根据token获取page。

    template<bool allowDebug>
    static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
       //此时传入的allowDebug为false
        if (allowDebug && PrintPoolHiwat) printHiwat();
       //出栈当前page的对象
        page->releaseUntil(stop);

        // memory: delete empty children
      //删除空子项
        if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (allowDebug && 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) {
            // hysteresis: keep one empty child if page is more than half full, 如果page超过一半,保存一个空的子集
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }

page->releaseUntil(stop);

 void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
      //  判断下一个对象是否等于stop,如果不等于,则进入while循环
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
          //获取当前操作页面,即hot页面
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            //next进行--操作,即出栈
            id obj = *--page->next;
          //将page的索引位设置为SCRIBBLE,表示已经被释放
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();
            //判断是否哨兵
            if (obj != POOL_BOUNDARY) {
              //释放对象
                objc_release(obj);
            }
        }

        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            ASSERT(page->empty());
        }
#endif
    }
    void kill() 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        AutoreleasePoolPage *page = this;
        //1.获取最后位置的page
        while (page->child) page = page->child;

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

推荐阅读更多精彩内容