iOS--自动释放池-autoreleasePool

495 阅读13分钟

一、临时变量什么时候释放:

首先我们在.m中写下下面代码,然后用clang命令编译成.cpp文件

clang -rewrite-objc main.m -o main.cpp

int main(int argc, const char * argv[]) {
    @autoreleasepool {

    }

    return 0;
}

我们发现被编译成这样了:

extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    }

    return 0;
}

发现生成了一个__AtAutoreleasePool对象,在.cpp文件中找一下,然后发现这是个结构体

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

其中__AtAutoreleasePool()是构造函数,~__AtAutoreleasePool()是析构函数;但是上面的.cpp文件的代码都没调用这两个函数,那这两个函数是啥时候调用的呢,我们来测试一下,首先创建一个结构体:

struct LGTest {
    LGTest(){
        printf("1123 - %s",__func__);
    }
    ~LGTest(){
        printf("5667 - %s",__func__);
    }
};

然后在main函数里面写个类似的代码,因为结构体是c++代码,所以记得要把.m文件改成.mm格式

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGTest test;
    }

    return 0;
}

然后运行一下,发现输出:

1123 - LGTest5667 - ~LGTest

说明__AtAutoreleasePool __autoreleasepool这句代码也会调用结构体里面的构造函数和析构函数,但是为啥两个函数都调用呢,析构函数是因为变量超出了作用域空间所以被析构,我们开发过程中经常遇到在一个大括号内的变量出了大括号就不能使用,这就是因为这个变量超出了自己的作用域,所以调用了析构函数是因为pool变量超出了自己的作用域空间,所以调用了析构函数,这个验证我们也可以通过汇编也能够看到

objc_autoreleasePoolPush

下面我们看一下源码是如何实现的,授信我们搜一下objc_autoreleasePoolPush

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

关于page,我们之前在虚拟内存中有内存分页的概念,可以猜测一下这个地方也应该有分页的可能,我们进去看一下

发现是继承于AutoreleasePoolPageData

struct AutoreleasePoolPageData
{
	magic_t const magic; // 16-检查完整性
	__unsafe_unretained id *next; //8  指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()
	pthread_t const thread; // 8  当前的线程
	AutoreleasePoolPage * const parent; //8 父节点,第一个节点的parent的值为nil
	AutoreleasePoolPage *child; //8  子节点,最后一个节点的child为nil
	uint32_t const depth; // 4  代表深度,从0开始,往后递增1
	uint32_t hiwat; // 4  代表high wate 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里面有这么多属性,也就是说AutoreleasePoolPage也有这么多属性,我们进到magic_t里面看一下

我们发现在magic_t里面第一个和第二个属性是全局静态变量,所以这两个属性不在magic_t结构体里面,二其中有个uint32_t类型的数组,并且有4个元素,所以magic_t大小是16个字节,并且在结构体中只有指针才是8个字节,而这个地方是结构体,所以需要加上整个结构体的大小

结合其他属性的大小,我们知道AutoreleasePoolPageData结构体的大小是56个字节,我们再看一下AutoreleasePoolPage 的描述

我们知道AutoreleasePoolPage是一个栈的形式,并且里面有一个边界对象,并且是一个双向链表,并且是跟线程一一对应的,这些解释跟上面的属性一一对应了

我们先在AutoreleasePoolPage里面看一下他的构造函数

我们发现传进去的第一个参数next指针传进去的是begin(),我们进到begin里面看下

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

我们发现这个地方就是本身的指针加上该对象的大小,而AutoreleasePoolPage本身大小是56字节

下面我们做个试验,将工程改成MRC,也就是在build setting里面搜一下ARC,将这个地方改成NO即可

然后在main.m文件里面写上下面代码。其中_objc_autoreleasePoolPrint是objc源码公开出来的,用来打印autoreleasePool里面对象的函数,我们通过extern引进来

extern void _objc_autoreleasePoolPrint(void);


int main(int argc, const char * argv[]) {
    @autoreleasepool {

        for (int i = 0; i < 5; i++) {
            NSObject *obj = [[NSObject alloc] autorelease];
            NSLog(@"obj = %@",obj);
        }
        _objc_autoreleasePoolPrint();
    }
    return 0;
}

打印:
2020-03-24 20:44:41.932647+0800 LGTest[29455:1511502] obj = <NSObject: 0x10183ce10>
2020-03-24 20:44:41.933305+0800 LGTest[29455:1511502] obj = <NSObject: 0x10063a780>
2020-03-24 20:44:41.933755+0800 LGTest[29455:1511502] obj = <NSObject: 0x10182ff10>
2020-03-24 20:44:41.934199+0800 LGTest[29455:1511502] obj = <NSObject: 0x101838500>
2020-03-24 20:44:41.934286+0800 LGTest[29455:1511502] obj = <NSObject: 0x101838530>
objc[29455]: ##############
objc[29455]: AUTORELEASE POOLS for thread 0x1000a95c0
objc[29455]: 6 releases pending.
objc[29455]: [0x101005000]  ................  PAGE  (hot) (cold)
objc[29455]: [0x101005038]  ################  POOL 0x101005038
objc[29455]: [0x101005040]       0x10183ce10  NSObject
objc[29455]: [0x101005048]       0x10063a780  NSObject
objc[29455]: [0x101005050]       0x10182ff10  NSObject
objc[29455]: [0x101005058]       0x101838500  NSObject
objc[29455]: [0x101005060]       0x101838530  NSObject
objc[29455]: ##############
Program ended with exit code: 0

我们发现autoreleasePool里面有6个对象,其中第一个0x101005000是POOL的开始地址,然后第一个对象地址是0x101005038,中间相隔56个字节,也就是正好是一个AutoreleasePoolPageData结构体的大小,所以这个额位置就是AutoreleasePoolPageData结构体,然后下面跟着的都是对象的指针,上面说了AutoreleasePoolPage是分页的形式,我们多加一些对象,将对象改成505个

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        for (int i = 0; i < 505; i++) {
            NSObject *obj = [[NSObject alloc] autorelease];
            NSLog(@"obj = %@",obj);
        }
        _objc_autoreleasePoolPrint();
    }
    return 0;
}

