Autoreleasepool详细解析

987 阅读9分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情 。如果哪里写的不对,请大家评论批评。本篇文章整理了有几天的时间,可能有部分大家都看过,我相信自己整理的文档相当的齐全,如果还有哪里不到位的请大家评论留言,笔者在去学习与思考。

Autoreleasepool详细解析

Autoreleasepool

自动释放池,管理局部变量,在作用域结束的时候,统一对内部的变量做release操作。

Main起点

 #import <Foundation/Foundation.h>
 #import "Person.h"
 ​
 int main(int argc, const char * argv[]) {
     @autoreleasepool {
         Person * p = [[Person alloc]init];
     }
     return 0;
 }

在autoreleasepool的用法上,main函数可以说是第一个使用autoreleasepool{}的地方,Main函数会开启RunLoop导致程序持续运行,不会退出,也就是作用域一直存在,那么为什么在这里会有一个autoreleasepool呢?这里的对象怎么去销毁呢?今天我们来详细的研究一下。

源码

常规操作对main进行编译生成C++文件。

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

在对底部可以找到一段这样的代码

 int main(int argc, const char * argv[]) {
     /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
         Person * p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
     }
     return 0;
 }

__AtAutoreleasePool

这里有一个__AtAutoreleasePool,在文件最下方可以找到对应的代码

 extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
 extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
 ​
 struct __AtAutoreleasePool {
   __AtAutoreleasePool() {
     //构造函数,在创建结构体的时候调用
     atautoreleasepoolobj = objc_autoreleasePoolPush();
   }
   ~__AtAutoreleasePool() {
     //析构函数,在结构体销毁的时候调用
     objc_autoreleasePoolPop(atautoreleasepoolobj);
   }
   void * atautoreleasepoolobj;
 };

由此可见OC内部所有的对象都是一个结构体。

__AtAutoreleasePool()是一个构建函数,也就是在main函数中,__AtAutoreleasePool __autoreleasepool;会自动调用objc_autoreleasePoolPush();

在main函数中,__AtAutoreleasePool __autoreleasepool;在大括号{}里面,也就是局部变量,那个{}执行完成之后自动回执行析构函数,执行objc_autoreleasePoolPop()

很有意思的是构造函数生成了atautoreleasepoolobj对象,在析构函数的时候有销毁了atautoreleasepoolobj对象

__AtAutoreleasePool主要的就是这个objc_autoreleasePoolPush以及objc_autoreleasePoolPop这里就需要去下载源码,我使用的objc4-680,版本不同可能会存在一定的差异

 void *
 objc_autoreleasePoolPush(void)
 {
     if (UseGC) return nil;
     return AutoreleasePoolPage::push();
 }
 ​
 void
 objc_autoreleasePoolPop(void *ctxt)
 {
     if (UseGC) return;
     AutoreleasePoolPage::pop(ctxt);
 }

在这里得到一个关键的对象。AutoreleasePoolPage,也就是左右的操作都是由AutoreleasePoolPage来完成的。

也就是说,所有调用autorelease的对象都是由AutoreleasePoolPage来管理的

AutoreleasePoolPage

