29、iOS底层分析 - @autoreleasepool 自动释放池

@autoreleasepool 自动释放池

 面试题:

 1、临时变量什么时候释放?
 2、自动释放池原理?
 3、自动释放池能否嵌套使用?
 

研究分析

例如main 中的 @autoreleasepool 思路是什么?
 clang、汇编

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


 1、clang
 终端

 cd main.m 地址
 clang -rewrite-objc main.m -o main.cpp

编译过程中会产生一些中间的状态。

查看得到的main.cpp,拉到最后查看

extern void _objc_autoreleasePoolPrint(void);
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

我们源码中的 @autoreleasepool 变成了 __AtAutoreleasePool __autoreleasepool;

查看 __AtAutoreleasePool 

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

 这是一个结构体,里面是
 构造函数  objc_autoreleasePoolPush();
 析构函数  objc_autoreleasePoolPop(atautoreleasepoolobj);
 

下面看一下实际是怎么调用的。
 写个方法

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


@autoreleasepool{
    LGTest test;
 }

打印结果

 1123 - LGTest
 5667 - ~LGTest
 Program ended with exit code: 0

可以发现,调用了函数的构造函数,然后又调用了析构函数(因为出了作用域就没有意义了)
 
2、 汇编查看

 
 查看源码
 objc 的源码。

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

内存分页
点进去,看到来自于一个私有的 AutoreleasePoolPageData 这么一个数据结构。

