【iOS】——AutoReleasePool底层原理及总结

打印 上一主题 下一主题

主题 766|帖子 766|积分 2298

主动开释池

AutoreleasePool主动开释池用来延长对象的开释机遇,将对象加入到主动开释池后这个对象不会立即开释,等到主动开释池被销毁后才将里边的对象开释。
主动开释池的生命周期


  • 从程序启动到加载完成,主线程对应的runloop会处于休眠状态,等候用户交互来唤醒runloop
  • 用户的每一次交互都会启动一次runloop,用于处理用户的全部点击、触摸事件等
  • runloop在监听到交互事件后,就会创建主动开释池,并将全部延长开释的对象添加到主动开释池中
  • 在一次完整的runloop竣事之前,会向主动开释池中全部对象发送release消息,然后销毁主动开释池
AutorealeasePool结构

每一个AutorealeasePool都是由一系列的 AutoreleasePoolPage 构成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节 。
AutorealeasePool就是由AutoreleasePoolPage构成的双向链表,AutoreleasePoolPage是双向链表的节点

AutoreleasePoolPage的界说如下:
  1. class AutoreleasePoolPage
  2. {
  3.     //magic用来校验AutoreleasePoolPage的结构是否完整
  4.     magic_t const magic;                   // 16字节
  5.     //指向最新添加的autoreleased对象的下一个位置,初始化时指向begin();
  6.     id *next;                              // 8字节
  7.     //thread指向当前线程
  8.     pthread_t const thread;                // 8字节
  9.     //parent指向父节点,第一个节点的parent指向nil;
  10.     AutoreleasePoolPage * const parent;    // 8字节
  11.     //child 指向子节点,第一个节点的child指向nil;
  12.     AutoreleasePoolPage *child;            // 8字节
  13.     //depth 代表深度,从0开始往后递增1;
  14.     uint32_t const depth;                  // 4字节
  15.     //hiwat 代表high water mark;
  16.     uint32_t hiwat;                        // 4字节
  17.     ...
  18. }
复制代码
主动开释池中的栈

如果我们的一个 AutoreleasePoolPage 被初始化在内存的 0x100816000 ~ 0x100817000 中,它在内存中的结构如下:

其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的 0x100816038 ~ 0x100817000 都是用来存储加入到主动开释池中的对象
begin() 和 end() 这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。
next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中

POOL_SENTINEL(哨兵对象)

POOL_SENTINEL 就是哨兵对象,它是一个宏,值为nil,标记着一个主动开释池的边界。
在每个主动开释池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到主动开释池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。
而当方法 objc_autoreleasePoolPop 调用时,就会向主动开释池中的对象发送 release 消息,直到第一个 POOL_SENTINEL:

AutoreleasePool实现

在每个文件中的main.m文件中都有下面这段代码:
  1. int main(int argc, char * argv[]) {
  2.     @autoreleasepool {
  3.         return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  4.     }
  5. }
复制代码
在整个main函数中只有一个autoreleasepool块,在块中只包含了一行代码,这行代码将全部的事件、消息全部交给了 UIApplication 来处理,也就是说整个 iOS 的应用都是包含在一个autoreleasepool的 block 中的
将main.m文件通过$ clang -rewrite-objc main.m重新编译生成发现aotuoreleasepool 被转换为为一个 __AtAutoreleasePool 结构体:
  1. struct __AtAutoreleasePool {
  2.   __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  3.   ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  4.   void * atautoreleasepoolobj;
  5. };
复制代码
这个结构领会在初始化时调用 objc_autoreleasePoolPush() 方法,会在析构时调用 objc_autoreleasePoolPop 方法。
以是实际上 main 函数在实际工作时其实是如许的:
  1. int main(int argc, const char * argv[]) {
  2.     {
  3.         void * atautoreleasepoolobj = objc_autoreleasePoolPush();
  4.         // do whatever you want
  5.         objc_autoreleasePoolPop(atautoreleasepoolobj);
  6.     }
  7.     return 0;
  8. }
复制代码
以是autoreleasepool的实现主要靠objc_autoreleasePoolPush()和objc_autoreleasePoolPop() 来实现
  1. void *objc_autoreleasePoolPush(void) {
  2.     return AutoreleasePoolPage::push();
  3. }
  4. void objc_autoreleasePoolPop(void *ctxt) {
  5.     AutoreleasePoolPage::pop(ctxt);
  6. }
