开启生长之旅!这是我参与「日新计划 12 月更文挑战」的第6天,点击检查活动概况

一、AutoreleasePool初探

咱们先看一个比如代码:

@autoreleasepool {
    // insert code here...
    NSLog(@"Hello world!");
    NYPerson *p = [NYPerson alloc];
    _objc_autoreleasePoolPrint();//检查主动开释池
  }

运转检查打印结果:

iOS内存管理之AutoreleasePool
在打印中没有看到NYPerson的内存信息。

假如咱们加上 __autoreleasing NYPerson *p = [NYPerson alloc]; 在看下打印:

iOS内存管理之AutoreleasePool
现在咱们看到了NYPerson的内存信息,NYPerson目标被增加到主动开释池中。

为什么不增加__autoreleasing关键字,不会增加到autoreleasepool中呢?

由于alloc、new、copy、allocWith...、copyWith...等关键字创立的目标是不会增加到autoreleasepool中的。

那么假如增加如下代码:

NSString *str = [NSString stringWithFormat:@"%@",@"123"];

会增加到autoreleasepool吗?答应是:不会。

iOS内存管理之AutoreleasePool
为什么呢?由于这个目标是tagged point目标。

假如改成如下代码:

NSString *str = [NSString stringWithFormat:@"%@",@"1234567890"];

答应是:会增加到autoreleasepool。由于现已不是tagged point目标,而且一个nsstring目标了。超越9个字符就无法保存在地址中了。

iOS内存管理之AutoreleasePool

咱们在来看一段代码:

    for (int i = 0; i < 100000000; i ++) {
      NSString *obj = [NSString stringWithFormat:@"%@",@"1234567890"];
  }

来看运转:

iOS内存管理之AutoreleasePool
发现履行了100000000次创立NSString目标,cpu和内存快速上涨到1G。

接下来,咱们来修改代码:

    for (int i = 0; i < 100000000; i ++) {
    @autoreleasepool {
      NSString *obj = [NSString stringWithFormat:@"%@",@"1234567890"];
    }
  }

咱们发现,只有cpu上涨了,内存保持在8.9MB没有太大的变化。

iOS内存管理之AutoreleasePool
为什么加了AutoreleasePool的代码能够确保内存不大涨呢?AutoreleasePool底层做了什么呢?咱们接着探究。

小结

AutoreleasePool(主动开释池)是OC中的一种内存主动收回机制,它能够推迟参加AutoreleasePool中的变量release的机遇。在正常状况下,创立的变量会在超出起作用域的时分release,可是假如将变量参加AutoreleasePool,那么release将会推迟履行

二、AutoreleasePool的数据结构

咱们持续探究:

@autoreleasepool {
    // insert code here...
    NSLog(@"Hello, World!");
  }

clang main.m 生成 main.cpp

iOS内存管理之AutoreleasePool
翻开main.cpp检查源码:
iOS内存管理之AutoreleasePool
查找__AtAutoreleasePool源码:
iOS内存管理之AutoreleasePool
咱们看到代码:

atautoreleasepoolobj = objc_autoreleasePoolPush();//结构函数
objc_autoreleasePoolPop(atautoreleasepoolobj);//析构函数

查找objc_autoreleasePoolPush进入源码:

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

依据AutoreleasePoolPage::push(); 来看下AutoreleasePoolPage的数据结构

//AutoreleasePoolPage 结构承继自AutoreleasePoolPageData
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
//........................省掉...........................//
}

发现AutoreleasePoolPage 结构承继自AutoreleasePoolPageData 数据结构:

struct AutoreleasePoolPageData
{
//........................省掉...........................//
magic_t const magic;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
//........................省掉...........................//
};
  • magic :用来校验AutoreleasePoolPage的结构是否完好;
  • next :指向最新增加的autoreleased目标的下一个位置,初始化时指向begin();
  • thread:指向当前线程;
  • parent:指向父结点,第一个结点的parent值为nil;
  • child:指向子结点,最后一个结点的child值为nil;
  • depth:代表深度,从0开始,往后递加1;
  • hiwat:代表 high water mark 最大入栈数量标记;