AutoreleasePoolPage在NSObject.mm有500+的代码,这里先提取一部分来展示

 class AutoreleasePoolPage 
 {
 ​
 #define POOL_SENTINEL nil
     static pthread_key_t const key = AUTORELEASE_POOL_KEY;
     static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
     static size_t const SIZE = PAGE_MAX_SIZE
     static size_t const COUNT = SIZE / sizeof(id);
 ​
     magic_t const magic;//用来校验 AutoreleasePoolPage 的结构是否完整;
     id *next;// 当前栈最后一个对象的地址
     pthread_t const thread;// 当前线程
     AutoreleasePoolPage * const parent;// 指向上一个节点
     AutoreleasePoolPage *child;// 指向下一个节点
     uint32_t const depth;// 深度
     uint32_t hiwat;
     

划重点:从类结构可以看出来AutoreleasePoolPage是一个双链表,为什么能看出来?看这里

 AutoreleasePoolPage * const parent;// 指向上一个节点
 AutoreleasePoolPage *child;// 指向下一个节点

Push

     static inline void *push() 
     {
         id *dest;
         if (DebugPoolAllocation) {
             // Debug模式下直接跳过,看下面真机模式
             dest = autoreleaseNewPage(POOL_SENTINEL);
         } else {
             dest = autoreleaseFast(POOL_SENTINEL);
         }
         assert(*dest == POOL_SENTINEL);
         return dest;
     }

autoreleaseFast

     static inline id *autoreleaseFast(id obj)
     {
         // 获取当前能够添加对象的page
         AutoreleasePoolPage *page = hotPage();
         if (page && !page->full()) {
             return page->add(obj);
         } else if (page) {
             return autoreleaseFullPage(obj, page);
         } else {
             return autoreleaseNoPage(obj);
         }
     }

hotPage

获取链表中最后一个page,tls_get_direct没理解,求大佬可以解答:

 static inline AutoreleasePoolPage *hotPage() 
     {
         AutoreleasePoolPage *result = (AutoreleasePoolPage *)
             tls_get_direct(key);
         if (result) result->fastcheck();
         return result;
     }

这里出现了几种情况,

1、当前page存在,并且page没有装满,那么可以直接add这个对象(push的时候实际上是一个POOL_SENTINEL)

2、如果page存在,已经满了,那么会创建一个新的page,autoreleaseFullPage

3、如果page不存在,表示这是第一个page,autoreleaseNoPage

划重点: 这里有一个问题,可以看到每一次push传入的参数都是POOL_SENTINEL,也就是我们理解的哨兵对象,那么需要释放的对象怎么入栈的呢?这个问题下面会给大家答案 请直接看《局部变量怎么入栈?》

怎么判断是否满了呢?

  bool full() { 
      return next == end();
  }
 ​
  id * end() {
       return (id *) ((uint8_t *)this+SIZE);
  }

当前的指针等于page指针+Size,也就是说,当前指针指向最后一位了,后面没有空间了~~~~

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);
 ​
         do {
             if (page->child) page = page->child;
             else page = new AutoreleasePoolPage(page);
         } while (page->full());
 ​
         setHotPage(page);
         return page->add(obj);
     }

这里有个while循环,一直在判断当前page已经满了。在循环里有个

if,等下看着if,大致就是获取到一个page,setHotPage从上文可以得知应该是设置当前使用的page,最后add

下面看下if

1、如果page->child意思是如果已经有了下一个链表,也就是已经有了一个新的page,把page->child赋值给page

2、else也就是没有新page,那么久去创建一个page,注意这里有个入参,当前的page?里面干啥了? 走去喽喽去

     AutoreleasePoolPage(AutoreleasePoolPage *newParent) 
         : magic(), next(begin()), thread(pthread_self()),
           parent(newParent), child(nil), 
           depth(parent ? 1+parent->depth : 0), 
           hiwat(parent ? parent->hiwat : 0)
     { 
         if (parent) { // 如果有父节点
             parent->check();
             assert(!parent->child);
             parent->unprotect();
             parent->child = this;
             parent->protect();
         }
         protect();
     }

这是个什么写法?????我学识浅薄不太懂...........看着都是AutoreleasePoolPage的参数,咱们就理解为一种写法吧,把入参分解的写法

大致的意思是创建一个新的page,把上一个节点的child指向当前的新的page,完成链表

unprotect && protect

在代码中经常发现一起使用,看起来像一个锁。

     inline void unprotect() {
 #if PROTECT_AUTORELEASEPOOL
         check();
         mprotect(this, SIZE, PROT_READ | PROT_WRITE);
 #endif
     }
     
         inline void protect() {
 #if PROTECT_AUTORELEASEPOOL
         mprotect(this, SIZE, PROT_READ);
         check();
 #endif
     }

看起来像是读写锁,unprotect多了PROT_WRITE

autoreleaseNoPage

回到这里,

     static __attribute__((noinline))
     id *autoreleaseNoPage(id obj)
     {
         // No pool in place.
         assert(!hotPage());
 ​
         if (obj != POOL_SENTINEL  &&  DebugMissingPools) {
             // We are pushing an object with no pool in place, 
             // and no-pool debugging was requested by environment.
             _objc_inform("MISSING POOLS: Object %p of class %s "
                          "autoreleased with no pool in place - "
                          "just leaking - break on "
                          "objc_autoreleaseNoPool() to debug", 
                          (void*)obj, object_getClassName(obj));
             objc_autoreleaseNoPool(obj);
             return nil;
         }
 ​
         // Install the first page.
         AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
         setHotPage(page);
 ​
         // Push an autorelease pool boundary if it wasn't already requested.
         if (obj != POOL_SENTINEL) {
             page->add(POOL_SENTINEL);
         }
 ​
         // Push the requested object.
         return page->add(obj);
     }