复制代码
而在objc_autoreleasePoolPush()和objc_autoreleasePoolPop() 中又分别调用了AutoreleasePoolPage类的push和pop方法。
入栈


objc_autoreleasePoolPush()

起首调用objc_autoreleasePoolPush()
  1. void *objc_autoreleasePoolPush(void) {
  2.     return AutoreleasePoolPage::push();
  3. }
复制代码
AutoreleasePoolPage::push()

  1. static inline void *push() {
  2.    return autoreleaseFast(POOL_SENTINEL);
  3. }
复制代码
该函数就是调用了关键的方法 autoreleaseFast,并传入哨兵对象POOL_SENTINEL
autoreleaseFast()

  1. static inline id *autoreleaseFast(id obj)
  2.     {
  3.         //1. 获取当前操作页
  4.         AutoreleasePoolPage *page = hotPage();
  5.         //2. 判断当前操作页是否满了
  6.         if (page && !page->full()) {
  7.             //如果未满,则压桟
  8.             return page->add(obj);
  9.         } else if (page) {
  10.             //如果满了,则安排新的页面
  11.             return autoreleaseFullPage(obj, page);
  12.         } else {//页面不存在,则新建页面
  13.             return autoreleaseNoPage(obj);
  14.         }
  15.     }
复制代码
  hotPage 可以为当前正在使用的 AutoreleasePoolPage
  上面代码主要分为三种情况:


  • 有 hotPage 并且当前 page 不满
调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中


  • 有 hotPage 并且当前 page 已满
调用 autoreleaseFullPage 初始化一个新的页接着调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中


  • 无hotPage
调用 autoreleaseNoPage 创建一个 hotPage,接着调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
总的来说这三种情况末了都会调用 page->add(obj) 将对象添加到主动开释池中。
page->add 添加对象

  1. //入桟对象
  2. id *add(id obj)
  3.     {
  4.         ASSERT(!full());
  5.         unprotect();
  6.         //传入对象存储的位置(比' return next-1 '更快,因为有别名)
  7.         id *ret = next;
  8.         //将obj压桟到next指针位置,然后next进行++,即下一个对象存储的位置
  9.         *next++ = obj;
  10.         protect();
  11.         return ret;
  12.     }
复制代码
这个方法其实就是一个压栈的操作,将对象加入 AutoreleasePoolPage 然后移动栈顶的指针
autoreleaseFullPage(当前 hotPage 已满)

  1. static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
  2.     do {
  3.         if (page->child) page = page->child;
  4.         else page = new AutoreleasePoolPage(page);
  5.     } while (page->full());
  6.     setHotPage(page);
  7.     return page->add(obj);
  8. }
复制代码
从传入的 page 开始遍历整个双向链表,直到查找到一个未满的 AutoreleasePoolPage
如果找到末了还是没找到创建一个新的 AutoreleasePoolPage
将找到的或者构建的page标记成 hotPage,然后调动上面分析过的 page->add 方法添加对象。
autoreleaseNoPage(没有 hotPage)

  1. static id *autoreleaseNoPage(id obj) {
  2.     AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
  3.     setHotPage(page);
  4.     if (obj != POOL_SENTINEL) {
  5.         page->add(POOL_SENTINEL);
  6.     }
  7.     return page->add(obj);
  8. }
复制代码
创建一个新的page,并且将新的page设置为hotpage。接着调用page->add 方法添加POOL_SENTINEL 对象,来确保在 pop 调用的时候,不会出现异常。末了,将 obj 添加到autoreleasepool中
既然当前内存中不存在 AutoreleasePoolPage,就要重新开始构建这个主动开释池的双向链表,也就是说,新的 AutoreleasePoolPage 是没有 parent 指针的。
出栈


objc_autoreleasePoolPop

  1. void objc_autoreleasePoolPop(void *ctxt) {
  2.     AutoreleasePoolPage::pop(ctxt);
  3. }