然后我们发现最后打印

发现AutoreleasePoolPage分页了,这是因为AutoreleasePoolPage每页大小是4096字节,也就是4KB,而每个指针是8个字节,每页都有一个AutoreleasePoolPage结构体56个字节,(4096-56)/8=505总共能存505个对象指针,因为第一页有一个pool对指针象,所以可以额外存504个对象,但是后面每页没有pool对象,所以可以存505个对象指针,我们可以进到AutoreleasePoolPage里面看下

然后我们发现AutoreleasePoolPage的SIZE是4096,这就是AutoreleasePoolPage的容量

压栈:objc_autoreleasePoolPush

然后我们再看下对象指针是如何加入到AutoreleasePoolPage里面的,我们先看一下AutoreleasePoolPage的push函数

static inline void *push() 
    {
        id *dest;
        if (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;
    }

一般不是debug模式,所以我们再看一下autoreleaseFast函数

static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }

我们发现上面有两个判断,

  • 如果有page,并且page没满,会走add方法,
  • 如果有page,但page满了,会走autoreleaseFullPage方法,
  • 其他情况也就是没有page会走autoreleaseNoPage函数

我们来一个个看,先看add方法

id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

发现这个方法会把当前的next指针return过去,并且会把page的next往下移obj大小,也就是一个指针的大小,那returen回去的地址就是page里面最新的对象的指针的位置,那么初始化时候我们可以看下autorelease方法

// Replaced by ObjectAlloc
- (id)autorelease {
    return ((id)self)->rootAutorelease();
}

inline id 
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里面,所以创建和push的时候都会走到该函数里面

我们再看一下autoreleaseFullPage,这个函数是在有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.
        assert(page == hotPage());
        assert(page->full()  ||  DebugPoolAllocation);

        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

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

这里面我们可以看到有个do-while循环,里面会不断遍历page的child,只有到找到有空余的childPage时候才能会终止循环,并且如果childPage为空的话,会通过AutoreleasePoolPage创建一个新的page

当找到page的时候,会通过setHotPage把该page定义为聚焦页,也就是会在tls中添加一个标记

static inline void setHotPage(AutoreleasePoolPage *page) 
    {
        if (page) page->fastcheck();
        tls_set_direct(key, (void *)page);
    }

上面就是AutoreleasePoolPage的压栈效果,下面图就形容了自动释放池AutoreleasePoolPage的结构

这是三个page在一起的结构,最后一个page被标记成hotPage

出栈:objc_autoreleasePoolPop

那出栈是如何出栈呢。我们在上面知道出栈是调用objc_autoreleasePoolPop函数的,我们先看一下源码:

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

