欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

AutoreleasePool的那些事

程序员文章站 2022-05-07 08:22:13
...

我们都知道一个iOS应用的如果是在main函数中,它的实现是

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

我们看到在main中有个@autoreleasepool,那它到底是什么呢?让我们转成.cpp看下:

 xcrun --sdk iphoneos clang -arch arm64 -rewrite-objc main.m 
复制代码

转换成c++后是

int main(int argc, char * argv[]) {
 /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
     return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
 }
}
复制代码

@autoreleasepool对应的就是个__AtAutoreleasePool

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};
复制代码

因为是C++代码,所以可以看出这个结构体构造函数会调用objc_autoreleasePoolPush(),析构函数会调用objc_autoreleasePoolPop();,所以main函数可以理解为:

int main(int argc, char * argv[]) {
 { 
     atautoreleasepoolobj = objc_autoreleasePoolPush();
     ...
     objc_autoreleasePoolPop(atautoreleasepoolobj);
 }
}
复制代码

那么objc_autoreleasePoolPush()objc_autoreleasePoolPop()都做了什么?我们可以从源码中一窥究竟

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

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}
复制代码

可以看到这两个方法只是调用了AutoreleasePoolPage的静态方法,那么AutoreleasePoolPage是什么?下面我们就去看一看

AutoreleasePoolPage

AutoreleasePoolPage是一个C++的类,并且是一个双向链表

class AutoreleasePoolPage 
{
    magic_t const magic;//16
    id *next;//8
    pthread_t const thread;//8
    AutoreleasePoolPage * const parent;//8
    AutoreleasePoolPage *child;//8
    uint32_t const depth;//4
    uint32_t hiwat;//4
}
复制代码

magic校验AutoreleasePoolPage的完整性,thread保存了当前所在线程

没一个自动释放池都是由多个AutoreleasePoolPage组成的,而每个AutoreleasePoolPage都有固定的大小

static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif

#define PAGE_MAX_SIZE           PAGE_SIZE
#define	PAGE_SIZE	        	I386_PGBYTES
#define I386_PGBYTES	    	4096		/* bytes per 80386 page */
复制代码

可以看出每个AutoreleasePoolPage的大小都是4096也就是16进制0x1000,而其中AutoreleasePoolPage自己的成员占56位,剩下的空间用于存储加入自动释放池的对象,AutoreleasePoolPage提供了两个方法begin()end()可以方便快速找到存储自动释放池对象的范围

    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));//当前`AutoreleasePoolPage`指针 + `AutoreleasePoolPage`大小得到起始位置
    }

    id * end() {
        return (id *) ((uint8_t *)this+SIZE);//当前`AutoreleasePoolPage`指针 + 整个`AutoreleasePoolPage`大小(4096)得到结束位置
    }
复制代码

next指针则指向了下一个为空的位置

大致的AutoreleasePoolPage我们已经了解了,那么我们回头去看下push操作

push

void *
objc_autoreleasePoolPush(void)
{
    return 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;
    }
复制代码

首先,我们看到传入了个POOL_BOUNDARY宏,这个宏只是个nil

#   define POOL_BOUNDARY nil
复制代码

从后面我们可以看到,传进去这个nil后会被加入自动释放池,并将这个值返回回来,然后在后面使用pop操作的时候传入这个POOL_BOUNDARY时,会一直release自动释放池中的对象直到找到第一个POOL_BOUNDARY

int main(int argc, char * argv[]) {
 { 
     atautoreleasepoolobj = objc_autoreleasePoolPush();
     ...
     objc_autoreleasePoolPop(atautoreleasepoolobj);
 }
}
复制代码

后面讲到pop时候我们在细说具体是怎么释放的
然后可以看到调用autoreleaseFast函数(DebugPoolAllocation我们不管)

    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);
        }
    }
复制代码

hotPage()获取可用的AutoreleasePoolPage,然后剩下要分三种情况

  • 当前有hotpage并且这个hotpage并没有满,调用add()函数
 if (page && !page->full()) {
    return page->add(obj);
   }
复制代码

会调用add()方法

id *add(id obj)
{
    assert(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;
    /*
	  *next++ = obj;看着可能会有点抽象了,我们给展开看
	  *next = obj;
	  next++;
   */
    protect();
    return ret;
    }
复制代码

这个方法很简单,将当前传入的对象加入第一个为空的位置(next)指向的位置,然后把next指针向后挪一位,最后返回传入的这个对象

  • 自动释放池有hotpage,但是hotpage已经满了,会调用autoreleaseFullPage()函数
if (page) {
    return autoreleaseFullPage(obj, page);
}
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;
        else page = new AutoreleasePoolPage(page);//创建一个新的page 将上一个的page child指针指向 新的page
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
    }
复制代码

这个方法里面在遍历整个AutoreleasePoolPage链表,找到不满的那个page或者如果遍历到最后一个page也都满了就创建一个新的page,并将这个page设置为hotPage,最后调用add()方法

  • 自动释放池没有hotPage,会调用autoreleaseNoPage()函数
else {
    return autoreleaseNoPage(obj);
}
    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
        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.
            //1,判断是否有空page
            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));
            //Debug环境 忽略
            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.
           // 2,如果传进来的是`POOL_BOUNDARY`则设置一个空page  使用tls技术 以键值对的方式存储
            return setEmptyPoolPlaceholder();
        }

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

        // Install the first page.
        //2,创建自动释放池中第一个page
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        //3,将这个page设置为hotPage
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
        //4,传入哨兵对象(POOL_BOUNDARY)
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        //5,添加对象进自动释放池
        return page->add(obj);
    }