复制代码
在这个方法中传入不是哨兵对象而是传入其它的指针也是可行的,会将主动开释池开释到相应的位置。
AutoreleasePoolPage::pop

  1. template<bool allowDebug>
  2.     static void
  3.     popPage(void *token, AutoreleasePoolPage *page, id *stop)
  4.     {
  5.         if (allowDebug && PrintPoolHiwat) printHiwat();
  6.         //出桟当前操作页面对象
  7.         page->releaseUntil(stop);
  8.         // 删除空子项
  9.         if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
  10.             //特殊情况:在每个页面池调试期间删除所有内容
  11.             //获取当前页面
  12.             AutoreleasePoolPage *parent = page->parent;
  13.             //将当前页面杀掉
  14.             page->kill();
  15.             //设置父节点页面为当前操作页
  16.             setHotPage(parent);
  17.         } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
  18.             //特殊情况:当调试丢失的自动释放池时,删除所有pop(top)
  19.             page->kill();
  20.             setHotPage(nil);
  21.         } else if (page->child) {
  22.             //滞后:如果页面超过一半是满的,则保留一个空的子节点
  23.             if (page->lessThanHalfFull()) {
  24.                 page->child->kill();
  25.             }
  26.             else if (page->child->child) {
  27.                 page->child->child->kill();
  28.             }
  29.         }
  30.     }
复制代码
该静态方法统共做了三件事情:

  • 使用 pageForPointer 获取当前 token 地点的 AutoreleasePoolPage
  • 调用 releaseUntil 方法开释栈中的对象,直到 stop
  • 调用 child 的 kill 方法
   

  • pop之后,全部child page肯定都为空了,且当前page肯定是hotPage
  • 体系为了节省内存,判断,如果当前page空间使用少于一半,就开释掉全部的child page,如果当前page空间使用大于一半,就从孙子page开始开释,预留一个child page。
  pageForPointer 获取 AutoreleasePoolPage

pageForPointer 方法主要是通过内存地址的操作,获取当前指针地点页的首地址:
  1. static AutoreleasePoolPage *pageForPointer(const void *p) {
  2.     return pageForPointer((uintptr_t)p);
  3. }
  4. static AutoreleasePoolPage *pageForPointer(uintptr_t p) {
  5.     AutoreleasePoolPage *result;
  6.     uintptr_t offset = p % SIZE;
  7.     assert(offset >= sizeof(AutoreleasePoolPage));
  8.     result = (AutoreleasePoolPage *)(p - offset);
  9.     result->fastcheck();
  10.     return result;
  11. }
复制代码
将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为全部的 AutoreleasePoolPage 在内存中都是对齐的:
p = 0x100816048
p % SIZE = 0x48
result = 0x100816000
而末了调用的方法 fastCheck() 用来查抄当前的 result 是不是一个 AutoreleasePoolPage。
   通过查抄 magic_t 结构体中的某个成员是否为 0xA1A1A1A1。
  releaseUntil 开释对象

  1. void releaseUntil(id *stop)
  2. {
  3.     // 这里没有使用递归, 防止发生栈溢出
  4.     while (this->next != stop) { // 一直循环到 next 指针指向 stop 为止
  5.         // Restart from hotPage() every time, in case -release
  6.         // autoreleased more objects
  7.         AutoreleasePoolPage *page = hotPage(); // 取出 hotPage
  8.         while (page->empty()) { // 从节点 page 开始, 向前找到第一个非空节点
  9.             page = page->parent; // page 非空的话, 就向 page 的 parent 节点查找
  10.             setHotPage(page); // 把新的 page 节点设置为 HotPage
  11.         }
  12.         page->unprotect(); // 如果需要的话, 解除 page 的内存锁定
  13.         id obj = *--page->next; // 先将 next 指针向前移位, 然后再取出移位后地址中的值
  14.         memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); // 将 next 指向的内存清空为SCRIBBLE
  15.         page->protect(); // 如果需要的话, 设置内存锁定
  16.         if (obj != POOL_BOUNDARY) { // 如果取出的对象不是边界符
  17.             objc_release(obj); // 给取出来的对象进行一次 release 操作
  18.         }
  19.     }
  20.     setHotPage(this); // 将本节点设置为 hotPage
  21. #if DEBUG
  22.     // we expect any children to be completely empty
  23.     for (AutoreleasePoolPage *page = child; page; page = page->child) {
  24.         assert(page->empty());
  25.     }
  26. #endif
  27. }
