08--内存管理--大话自动释放池

思考:一个对象什么时候加入自动释放池?

How AutoreleasePool

  • 自动释放池是一个抽象的概念
  • 自动释放池是一组page的集合
  • 自动释放池是维持page的栈的逻辑结构

需要理解这里的概念,一定是看过AutoreleasePoolPage源码的,参考链接中的很多文章中都做了非常详细的分析,可以先阅读参考链接中的文章。

What AutoreleasePoolPage

在源码中,是不存在AutoreleasePool这个数据结构的,但我们平时讲的都是都是“自动释放池”,而“自动释放池”所对应的词汇就是AutoreleasePool,并没有说错,因为“自动释放池”是一个抽象的概念。在内存中真正存在的数据结构是AutoreleasePoolPage——自动释放池页。
1、为什么是page?计算机的内存并不是一块连续的足够大的内存,而是分成非常多的页。
2、为什么是4096KB?绝大多数计算机的内存页大小就是4096KB,一个自动释放池页刚好占的大小就是内存中的一页,这样的设计可以减少page置换、减少pagefault,往往效率会更高。
在源码中,AutoreleasePoolPage被设计为双向链表结构,不知道双向链表是啥的可以联想下高铁,高铁是不需要掉头的,可以直接在铁轨上朝两个方向运行,而每一节车厢就是一个AutoreleasePoolPage。高铁的车厢中可以坐人,同样的,对象的地址就是存在AutoreleasePoolPage中。这样设计的原因自然是有需求——可以从头往尾、也可以从尾往头操作。譬如我们需要往自动释放池中加入新的对象,我们变需要从尾部添加,当我们需要释放所有对象的时候,就会从头开始,将所有的对象都释放掉。

What AutoreleasePool

说了这么多,可能仍然对自动释放池没有一个清晰的概念,实际上,自动释放池是有明确的定义的。根据上面对AutoreleasePoolPage的理解,我们可以假定内存中存在这样一个结构:page1-page2-page3,page3中的next指针指向4096的2000的位置。

在ARC环境下,@autoreleasepool {...}即表示一个自动释放池。

当运行到@autoreleasepool {}这行代码时,会执行push操作,插入一个POOL_SENTINEL哨兵对象,插入的位置就是page3next指针。具体点的过程是:

  1. 找到hotPage:page3;
  2. 首先插入一个POOL_SENTINEL哨兵对象;
  3. ...中需要加入自动释放池的对象地址挨个记录到page3中;(为什么是挨个?在代码块中,我们可能会创建非常多的对象,系统并不是等这些对象都创建完了之后,一次性的将所有对象加入自动释放池,而是出现一个对象即往池子加入一个对象)
  4. 找到next指针,挨个记录需要加入自动释放池的对象;
  5. push操作会返回POOL_SENTINEL哨兵对象所在的地址;

到这里为止,哨兵对象POOL_SENTINEL的地址~next的地址这段内存就被称为“自动释放池”,这个自动释放池有多少个page,便由...中的对象决定,一个page中用于记录对象地址的空间大小是4032KB,也就是504个对象。如果...加入自动释放池的对象只有10个,那么这个自动释放池所表示的范围从2000~2096这段内存,如果...加入自动释放池的对象有1000个,在步骤4中,还存在开辟新的page的操作,当前的结构就会变成:page1-page2-page3-page4-page5,其中:page3(2000)-page4-page5的这段内存表示当前的自动释放池。

当代码运行到作用域结束的地方时},执行pop操作,首先会拿到步骤5返回的POOL_SENTINEL哨兵对象的地址,根据这个地址可以确定该自动释放池的起点,然后就开始执行释放操作。具体点的过程是:

  1. 找到hotPage:page5;(假定加入了1000个对象)
  2. 释放page5中的所有对象,如果是对象,则发送release消息;
  3. page5释放完了,找到parentPage,也就是page4,并将page4设置为hotPage,释放page4中的所有对象;
  4. page4页释放完了,找到parentPage,也就是page3,并将page3设置为hotPage,开始释放page3中对象;
  5. 当释放到POOL_SENTINEL哨兵对象的地址时,程序便知道这个自动释放池已经释放完了,next指针又回到的2000的位置了。在释放对象的时候,需要先判断是否是对象,原因就是可能在某个地方取出来的是POOL_SENTINEL哨兵对象,而POOL_SENTINEL哨兵对象是nil,不需要给它发送release消息。