三、AutoreleasePool的增加数据

咱们先来看下AutoreleasePoolPage 的注释:

/***********************************************************************
 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. 
   //每个指针要么是行将开释的目标,要么是一个POOL_BOUNDARY,作为主动开释池的鸿沟。
   (主动开释池里面的数据有两种类型,一种是行将被开释的目标,另一种是POOL_BOUNDARY,
   超出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.
 //有一个指向该池的POOL_BOUNDARY的指针。当池被弹出时,每个比岗兵更热的目标
   都会被开释。(在调用pop函数的时分,整个主动开释池里面的目标都会被开释。)
 The stack is divided into a doubly-linked list of pages. Pages are added
  and deleted as necessary. 
   //栈被分红一个双向链接的页面列表。依据需要增加和删去页面。(主动开释池是一个双向列表
   的栈结构。它的每个节点都是AutoreleasePoolPage。)
 Thread-local storage points to the hot page, where newly autoreleased
  objects are stored. 
   //线程的本地存储指向热页,其间存储了新的主动开释目标。(
   主动开释池里面的数据也有线程缓存)
**********************************************************************/

咱们在进入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);//初始化 newpage
    } else {
      dest = autoreleaseFast(POOL_BOUNDARY);//参加岗兵目标
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
  }

检查autoreleaseNewPage(POOL_BOUNDARY)源码:

id *autoreleaseNewPage(id obj)
  {
    AutoreleasePoolPage *page = hotPage();//线程的快速缓存中找到AutoreleasePoolPage 没有回来nil
    if (page) return autoreleaseFullPage(obj, page);
    else return autoreleaseNoPage(obj);//nil 就履行创立AutoreleasePoolPage第一页并增加增加岗兵目标
  }

这个hotpage函数做了什么?得进入源码检查:

static inline AutoreleasePoolPage *hotPage()
  {
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
      tls_get_direct(key);//线程的快速缓存中找到AutoreleasePoolPage
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;//没找到回来nil
    if (result) result->fastcheck();
    return result;
  }

大概意思是:线程的快速缓存中找到AutoreleasePoolPage则回来nil,假如有值就回来找到的AutoreleasePoolPage

先看没有找到AutoreleasePoolPage的状况:autoreleaseNoPage(obj)做了什么?

iOS内存管理之AutoreleasePool
主要是说,创立一个AutoreleasePoolPage并设置它为热页,把传入的obj(岗兵目标)增加到这页中(第一页)。

在看下new AutoreleasePoolPage(nil);详细做了什么?

iOS内存管理之AutoreleasePool
看下begin()做了什么?

    //this=AutoreleasePoolPage+(page巨细56)等于内存平移,的地址
  id * begin() {
    return (id *) ((uint8_t *)this+sizeof(*this));
  }

iOS内存管理之AutoreleasePool
然后咱们接着看下autoreleaseFullPage(obj, page); 线程的快速缓存中找到AutoreleasePoolPage的状况。

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);//子节点也满了,创立一个AutoreleasePoolPage
    } while (page->full());//判别是否满了
    setHotPage(page);//设置成热页
    return page->add(obj);
  }

找到这个page,假如满了->找子节点。假如子节点也满了,创立一个AutoreleasePoolPage页。 设置page为热页,并增加obj到page中。

双向链表示意图:

iOS内存管理之AutoreleasePool

四、Autorelease目标的开释

先来看下autorelease和runloop的联系图:

iOS内存管理之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,优先级最低,确保其开释池产生在其他一切回调之后。

⼦线程的autorelease目标

子线程中一旦呈现了Autorelease目标,就会创立主动开释池。子线程中创立主动开释池的方法和主线程是相同的,也是调用autoreleaseNoPage。也便是说在子线程中的Autorelease目标,咱们同样不需要进行手动的内存管理,也不会内存走漏。

AutoreleasePool的开释数据

咱们在源码中看到:

iOS内存管理之AutoreleasePool
AutoreleasePoolPage 的巨细在4KB 4096。 一切一页page能够存 4096-56-8(岗兵)= 4032/8 = 504 个目标。

咱们在经过一个比如验证,上述理论:

@autoreleasepool {
    for(int i=0;i<1010;i++){
      __autoreleasing NYPerson *p = [NYPerson alloc];
    }
    _objc_autoreleasePoolPrint();//检查主动开释池
  }

检查打印:

iOS内存管理之AutoreleasePool
第一页为full cold页存504个目标,岗兵目标占用一个。
iOS内存管理之AutoreleasePool
第二页为full页存505个目标。
iOS内存管理之AutoreleasePool
第三页为hot页存了一个目标。

所以应该autoreleasepoolpage第一页能够存504个目标,其他页能够存505个目标。

咱们接着看objc_autoreleasePoolPop(atautoreleasepoolobj)进入到源码:

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

在进入pop源代码:

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);//设置热页为nil
      }
      // Pool was used. Pop its contents normally.
      // Pool pages remain allocated for re-use as usual.
      page = coldPage();//获取cold页
      token = page->begin();//把cold页第一个设置为岗兵
    } else {
      page = pageForPointer(token);//token便是岗兵,获取岗兵地点页
    }
    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 (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
      return popPageDebug(token, page, stop);
    }
    return popPage<false>(token, page, stop);//开释链表中的目标到stop位置。
  }

先看下pageForPointer做了什么:

iOS内存管理之AutoreleasePool
在接着看popPage<false>(token, page, stop);源码:
iOS内存管理之AutoreleasePool
进行跟踪page->releaseUntil的源码:

void releaseUntil(id *stop)
  {
    while (this->next != stop) {//当前页的下一页不是岗兵页
      // Restart from hotPage() every time, in case -release
      // autoreleased more objects
      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();
//........................省掉...........................//
      memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
      page->protect();
      if (obj != POOL_BOUNDARY) {//判别不是岗兵
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
        // release count+1 times since it is count of the additional
        // autoreleases beyond the first one
        for (int i = 0; i < count + 1; i++) {
          objc_release(obj);
        }
#else
        objc_release(obj);//将这段代码插入开释池中的每目标
#endif
      }
    }
    setHotPage(this);//设置当前页为热页
    //........................省掉...........................//
  }

小结

objc_autoreleasePoolPop函数经过token(岗兵目标)地点的page,怎么依据hotpage经过releaseUntil函数从hotpage页一直插入开释代码objc_release(obj)直到token(岗兵目标)停止。

在ARC环境下,autorelease 目标在什么时分开释?

  1. 假如是手动增加到@autoreleasepool里面的autorelease目标,是在出了@autoreleasepool的作用域之后被开释。
  2. 假如是体系增加到主动开释池的autorelease目标的开释机遇便是由RunLoop操控的,会在每一次的RunLoop循环结束时开释。
  3. 假如是子线程没有开启RunLoop,便是在子线程毁掉的时分开释。

总结

  1. AutoreleasePool初探AutoreleasePool(主动开释池)是OC中的一种内存主动收回机制,它能够推迟参加AutoreleasePool中的变量release的机遇。在正常状况下,创立的变量会在超出起作用域的时分release,可是假如将变量参加AutoreleasePool,那么release将会推迟履行
  2. AutoreleasePool的数据结构:发现AutoreleasePoolPage 结构承继AutoreleasePoolPageData 数据结构,56个字节是一个双向链表结构。
  3. AutoreleasePool的增加数据:经过objc_autoreleasePoolPush函数压栈,第一页第一个会创立一个岗兵目标也便是autoreleaseNoPage(obj)函数,然后autoreleaseFullPage(obj, page); 线程的快速缓存中找到热页增加目标到热页中。
  4. Autorelease目标的开释objc_autoreleasePoolPop函数经过token(岗兵目标)地点的page,怎么依据hotpage经过releaseUntil函数从hotpage页一直插入开释代码objc_release(obj)直到token(岗兵目标)停止。