复制代码
调用者是用 pageForPointer() 找到的, token 地点的 page 节点, 参数为 token. 这个函数主要操作流程就是, 从 hotPage 开始, 使用 next 指针遍历存储在节点里的 autorelease 对象列表, 对每个对象举行一次 release 操作, 并且把 next 指向的指针清空, 如果 hotPage 里面的对象全部清空, 则继续循环向前取 parent 并继续用 next 指针遍历 parent, 不停到 next 指针指向的地址为 token 为止. 因为 token 就在 this 里面, 以是这个时候的 hotPage 应该是 this.
kill() 方法

  1. void kill()
  2. {
  3.     // 这里没有使用递归, 防止发生栈溢出
  4.     AutoreleasePoolPage *page = this; // 从调用者开始
  5.     while (page->child) page = page->child; // 先找到最后一个节点
  6.     AutoreleasePoolPage *deathptr;
  7.     do { // 从最后一个节点开始遍历到调用节点
  8.         deathptr = page; // 保留当前遍历到的节点
  9.         page = page->parent; // 向前遍历
  10.         if (page) { // 如果有值
  11.             page->unprotect(); // 如果需要的话, 解除内存锁定
  12.             page->child = nil; // child 置空
  13.             page->protect(); // 如果需要的话, 设置内存锁定
  14.         }
  15.         delete deathptr; // 回收刚刚保留的节点, 重载 delete, 内部调用 free
  16.     } while (deathptr != this);
  17. }
复制代码
主动开释池中需要 release 的对象都已操作完成, 此时 hotPage 之后的 page 节点都已经清空了, 需要把这些节点的内存都接纳, 操作方案就是从末了一个节点, 遍历到调用者节点, 挨个接纳
总结



  • 主动开释池是由 AutoreleasePoolPage 以双向链表的方式实现的,每一个AutoreleasePoolPage所占内存大小为4096字节,其中56字节用于存储结构体中的成员变量。
  • autoreleasepool在初始化时,内部是调用objc_autoreleasePoolPush方法
  • autoreleasepool在调用析构函数开释时,内部是调用objc_autoreleasePoolPop方法
入栈(push)
在页中压栈平凡对象主要是通过next指针递增举行的


  • 当没有pool,即只有空占位符(存储在tls中)时,则创建页,压栈哨兵对象
  • 当页未满,将autorelease对象插入到栈顶next指针指向的位置(向一个对象发送autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置)
  • 当页满了(next指针马上指向栈顶),建立下一页page对象,设置页的child对象为新建页,新page的next指针被初始化在栈底(begin的位置),下次可以继续向栈顶添加新对象。

出桟(pop)
在页中出栈平凡对象主要是通过next指针递减举行的


  • 根据传入的哨兵对象地址找到哨兵对象所处的page
  • 在当前page中,将晚于哨兵对象插入的全部autorelease对象都发送一次- release消息,并向回移动next指针到正确位置.(从最新加入的对象不停向前清理,可以向前超过多少个page,直到哨兵地点的page(在一个page中,是从高地址向低地址清理))
  • 当页空了时,需要赋值页的parent对象为当前页

Autorelease对象什么时候开释

当创建了局部开释池时,会在@autoreleasepool{}的右大括号竣事时开释,及时开释对象大幅度低落程序的内存占用。在没有手动加@autoreleasepool的情况下,Autorelease对象是在当前的runloop迭代竣事时开释的(每个线程对应一个runloop),而它可以或许开释的缘故原由是体系在每个runloop迭代中都加入了主动开释池。
AutoreleasePool能否嵌套使用

可以嵌套使用,其目的是可以控制应用程序的内存峰值,使其不要太高 可以嵌套的缘故原由是因为主动开释池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的 主动开释池的多层嵌套其实就是不停的push哨兵对象,在pop时,会先开释里面的,在开释表面的
哪些对象可以加入AutoreleasePool?alloc创建可以吗?

使用new、alloc、copy关键字生成的对象和retain了的对象需要手动开释,不会被添加到主动开释池中 设置为autorelease的对象不需要手动开释,会直接进入主动开释池 全部 autorelease 的对象,在出了作用域之后,会被主动添加到近来创建的主动开释池中
AutoreleasePool的开释机遇是什么时候?

App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视一个事件:
监听 Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建主动开释池。其 order 是 -2147483647,优先级最高,包管创 建开释池发生在其他全部回调之前。
第二个 Observer 监视了两个事件:
BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 开释旧的池并创建新池;
Exit(即 将退出 Loop) 时调用_objc_autoreleasePoolPop()来开释主动开释池。这个 Observer 的 order 是 2147483647,优先级最低,包管其开释池子发生在其他全部回调之后。
thread 和 AutoreleasePool的关系

每个线程都有与之关联的主动开释池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,开释对象会被压栈到栈顶,线程停止时,会主动开释与之关联的主动开释池
RunLoop 和 AutoreleasePool的关系

主程序的RunLoop在每次事件循环之前之前,会主动创建一个 autoreleasePool 并且会在事件循环竣事时,执行drain操作,开释其中的对象

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

九天猎人

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表