从上面转成的c++代码我们知道objc_autoreleasePoolPush会返回一个atautoreleasepoolobj,而objc_autoreleasePoolPop就是对这个对象进行释放的,我们进入到pop函数里面看看

static inline void pop(void *token) 
    {
        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) {
                // 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 {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

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

其中EMPTY_POOL_PLACEHOLDER是当pool为空的时候的临时占位标志,说明没有任何压栈对象,然后判断有没有hotpage,如果有就将它释放掉,然后如果没有就把hotpage置为nil,然后返回;

如果page里面有对象的话就要,先进行容错处理,也就是如果stop不为空的话,假如stop为第一页,但是page的parent还存在的话,就会走badPop,如果没问题的花椒就走 page->releaseUntil(stop);我们看一下releaseUntil函数

    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 = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;
            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
    }

因为objc_autoreleasePoolPop传进去的是pool对象指针,而pool对象指针就是最开始的内存位置,所以releaseUntil里面有个whilf循环,会从page的最后的指针开始遍历一直遍历到最开头,

然后我们看下while循环里面的,先拿到hotpage,然后从hotpage开始遍历所有的page,当page不为空的时候,开始往下走,通过next指针偏移拿到page的最后面的对象 ,然后进行release

就这从最后面的page的最尾端的objc进行release,一直遍历到最pool的指针位置,将pool里面所有的objc对象都release

autorelease的面试题

子线程的autoreleasepool和主线程的autoreleasepool分布有什么影响:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSObject *objc = [[NSObject alloc] autorelease];
        NSLog(@"objc = %@",objc);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSObject *obj = [[NSObject alloc] autorelease];
                NSLog(@"obj = %@",obj);
                _objc_autoreleasePoolPrint();
        });
        _objc_autoreleasePoolPrint();
    }
    sleep(2);
    return 0;
}

打印:
2020-03-25 18:46:57.190368+0800 LGTest[23551:3261388] objc = <NSObject: 0x102825c30>
2020-03-25 18:46:57.190943+0800 LGTest[23551:3261798] obj = <NSObject: 0x100549760>
objc[23551]: ##############
objc[23551]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[23551]: ##############
objc[23551]: AUTORELEASE POOLS for thread 0x70000070b000
objc[23551]: 2 releases pending.
objc[23551]: [0x100808000]  ................  PAGE  (hot) (cold)
objc[23551]: [0x100808038]  ################  POOL 0x100808038
objc[23551]: [0x100808040]       0x100549760  NSObject
objc[23551]: ##############
objc[23551]: 2 releases pending.
objc[23551]: [0x101803000]  ................  PAGE  (hot) (cold)
objc[23551]: [0x101803038]  ################  POOL 0x101803038
objc[23551]: [0x101803040]       0x102825c30  NSObject
objc[23551]: ##############
Program ended with exit code: 0

我们发现pool里面只有一个objc,这是因为在MRC里面,需要手动添加autorelease,所以代码需要改一下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSObject *objc = [[NSObject alloc] autorelease];
        NSLog(@"objc = %@",objc);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @autoreleasepool {
                NSObject *obj = [[NSObject alloc] autorelease];
                NSLog(@"obj = %@",obj);
                sleep(1);
                _objc_autoreleasePoolPrint();
            }
        });
        _objc_autoreleasePoolPrint();
    }
    sleep(2);
    return 0;
}

打印:

020-03-25 19:07:08.539509+0800 LGTest[24640:3289994] objc = <NSObject: 0x10054e110>
2020-03-25 19:07:08.540138+0800 LGTest[24640:3290336] obj = <NSObject: 0x100721950>
objc[24640]: ##############
objc[24640]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[24640]: 2 releases pending.
objc[24640]: [0x101804000]  ................  PAGE  (hot) (cold)
objc[24640]: [0x101804038]  ################  POOL 0x101804038
objc[24640]: [0x101804040]       0x10054e110  NSObject
objc[24640]: ##############
objc[24640]: ##############
objc[24640]: AUTORELEASE POOLS for thread 0x700004771000
objc[24640]: 3 releases pending.
objc[24640]: [0x101800000]  ................  PAGE  (hot) (cold)
objc[24640]: [0x101800038]  ################  POOL 0x101800038
objc[24640]: [0x101800040]  ################  POOL 0x101800040
objc[24640]: [0x101800048]       0x100721950  NSObject
objc[24640]: ##############

我们发现有两个pool,每个pool都和一个线程进行绑定了,并且外面的pool会保存内部pool的指针,所以当循环嵌套时候,里面的pool会被着外面的pool持有