复制代码

1,判断是否有空page
2,如果传进来的是POOL_BOUNDARY则设置一个空page 使用tls技术 以键值对的方式存储
3,将这个page设置为hotPage
4,传入哨兵对象(POOL_BOUNDARY)
5,添加对象进自动释放池

push操作就是这样的,下面我们继续看下pop

pop

void
objc_autoreleasePoolPop(void *ctxt)
{
   AutoreleasePoolPage::pop(ctxt);
}
复制代码

这个地方传入的ctxt正式调用push时返回的那个哨兵对象POOL_BOUNDARY(上文有说到)

  static inline void pop(void *token) 
   {
       AutoreleasePoolPage *page;
       id *stop;
       //这块貌似关于tls 能力有限不是太懂就不丢人现眼了,如果有大神对这些比较了解 望不吝赐教
       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);//根据token(一个指针)获取当前的page
       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);//释放栈中对象 直到stop (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();
           }
       }
   }
复制代码

上面貌似关于tls 能力有限不是太懂就不丢人现眼了,如果有大神对这些比较了解 望不吝赐教

这个方法的下半部分主要是以传入的token为标记从上往下一直进行release操作,指导遇到token为止,最后判断当前 page 使用不满一半,从 child page 开始将后面所有 page 删除;当前 page 使用超过一半,从 child page 的 child page(即孙子,如果有的话)开始将后面所有的 page 删除。具体为什么要又区分不是特别理解...

到此pushpop就已经说完了。在我们的理解中ARC环境下编译器会自动的给我们在变量后面加上retain,release,autorelease等方法,下面我们就去看下autorelease的实现

autorelease

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

   return rootAutorelease2();
}
inline id 
objc_object::rootAutorelease()
{
   if (isTaggedPointer()) return (id)this;
   if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

   return rootAutorelease2();
}
__attribute__((noinline,used))
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操作类似了。

TLS (Thread Local Storage)

那么事实上编译器真的只是在我们代码的后面加上了autorelease吗?我们写份代码

然后拖进Hopper Disassemebler中进行反编译看下

发现编译器并没有给我们添加autorelease,而是多了两个objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue方法,我们一个个先看看

id 
objc_autoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus1))
		return obj;

    return objc_autorelease(obj);
}
static ALWAYS_INLINE bool 
prepareOptimizedReturn(ReturnDisposition disposition)
{
    assert(getReturnDisposition() == ReturnAtPlus0);

    if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
        if (disposition) setReturnDisposition(disposition);
        return true;
    }

    return false;
}
复制代码

这个函数调用了prepareOptimizedReturn,然后调用了callerAcceptsFastAutorelease,传入一个__builtin_return_address(0)

__builtin_return_address接收一个称为 level 的参数。这个参数定义希望获取返回地址的调用堆栈级别。例如,如果指定 level 为 0,那么就是请求当前函数的返回地址。如果指定 level 为 1,那么就是请求进行调用的函数的返回地址,依此类推链接

接下来来看 callerAcceptsFastAutorelease 这个函数(以arm64为例):

static ALWAYS_INLINE bool 
callerAcceptsOptimizedReturn(const void *ra)
{
    // fd 03 1d aa    mov fp, fp
    // arm64 instructions are well-aligned
    if (*(uint32_t *)ra == 0xaa1d03fd) {
        return true;
    }
    return false;
}
复制代码

它检查了主调方在返回值之后是否紧接着调用了objc_retainAutoreleasedReturnValue,如果是,就知道了外部是ARC环境,反之就走没被优化的老逻辑。
简单的可以理解为,由objc_autoreleaseReturnValue将对象放入tls(Thread Local Storage);而外部由objc_retainAutoreleasedReturnValue将对象由tls中取出,这样就不用走autoreleasepool了,而由tls代劳了,这样就节省了autoreleasepool对对象的存储,清除开销了。

那也就是说ARC下只要调用方和被调方都用ARC编译时,所建立的对象都不加入autoreleasepool.更简单的说我们自己写的类,调用工厂方法生成对象都不会放 入autoreleasepool.(引用iOS Objective-C底层 part3:live^ARC )

最后琐事

  • 使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 这里被一个局部@autoreleasepool包围着
}];
复制代码

这段引自sunnyxx大神的文章

不过不知道是不是我理解的问题,我在代码中没有看到block中有autoreleasepool
main写了如下代码

编译成汇编后,并没有看到autoreleasepool的身影
然后在AutoreleasePoolPage::push()打上断点
可以看到enumerateObjectsWithOptions:usingBlock:这个方法中是有pushpop操作的(不知道理解的对不对,如果不对,请轻喷)

存疑:
其实AutoreleasePool还有很多可以说的,比如AutoreleasePool是在什么时候释放的,在下功力浅薄只知道在runloop每次循环的开始时候会去push,结束的时候去pop但是真的深入就不了解了,此处暂且存疑,待日后修炼归来再来解答


文章参考:
黑幕背后的Autorelease
iOS Objective-C底层 part3:live^ARC
自动释放池的前世今生