我们理解了如果分配一个page,这里大致也了解了,其实就是判断一下你是否应有page,如果有直接断言了。

也就是说,第一次(没有创建过page才会走到这里),直接就是AutoreleasePoolPage入参还是nil,不需要设置父节点(因为没有父节点,他就是第一个),然后直接add,这里大概率add的也是一个哨兵对象,真实的数据理论不会到走这个方法

这里整个push就完事了

不对不对??有个问题,push的时候都是哨兵对象啊?真实的数据怎么进来的呢?

咦??是啊,我去找找

局部变量怎么入栈?

首先找到最后在哪里入栈对象的,autoreleaseFast方法。我们知道在push的时候autoreleaseFast入参是POOL_SENTINEL,也就是说我们需要找到不是入参POOL_SENTINEL的。

autorelease

只有它

    static inline id autorelease(id obj)
     {
         assert(obj);//对象存在
         assert(!obj->isTaggedPointer());//对象是优化过的指针对象
         id *dest __unused = autoreleaseFast(obj);//入栈,进入到page,就是它了
         assert(!dest  ||  *dest == obj);
         return obj;
     }

谁来调用它呢?

我有点疯了~~

objc_object::rootAutorelease2()

 __attribute__((noinline,used))
 id 
 objc_object::rootAutorelease2()
 {
     assert(!isTaggedPointer());
     return AutoreleasePoolPage::autorelease((id)this);
 }

autorelease周期

 - [NSObject autorelease]
 └── id objc_object::rootAutorelease()
     └── id objc_object::rootAutorelease2()
         └── static id AutoreleasePoolPage::autorelease(id obj)
             └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
                 ├── id *add(id obj)
                 ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                 │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                 │   └── id *add(id obj)
                 └── static id *autoreleaseNoPage(id obj)
                     ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                     └── id *add(id obj)

总结一下,也就是当对象直接autorelease的时候,会把当前对象入栈到当前的page中。

pop

源代码挺多,一点点分析

     static inline void pop(void *token) 
     {
         AutoreleasePoolPage *page;
         id *stop;
     
         page = pageForPointer(token);
         stop = (id *)token;
         if (DebugPoolAllocation  &&  *stop != POOL_SENTINEL) {
             // This check is not valid with DebugPoolAllocation off
             // after an autorelease with a pool page but no pool in place.
             _objc_fatal("invalid or prematurely-freed autorelease pool %p; ", 
                         token);
         }
 ​
         if (PrintPoolHiwat) printHiwat();
        //释放page中的对象
         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();
             }
         }
     }
  1. 传入的实际是一个POOL_SENTINEL哨兵对象

  2. 我们需要找到对应的page

  3. 通过pageForPointer去找page,入参是一个token,这个方法等会细看,token应该是一个哨兵指针

  4. releaseUntil(stop),看起来像去释放空间了

  5. 中间略过,可以看到最后最有的节点都kill()

    怎么理解这个地方呢?

    @autoreleasepool {}会把整个空间变成一个局部的变量,下面代码如是

    { __ AtAutoreleasePool __autoreleasepool; Person * p = [[Person alloc]init]; }

    __autoreleasepool会走构造函数,初始化了一个哨兵对象插入到了page中,并返回了这个哨兵对象的地址,在析构时候,又传入了这个哨兵对象地址。我们可以想象一个案例

    举个🌰:

    ABCDE几个公司做核酸,当E公司来的时候会给你一个牌子(哨兵对象),代表后面这一块都是E公司员工,突然物业让E公司去其他对方做核酸,这个队伍就要释放E公司,从后面开始释放排队成员,直到遇到E公司的牌子,我们就可以很轻松的释放掉E公司员工

pageForPointer找到当前page

     static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
     {
         AutoreleasePoolPage *result;
         uintptr_t offset = p % SIZE;
 ​
         assert(offset >= sizeof(AutoreleasePoolPage));
 ​
         result = (AutoreleasePoolPage *)(p - offset);
         result->fastcheck();
 ​
         return result;
     }

将指针与4096取模,得到当前指针的偏移量,最后得到Page

p = 0x100816048 p % SIZE = 0x48 result = 0x100816048 - 0x48 = 0x100816000

而最后调用的方法 fastCheck() 用来检查当前的 result 是不是一个 AutoreleasePoolPage

通过检查 magic_t 结构体中的某个成员是否为 0xA1A1A1A1

releaseUntil

