AutoreleasePool的那些事

AutoreleasePool的那些事

我们都知道一个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_autoreleaseReturnValue
objc_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:这个方法中是有
push
pop操作的(不知道理解的对不对,如果不对,请轻喷)

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


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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/101608.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • R、RStudio下载与安装方法

    R、RStudio下载与安装方法现如今,R语言是统计领域广泛使用的工具,是属于GNU系统的一个自由、免费、源代码开放的软件,是用于统计计算和统计绘图的优秀工具。而RStudio是R的集成开发环境,用它进行R编程的学习和实践会更加轻松和方便。下面就教大家如何下载并安装R和RStudio,比较简单。R的维护工作由一个国际化的开发者团队负责。R软件的官方下载页面叫作TheComprehensiveRArchiveNetwor…

    2022年6月30日
    46
  • Django(25)WSGIRequest对象[通俗易懂]

    Django(25)WSGIRequest对象[通俗易懂]Django在接收到http请求之后,会根据http请求携带的参数以及报文信息创建一个WSGIRequest对象,并且作为视图函数第一个参数传给视图函数。也就是我们经常看到的request参数。在这个

    2022年7月30日
    11
  • 继电器的驱动电路

    继电器的驱动电路文章目录前言一、DC5V,DC12V,DC~V是什么?二、使用步骤1.小测试三极管如何驱动继电器总结前言很多同学不知道继电器如何使用,这里简单的介绍一下,我这里介绍一下5脚的继电器一、DC5V,DC12V,DC~V是什么?这里的5V,12V指的是继电器的工作电压,也就是1脚和3脚两端的电压,具体看电压器的规格,如果你是DC5V,那你1脚和3脚之间的电压必须是5V,2脚是输入,5脚是常闭端,也就是平时2脚跟5脚连接在一起,当13脚电流达到50mA之后,触点就打到了4脚,也就是说2脚跟4脚连在了一起。

    2022年6月24日
    25
  • 线程指令重排[通俗易懂]

    线程指令重排[通俗易懂]1、指令重排JVM为优化执行效率对线程内的执行顺序进行重排,对单线程来说执行指令重排并不会影响程序从上到下执行的代码逻辑。但是在多线程的情况下,则可能会出现问题。2、指令重排原则程序顺序原则:一个线程内保证语义的串行性volatile规则:volatile变量的写,先发生于读锁规则:解锁(unlock)必然发生在随后的加锁(lock)前传递性:A先于B,B先于C那么A必然先于C线程的start方…

    2022年10月18日
    4
  • C#中什么是泛型

    C#中什么是泛型参考视频c#教程泛型集合与非泛型集合最大的区别在于,泛型集合,不需要进行装箱和拆箱的操作。如集合元素为值类型,通常泛型集合要优于非泛型集合,并优于从非泛型集合派生出来的类型,泛是广泛的意思,而型是数据类型。这里的泛型可以理解为应用广泛的数据类型。为了提高性能及维护类型安全,一般最好采用泛型集合。如果两个类的内容完全一样,只是处理的数据类型不同。那么,采用泛型是一个不错的选择。泛型类用于封装不是特定于具体数据类型的操作,通常用于集合。诸如从集合中添加和移除项这样的操作都以大体上相同的方式执行,与所存

    2022年6月16日
    33
  • 一篇万字博文带你入坑爬虫这条不归路 【万字图文】

    ????最近,很多粉丝私信我问——爬虫到底是什么?学习爬虫到底该从何下手?????????其实,我想说的也是曾经的我身为小白的时候某些大牛对我说过的——很多时候我们都有一颗想要学习新知识的心,却总是畏惧于对想要学习内容的无知,这也是多数人失败甚至后悔终身的:因为他们从来没有开始过!????????借一位几年前带我入坑的前辈的话——坑就在你面前,别总是犹豫徘徊,大胆一点:向前一步,入了这个坑,莽着头就是往前冲,别多想,别回头,终有一天——>你也会成为别人的前辈!????今日份鸡汤已成功送达,目

    2022年4月6日
    44

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号