假如计算机的内存是连续的,分给自动释放池的内存也足够大,那么自动释放池应该是一种什么样的结构呢?我们可以将page设置为一个栈的结构即可,当push时,往栈顶添加一个POOL_SENTINEL哨兵对象,然后继续push对象,当pop时,一直pop到POOL_SENTINEL哨兵对象的地址停止,这个时候就不需要page了,pool就可以定义成真正的数据结构了,然而这只是假设

Number Of AutoreleasePool

自动释放池是由一些列AutoreleasePoolPage维持的抽象数据结构。既抽象,在整个程序中没有AutoreleasePool这个对象,又具体,若干个AutoreleasePoolPage组成一个AutoreleasePool。既然AutoreleasePool在抽象结构上有了明确的定义,那么自然可以按个数区分了。这里,先不考虑子线程,只关注主线程的自动释放池。
1、runloop:主线程的runloop在启动的时候,会率先注册一个自动释放池的回调,确保整个runloop的代码都在自动释放池中,也就是当runloop工作的时候需要一个AutoreleasePool的环境。
2、场景一:随着程序的执行,越来越多的对象需要加入自动释放池,而我们没有主动的调用过@autoreleasepool {...}代码,尽管AutoreleasePoolPage一直在增加,但AutoreleasePool只有一个。
3、场景二:当我们在代码中主动调用@autoreleasepool {...}时,系统会往当前的AutoreleasePoolPage中插入一个POOL_SENTINEL哨兵对象,代码块中的对象将会保存到POOL_SENTINEL哨兵对象后面。自POOL_SENTINEL哨兵对象之后的结构被称为新的自动释放池,当出了代码块之后,这个自动释放池就会被释放,一直释放到POOL_SENTINEL哨兵对象的位置。
所以,从逻辑上来看,一个线程同一时间最多只会存在两个自动释放池,一个是随runloop创建的,一个是临时的。

Life Of AutoreleasePool

尽管AutoreleasePool是一个逻辑上的对象,但只要是对象,就必然有生命。

临时AutoreleasePool

对于临时AutoreleasePool,就像ARC里面的临时变量一样,用完了就释放了。临时的AutoreleasePool也一样遵循这个规则。@autoreleasepool {...},push的时候,临时AutoreleasePool被创建出来;pop时,临时AutoreleasePool被释放。

全局AutoreleasePool

  • 主线程,在runloop即将退出或进入休眠的时候,会释放当前自定释放池,并创建一个新的自动释放池;
  • 子线程,runloop未启动,当产生了Autorelease对象时,自动创建自动释放池,当线程销毁时,释放自定释放池;
  • 子线程,runloop已启动,和主线程模式下的自动释放池过程一致。

要理解这个过程,需要了解线程、runloop、Autoreleasepool之间的关系

Thread & Runloop & AutoreleasePool

Thread & Runloop

  1. 线程和Runloop是一一对应的,每个线程都有一个对象的Runloop对象,对应关系保存在全局字典里面;
  2. 主线程的Runloop由系统创建并自启动;
  3. 子线程的Runloop不会自启动,需要获取时才会创建(类似懒加载)。系统并没有提供直接创建Runloop的方法,在第一次获取的时候创建;
  4. 当线程结束的时候,其对应的Runloop也会被销毁;

Runloop & AutoreleasePool

  1. AutoreleasePool并不依赖Runloop,也就是说不管线程的Runloop有没有启动,AutoreleasePool都会正常工作;
  2. Runloop依赖AutoreleasePool,表现形式为_wrapRunLoopWithAutoreleasePoolHandler()
    • Observer监听即将进入Loop的回调,内部会创建自动释放池,且优先级最高,保证自动释放池的创建发生在所有的回调之前;
    • Observer监听准备进入休眠的回调,内部会创建新的自动释放池,并释放旧的自动释放池,且优先级最低,保证所有回调处理完成之后才释放旧的自动释放池;

Thread & AutoreleasePool

Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.

苹果的文档中描述了线程和自动释放池的三个要点:

  1. 每个线程都维护自己的自动释放池栈;
  2. 新的自动释放池会被添加到栈顶,释放的时候,会从栈顶移除;
    3.当线程销毁时,线程关联的所有自动释放池都会被销毁;

然而它们还有一些不可告人的秘密,其中一个就是前面提到的——当产生了Autorelease对象时,自动创建自动释放池。还有一个不可告人的秘密来自于线程的TLS(Thread Local Storage)私有存储空间。 Objective-C 小记(9)__strong
可以看看先这篇文章中关于objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue

Autorelease pool blocks provide a mechanism whereby you can relinquish ownership of an object, but avoid the possibility of it being deallocated immediately (such as when you return an object from a method). Typically, you don’t need to create your own autorelease pool blocks, but there are some situations in which either you must or it is beneficial to do so.

苹果的文档中指出,@autoreleasepool {...}可以避免对象立刻释放,还举了个栗子,当从一个方法中返回一个对象时。

  • 当方法返回对象时,会调用objc_autoreleaseReturnValue返回
  • 在接受返回对象的地方,调用objc_retainAutoreleasedReturnValue接受

正常操作是,在对象返回时,将对象加入自动释放池,因为自动释放池在销毁的时候会向对象发送release消息,而加入自动释放池并不会retain对象,所以需要在接受返回值的地方通过objc_retainAutoreleasedReturnValue方法,将对象retain。如果仅仅是加入自动释放池和持有对象,直接调用方法objc_autoreleaseobjc_retain即可实现,这里却用了两个新的方法。

计算机领域一直保持着物尽其用的良好传统,所以不会浪费任何一点空间的。每个线程都有一个自己存放小秘密的私有存储空间TLS,如果TLS里面还有多余的空间,那么就不要浪费了,直接来保存这个返回对象,取的时候,就从TLS里面取,那么对象就不用加入自动释放池,也可以保证延迟释放了。所以有了objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue两个方法,其实就是先判断TLS里面是否可以存对象,不能存才会存到自动释放池里面,两个方法成对出现,通过一个标记来判断对象存在哪里,所以也不会出现对象存在自动释放池,但从TLS里面取的情况。

相关源码:

// Prepare a value at +1 for return through a +0 autoreleasing convention.
id 
objc_autoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}

// Prepare a value at +0 for return through a +0 autoreleasing convention.
id 
objc_retainAutoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus0)) return obj;

    // not objc_autoreleaseReturnValue(objc_retain(obj)) 
    // because we don't need another optimization attempt
    return objc_retainAutoreleaseAndReturn(obj);
}

// Accept a value returned through a +0 autoreleasing convention for use at +1.
id
objc_retainAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

    return objc_retain(obj);
}

// Accept a value returned through a +0 autoreleasing convention for use at +0.
id
objc_unsafeClaimAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus0) return obj;

    return objc_releaseAndReturn(obj);
}

Ahead & Delay

对于自动释放池的延迟释放机制,前面已经讲了很多了,在开发者不去主动指明时,系统会自动甄别那些需要延迟释放的对象,并将其加入自动释放池。

对于自动释放池的提前释放机制,苹果在文档中也有指出:

  • If you are writing a program that is not based on a UI framework, such as a command-line tool.

  • If you write a loop that creates many temporary objects.

    You may use an autorelease pool block inside the loop to dispose of those objects before the next iteration. Using an autorelease pool block in the loop helps to reduce the maximum memory footprint of the application.

  • If you spawn a secondary thread.

    You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects.

  1. 编写的不是基于UI框架的程序,例如命令行工具;
  2. 通过循环方式创建大量临时对象;(系统对于快速枚举的block中有添加自动释放池,而for循环中并没有自动添加)
  3. 使用非Cocoa程序创建的子线程;

Autorelease & RetainCount & Weak & Associate

objc内存管理的四大机制:

  1. 自动释放池
  2. 引用计数
  3. 弱引用表
  4. 关联对象

对于弱引用表、关联对象这两个知识点都是存在两个单独全局表,不影响对象的生命周期。引用计数其本身就是对象是否会被释放的标识,而自动释放池的作用也是对池子里面的所有对象发送release消息,这个也会将引用计数-1。自动释放池存的都是对象的地址,并不会让对象的引用计数+1,为了避免引用计数的加减不一致,所以在对象被加入自动释放池的时候,都会先retain一次。在MRC里面的Getter方法:

- (Person *)person {
    return [[_person retain] autorelease];
}

参考文章

  • Advanced Memory Management Programming Guide
  • 黑幕背后的Autorelease
  • 自动释放池的前世今生-- 深入解析 autoreleasepool
  • iOS内存管理-深入解析自动释放池
  • Objective-C 小记(9)__strong
  • iOS开发笔记(七):深入理解 Autorelease

你可能感兴趣的:(08--内存管理--大话自动释放池)