/***********************************************************************
 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.
 **********************************************************************/
    
 BREAKPOINT_FUNCTION(void objc_autoreleaseNoPool(id obj));
 BREAKPOINT_FUNCTION(void objc_autoreleasePoolInvalid(const void *token));

 class AutoreleasePoolPage : private AutoreleasePoolPageData
 {
    friend struct thread_data_t;

 1、具有栈的特性,先进后出。
 2、每个指针要么是要释放的对象,要么是要释放的 POOL_BOUNDARY 自动释放池边界。
    POOL_BOUNDARY 边界(之前的说法是哨兵对象)
    边界的用途。在栈的结构体中有一些自己的成员变量,再往里面添加一些对象,加进来的对象也需要pop出去,需要根据边界来判断释放到什么时候为止。
 3、双向列表
    doubly-linked list
 4、跟我们当前寄存的
     tls 跟 线程 是有关系的。
 
 链表结点:
 

 

 struct AutoreleasePoolPageData
 {
     magic_t const magic;               // 16   检查结构体完整性
     __unsafe_unretained id *next;      // 8    next指针,指向最新添加的 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    最大入栈数量
 

看一下

struct magic_t {
     static const uint32_t M0 = 0xA1A1A1A1; //静态变量占用的是全局静态变量的空间,不占结构体空间
     #   define M1 "AUTORELEASE!"
     static const size_t M1_len = 12;       //静态变量
     uint32_t m[4];                         //m[4]; 数组有4个元素,都是uint32_t占 4*4 = 16;
 
     magic_t() {
         ASSERT(M1_len == strlen(M1));
         ASSERT(M1_len == 3 * sizeof(m[1]));
 
         m[0] = M0;
         strncpy((char *)&m[1], M1, M1_len);
     }

如上注释分析可以知道  magic_t const magic; 是16字节。 AutoreleasePoolPageData 的总内存大小为56
 
 AutoreleasePoolPage * const parent;// 8    父节点
 AutoreleasePoolPage *child;        // 8    子节点
 这就类似 - Class 类
 类里面有superClass。普通的 类的初始化的时候,会先将 父类 和 元类 初始化构造完成。初始化父类之后,会将我们本类这个类添加为我们父类的类的子节点。
 
 通过上面的代码分析,说明我们所有的 AutoreleasePool 都会有以上这些属性。
 暂时不知道这些都是干什么的,先回到 return AutoreleasePoolPage::push(); 点击跳到 push 里面去

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

这里就是压栈进去
分析各个参数

class AutoreleasePoolPage : private AutoreleasePoolPageData
 {
     ......
     AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
     AutoreleasePoolPageData(begin(),
     objc_thread_self(),
     newParent,
     newParent ? 1+newParent->depth : 0,
     newParent ? newParent->hiwat : 0)
     {

AutoreleasePoolPageData(begin(),  数据类型的第一个参数传的是begin()。
  看一下 begin()

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


 为什么 sizeof(*this) 是 56呢?看一下上面的 AutoreleasePoolPageData 这些是固有属性大小,总共是56字节。

经过上面的代码验证,我们猜测 autoreleasePool 里面添加对象进行了分页,一页的话存了多少个对象呢。如上我们循环了505次,第一页里存了我们的504个对象,但是还有个问题,我们循环了505次,为什么会有506个对象,506个对象的话第二页存了一个那么说明第一页存了505个。难道是一页能存505个对象?

看源码

AutoreleasePoolPage    size_t   就是我们 page 的大小。

 objc_autoreleasePoolPush
 class AutoreleasePoolPage : private AutoreleasePoolPageData
 {
    friend struct thread_data_t;
 
 public:
    static size_t const SIZE =
 #if PROTECT_AUTORELEASEPOOL
    PAGE_MAX_SIZE;  // must be multiple of vm page size
 #else
    PAGE_MIN_SIZE;  // size and alignment, power of 2
 #endif
 #define PAGE_MIN_SIZE           PAGE_SIZE
 
 #define PAGE_SIZE               I386_PGBYTES
 
 #define I386_PGBYTES            4096

总共的 page 大小 - 固有属性大小(56) = 可以存放对象的大小。一个对象是8字节  那么可存放对象的大小/8 = 可存放对象个数。

4096 - 56 = 4040 / 8 = 505
 505 - 1个特殊的属性之外第一页能容纳504个对象。
 一个 AutoreleasePoolPage 里面最多可以容纳多少个8字节对象?
 每页可以容纳 505 个对象,第一页会加入一个特殊的对象 边界。也就是说第一页可以加 504个自己的对象,之后的都是505个。
 
 验证

 for (int i=0; i<505+505; i++) {
    NSObject *objc = [[NSObject alloc] autorelease];
 }

控制台打印出来的内容可以看到
 
 由此可以知道每页存了505个对象,第一页加入了一个特殊的边界对象。

下面就看一下怎么添加这个边界对象的

 void
 _objc_autoreleasePoolPrint(void)
 {
    AutoreleasePoolPage::printAll();
 }
static void printAll()
 {
     _objc_inform("##############");
     _objc_inform("AUTORELEASE POOLS for thread %p", objc_thread_self());
 
     AutoreleasePoolPage *page;
     ptrdiff_t objects = 0;
     for (page = coldPage(); page; page = page->child) {
         objects += page->next - page->begin();
     }
     _objc_inform("%llu releases pending.", (unsigned long long)objects);
    //是否有空的池占位符
     if (haveEmptyPoolPlaceholder()) {
         _objc_inform("[%p]  ................  PAGE (placeholder)",
         EMPTY_POOL_PLACEHOLDER);
         _objc_inform("[%p]  ################  POOL (placeholder)",
         EMPTY_POOL_PLACEHOLDER);
     }
     else {
         for (page = coldPage(); page; page = page->child) {
         page->print();
     }
 }

判断是否有空的池占位符,也就是看是否是第一页,是的话就会添加边界(哨兵对象),所以第一页只能容纳我们自己的504个对象。


 2、对象是怎么进入的,怎么释放的。

进栈

AutoreleasePoolPage::push();

//看一下 push()
static inline void *push()
 {
     id *dest;
     //判断是否是debug 环境
     if (slowpath(DebugPoolAllocation)) {
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_BOUNDARY);
     } else {
        //一般是fast环境
        dest = autoreleaseFast(POOL_BOUNDARY);
     }

看一下 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);
     }
 }

添加的话先拿到 next 指向最新添加到 autoreleased 对象的下个位置。然后 *next++ = obj; 这行代码的意思是将obj 放到这个next 指向的这个地址。然后再将next 指针向下移动8字节(++)。同时是一个初始化方法。

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

添加的obj  初始化

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


 public:
 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;
 }
 

page 满的话开分页

 static __attribute__((noinline))
 id *autoreleaseFullPage(id obj, AutoreleasePoolPage *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 循环,递归去找下一页,如果还是满的继续递归。直到找到没有满的,或者是刚好最后一个满的话新创建一页 然后add。
 
 如果找到的 page 会被设置成(setHotPage(page)) PAGE (hot)聚焦页(也就是给了一个标记),其他的page 就是 PAGE (full)

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


出栈

释放

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

ctxt 是上下文。从最开始的 main.cpp

 __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
 ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}

可以看到传的是一个 objc_autoreleasePoolPush(); 的返回值,也就是标记 析构  构造函数 对应的 对象。

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();
         //如果是空的池就把 page->begin() 赋值给 token
         token = page->begin();
         } else {
            page = pageForPointer(token);
         }
     stop = (id *)token;
     //判断是否等于 nil
     if (*stop != POOL_BOUNDARY) {

         // 第一个结点 - 没有父结点  parent 父结点
         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 (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
        return popPageDebug(token, page, stop);
     }
 
     return popPage<false>(token, page, stop);
 }
 #   define EMPTY_POOL_PLACEHOLDER ((id*)1)
 
EMPTY_POOL_PLACEHOLDER 是一个空的占位的,表示没有压栈任何对象。这个时候直接把 page->begin() 给 占位符
 
 #   define POOL_BOUNDARY nil

stop!=nil; 的时候

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

template<bool allowDebug>
 static void
 popPage(void *token, AutoreleasePoolPage *page, id *stop)
 {
     if (allowDebug && PrintPoolHiwat) printHiwat();
     //直接进行 release。
     page->releaseUntil(stop);

release 不断的向上递归,直到this->next == stop  next(最新对象的下个位置,也就是最后一个位置)
 递归找到obj (id obj = *--page->next;) 直接进行 objc_release(obj);

void releaseUntil(id *stop)
 {
     while (this->next != stop) {

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

判断每一页是否递归完成  next == begin()节点

 bool empty() {
    return next == begin();
 }


 找到的obj 进行 objc_release 释放对象

 __attribute__((aligned(16), flatten, noinline))
 void
 objc_release(id obj)
 {
     if (!obj) return;
     if (obj->isTaggedPointer()) return;
     return obj->release();
 }

对象释放完之后然后一层一层的去把表杀掉,同时设置 setHotPage(nil)

 page->kill();
 setHotPage(parent);
 static AutoreleasePoolPage *pageForPointer(const void *p)
 {
    return pageForPointer((uintptr_t)p);
 }


3、不同线程 @autoreleasePool

@autoreleasePool 嵌套


 MRC 下必须要要手动添加 autorelease
 1、@autoreleasePool 与线程关联,不同的线程 @autoreleasePool 是不相同的。
 2、@autoreleasePool 嵌套  -  只会创建一个 page 但是有多个(嵌套几层有几个)个边界(哨兵对象)
 为什么是多个边界(哨兵对象),因为他们有不同的作用域空间,
 例如:

int main(int argc, const char * argv[]) {
     @autoreleasepool {
         // insert code here...
         // 1 + 504 + 505 + 505
         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);
                 _objc_autoreleasePoolPrint();
             }
         });
         _objc_autoreleasePoolPrint();
     }
 
     // 3*16 + 8 = 56
     // 504
     sleep(2);
     return 0;
 }

有两个边界(哨兵对象)

 通过打印可以看到不同的线程 autoreleasePool 绑定是不同的
 第一个绑定在 thread 0x1000d2dc0
 第二个绑定在 thread 0x700005b04000

 

所以AutoreleasePool的释放有如下两种情况。
 1、Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
 2、手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool

 在 ARC 环境下, 方法在将要执行结束的时候, 局部变量的指针都会被置为 nil
 

 总结

 1、clang
 __AtAutoreleasePool
 __autoreleasepool
 
 2、__AtAutoreleasePool
 构造函数  objc_autoreleasePoolPush();
 析构函数  objc_autoreleasePoolPop(atautoreleasepoolobj);
 
 3、通过汇编
 可以知道 运行的时候先进行了 objc_autoreleasePoolPush 构造
 然后执行完,直接进行了 objc_autoreleasePoolPop 析构
 
 4、push

  1.  栈属性 先进后出
  2.  存放的对象 边界、我们创建的需要释放的对象
  3.  是一个双向联保结构
  4.  自动释放池与线程是一一对应的关系

 
 5、AutoreleasePoolPageData
 固有属性 56字节
 magic;              //16   检查结构体完整性
 next;                 //8   next指针,指向最新添加的 autoreleased 对象的下个位置,用来进行偏移的。初始化时指向begin();
 thread;            // 8  指向当前线程  用来取出自动释放池
 parent;           //8     指向父结点,第一个结点的 parent 值为nil
 child;              //8    指向子结点,最后一个结点的 child 值为nil
 depth;             //4    深度 链表链层的深度 从0开始,往后递增1
 hiwat;             // 4    最大入栈数量
 
 6、AutoreleasePoolPage
 每页可以容纳 505 个对象,第一页会加入一个特殊的对象 边界。也就是说第一页可以加 504个自己的对象,之后的都是505个。
 
 7、入栈
 通过next 指针存取对应地址,然后next 指针指向向下移动8字节
 如果page 满的话新创建一页 new AutoreleasePoolPage(page);
 然后 add
 存入的这一页  setHotPage(page)  设置为聚焦页
 
 8、出栈 也就是释放
 objc_autoreleasePoolPop(void *ctxt)   ctext 上下文,对应构造函数

  1.  判断是否是空池  如果是空池,把begin() 赋值给 token
  •  stop = (id*)token
  •  判断token是否等于 nil
  •  不等于nil
  •  是都是第一节点,是否有父节点(如果是第一个节点又有父节点的说明有问题,直接不处理)
  •  然后 badpop(token)
  1.  通过next 不断向上一页一页的去递归去释放,直到next = stop(begin())起始位置为止。每一页有begin(),同样通过next 去判断,当前页是否递归完。
  2.  对象释放之后然后重下向上一层一层的去把表杀掉,

 
 9、与线程管理
  @autoreleasePool 与线程关联,不同的线程 @autoreleasePool 是不相同的。
 
  10、@autoreleasePool 嵌套
 只会创建一个 page 但是有多个(嵌套几层有几个)个边界(哨兵对象)
  为什么是多个边界(哨兵对象),因为他们有不同的作用域空间,

发布了104 篇原创文章 · 获赞 13 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/shengdaVolleyball/article/details/105025511