循环释放对象,直到stop

   void releaseUntil(id *stop) 
     {
         while (this->next != stop) {
      
           AutoreleasePoolPage *page = hotPage();
 ​
            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_SENTINEL) {
                 objc_release(obj);
             }
         }
 ​
         setHotPage(this);
     }

一个大的while循环,release掉当前空间的所有变量,直到遇到哨兵指针。

kill

    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;
         while (page->child) page = page->child;
 ​
         AutoreleasePoolPage *deathptr;
         do {
             deathptr = page;
             page = page->parent;
             if (page) {
                 page->unprotect();
                 page->child = nil;
                 page->protect();
             }
             delete deathptr;
         } while (deathptr != this);
     }

最终其实就是把page的child都置成了nil

总结

当pop的时候,传入一个哨兵对象,根据哨兵对象的位置地址,计算当前所在的page,然后releaseUntil将栈中的对象释放,知道stop也就是遇到哨兵对象。最后在kill()中将链表全部删除。

与RunLoop的关系

RunLoop的几种状态

 typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
 kCFRunLoopEntry = (1UL << 0), 1
 kCFRunLoopBeforeTimers = (1UL << 1), 2
 kCFRunLoopBeforeSources = (1UL << 2), 4
 kCFRunLoopBeforeWaiting = (1UL << 5), 32
 kCFRunLoopAfterWaiting = (1UL << 6), 64
 kCFRunLoopExit = (1UL << 7), 128
 kCFRunLoopAllActivities = 0x0FFFFFFFU
 };
  • kCFRunLoopEntry

    • 即将进入Loop,runloop 每一次进入一个 mode,就通知一次外部 kCFRunLoopEntry,之后会一直以该 mode 运行,知道当前 mode 被终止,进而切换到其他 mode,并再次通知 kCFRunLoopEntry。
  • kCFRunLoopBeforeTimers

    • 即将处理 Timer
  • kCFRunLoopBeforeSources

    • 即将处理 Source
  • kCFRunLoopBeforeWaiting

    • 即将进入休眠,如果能够从内核队列上读出 msg 则继续运行任务,如果当前队列上没多余消息,则进入睡眠状态。
  • kCFRunLoopAfterWaiting

    • 刚从休眠中唤醒,mach_msg 终于从队列里读出了 msg,可以继续执行任务了。
  • kCFRunLoopExit

    • kCFRunLoopExit

关系

iOS在主线程的Runloop中注册了2个Observer

1、当第1个Observer监听到了进入状态(kCFRunLoopEntry),就会调用objc_autoreleasePoolPush()

2、当第2个Observer监听到了即将休眠状态(kCFRunLoopBeforeWaiting)就会调用objc_autoreleasePoolPop()和objc_autoreleasePoolPush()

3、当第2个Observer监听到了即将退出状态(kCFRunLoopBeforeExit)就会调用objc_autoreleasePoolPop()

image-20220812213749821.png

面试相关

什么时候经常用到autoreleasePool

  • 需要创建很多临时对象的循环,可以在循环内使用自动释放池

方法里有局部对象,出了方法后会立即释放吗

  • 在ARC,在局部方法结束的时候会对局部对象做relase操作,所以局部对象会立即释放
  • 如果局部对象是放在了 autoreleasePool 自动释放池,在 runloop 迭代结束的时候释放

autorelease对象在什么时机会被调用release

  • 如果对象直接被autoreleasepool包住,那在autoreleasepool大括号结束的时候就release;

     - (void)viewDidLoad {
       Preson * preson = [[[Preson alloc] init] autorelease];
     }
     // 会在大括号结束后就释放吗?
     // 不够严谨啊
     ​
     - (void)viewwillAppear:(bool)animated{
       [super viewwillAppear:animated];
     }
     ​
     // 实际测试发现会在viewwillAppear和viewDidLoad之后被释放?
     // 实际和runloop有关。
    
  • 如果对象不是被autoreleaspool包住,释放是由runloop控制的。在所属的runloop循环中,runloop休眠之前调用release

main函数为什么会有autoreleasePool

官方文档有一句这样的话:翻译过来

Cocoa 总是希望代码在自动释放池块中执行,否则自动释放的对象不会被释放并且您的应用程序会泄漏内存。

所以猜测,在mian之前可能会产生一些对象,需要再程序结束的时候释放,在mian之前,RunLoop还没有开启,这些对象不会随着runloop休眠释放。

相关

developer.apple.com/library/arc…

www.codenong.com/js5fcc28ba2…

\

/