概述
这篇博客的主要目的是解析autoreleasepool 的具体实现,因为底层的实现其实都是基于我们的数据结构的,最终我们的目的就是学习这些之前学习到的数据结构是怎么用于实际项目当中的,而不是仅仅限于书本的知识。该篇文章内容是参考iOS - 聊聊 autorelease 和 @autoreleasepool。有不对或者说的不够全面的点,欢迎留言大家补充。
一、iOS中的内存管理技术
内存管理是程序员在写bug的过程中必然会涉及到的主题,比如Java的垃圾回收机制 ,C++基于栈和析构函数的RAII(Resource Acquisition Is Initialization) 。在iOS中,使用的是引用计数的技术(Reference count )。
在iOS的不断发展过程中,又分为手动引用计数(MRC)和自动引用计数(ARC)。ARC在iOS 5之后引入,通过LLVM编译器和Runtime协作来进行自动管理内存。LLVM编译器会在编译时在合适的地方为 OC 对象插入retain 、release 和autorelease 代码,省去了在MRC(Manual Reference Counting )手动引用计数下手动插入这些代码的工作,减轻了开发者的工作量。Reference.
注:retain 可以简单理解为引用计数加一,release 和autorelease 是使引用计数减一。
在MRC下,当我们不需要一个对象的时候,要调用release 或autorelease 方法来释放它。调用release 会立即让对象的引用计数减 1 ,如果此时对象的引用计数为 0,对象就会被销毁。调用autorelease 会将该对象添加进自动释放池(autoreleasepool )中,它会在一个恰当的时刻自动给对象调用release ,所以autorelease 相当于延迟了对象的释放。 在ARC下,autorelease 方法已被禁用,我们可以使用__autoreleasing 修饰符修饰对象将对象注册到自动释放池中。详情请参阅《iOS - 老生常谈内存管理(三):ARC 面世 —— 所有权修饰符》。
二、autoReleasePool 自动释放池
2.1 autoReleasePool定义
截取两段Apple官方的定义是:
The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.
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.
从以上定义的两个片段可以得到:最开始的时候,主线程会创健一个autoReleasePool ,这是默认的autoReleasePool ,但是每个thread 都会维护自己的autoReleasePool 。autoReleasePool 也是和runloop 一一对应的。线程的 observer 观察到 RunLoop 即将开始和休眠/结束,会调用 autoreleasePool 的 push 和 pop 进行相关的操作。
2.2 创建autoReleasePool
- 在MRC下,可以使用
NSAutoreleasePool 或者@autoreleasepool 。建议使用@autoreleasepool ,苹果说它比NSAutoreleasePool 快大约六倍。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool release];
- 在ARC下,已经禁止使用NSAutoreleasePool类创建自动释放池,只能使用@autoreleasepool
@autoreleasepool {
}
三、autoReleasePool 原理分析
3.1 总体概述
在xcode中新建一个项目,选择oc语言,然后直接进入到改项目的源文件目录下,然后使用
xcrun -sdk iphonesimulator clang -rewrite-objc main.m
将默认生成的main.m 文件转换为main.cpp
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
我们先不管其他地方的代码,只截取其中关于autoreleasepool的一部分代码:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
int main(int argc, char * argv[]) {
{ __AtAutoreleasePool __autoreleasepool;
}
return 0;
}
从以上的cpp代码可以看出:
@autoreleasepool 底层是创建了一个__AtAutoreleasePool 结构体对象;- 在创建
__AtAutoreleasePool 结构体时会在构造函数中调用objc_autoreleasePoolPush ()函数,并返回一个atautoreleasepoolobj (POOL_BOUNDARY存放的内存地址,下面会讲到); - 在释放
__AtAutoreleasePool 结构体时会在析构函数中调用objc_autoreleasePoolPop ()函数,并将atautoreleasepoolobj 传入。
objc_autoreleasePoolPush ()和objc_autoreleasePoolPop ()两个函数其实是调用了AutoreleasePoolPage 类的两个类方法push()和pop()。所以@autoreleasepool 底层就是使用AutoreleasePoolPage 类来实现的。 AutoreleasePoolPage 的定义如下,就是一个双向链表节点的定义:
class AutoreleasePoolPage
{
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3;
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE;
#else
PAGE_MAX_SIZE;
#endif
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
......
}
整个程序运行过程中,可能会有多个AutoreleasePoolPage 对象。从它的定义可以得知:
- 自动释放池(即所有的AutoreleasePoolPage对象)是通过双向链表的形式组合在一起,内部的对象地址是按照栈的方式存储;
- 自动释放池与线程一一对应;
- 每个AutoreleasePoolPage对象占用4096字节内存,其中56个字节用来存放它内部的成员变量,剩下的空间(4040个字节)用来存放autorelease对象的地址。
3.2 AutoreleasePoolPage
AutoreleasePoolPage 之前说过其实就是双向链表的一个节点,那么这个节点的构造函数是如何的呢,我们来看一下源码:
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 ()方法的参数为parentPage- next指针指向该page中可以存储对象地址的开始位置
- child指针为nil
- 新创建的Page的depth加一
- 将新创建的Page的parent指针指向parentPage
- 将parentPage的child指针指向自己。
3.2 POOL_BOUNDARY
POOL_BOUNDARY 的前世叫做POOL_SENTINEL ,称为哨兵对象或者边界对象;POOL_BOUNDARY 用来区分不同的自动释放池,以解决自动释放池嵌套的问题;- 每当创建一个自动释放池,就会调用
push ()方法将一个POOL_BOUNDARY 入栈,并返回其存放的内存地址; - 当往自动释放池中添加
autorelease 对象时,将autorelease 对象的内存地址入栈,它们前面至少有一个POOL_BOUNDARY ; - 当销毁一个自动释放池时,会调用
pop ()方法并传入一个POOL_BOUNDARY ,会从自动释放池中最后一个对象开始,依次给它们发送release 消息,直到遇到这个POOL_BOUNDARY 。
3.3 autoReleasePool的操作
3.3.1 push
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) { // 出错时进入调试状态
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY); // 传入 POOL_BOUNDARY 哨兵对象
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
从代码中看出,当创建一个自动释放池时,会调用push ()方法。push ()方法中调用了autoreleaseFast ()方法并传入了POOL_BOUNDARY 哨兵对象。
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage(); // 双向链表中的最后一个 Page
if (page && !page->full()) { // 如果当前 Page 存在且未满
return page->add(obj); // 将 autorelease 对象入栈,即添加到当前 Page 中;
} else if (page) { // 如果当前 Page 存在但已满
return autoreleaseFullPage(obj, page); // 创建一个新的 Page,并将 autorelease 对象添加进去
} else { // 如果当前 Page 不存在,即还没创建过 Page
return autoreleaseNoPage(obj); // 创建第一个 Page,并将 autorelease 对象添加进去
}
}
autoreleaseFast() 中先是调用了hotPage() 方法获得未满的Page,从AutoreleasePoolPage 类的定义可知,每个Page的内存大小为4096个字节,每当Page满了的时候,就会创建一个新的Page。hotPage()方法就是用来获得这个新创建的未满的Page。 autoreleaseFast ()在执行过程中有三种情况:
- ① 当前Page存在且未满时,通过
page->add(obj) 将autorelease 对象添加到Page中的next指针所指向的位置,并将next指针指向这个对象的下一个位置,然后将该对象的位置返回。 - ② 当前Page存在但已满时,通过
autoreleaseFullPage(obj, page) 创建一个新的Page,并将autorelease 对象添加进去; - ③ 当前Page不存在,通过
autoreleaseNoPage (obj)创建第一个Page,并将autorelease 对象添加进去。该方法会判断是否有空的自动释放池存在,如果没有会通过setEmptyPoolPlaceholder ()生成一个占位符,表示一个空的自动释放池。接着创建第一个Page,设置它为hotPage。最后将一个POOL_BOUNDARY 添加进Page中,并返回POOL_BOUNDARY 的下一个位置。
小节总结:以上就是push操作的实现,往自动释放池中添加一个POOL_BOUNDARY ,并返回它存放的内存地址。接着每有一个对象调用autorelease 方法,会将它的内存地址添加进自动释放池中。
3.3.2 pop
pop ()方法的传参token即为POOL_BOUNDARY 对应在Page中的地址。当销毁自动释放池时,会调用pop()方法将自动释放池中的autorelease 对象全部释放(实际上是从自动释放池的中的最后一个入栈的autorelease 对象开始,依次给它们发送一条release消息,直到遇到这个POOL_BOUNDARY )。pop()方法的执行过程如下:
- ① 判断token是不是
EMPTY_POOL_PLACEHOLDER ,是的话就清空这个自动释放池; - ② 如果不是的话,就通过
pageForPointer (token)拿到token所在的Page(自动释放池的首个Page); - ③ 通过
page->releaseUntil(stop) 将自动释放池中的autorelease 对象全部释放,传参stop即为POOL_BOUNDARY 的地址; - ④ 判断当前Page是否有子Page,有的话就销毁。
3.3.3 autoRelease
可以看到,调用了autorelease 方法的对象,也是通过以上解析的autoreleaseFast ()方法添加进Page中。
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;
}
3.4 总结
push 操作是往自动释放池中添加一个POOL_BOUNDARY ,并返回它存放的内存地址;- 接着每有一个对象调用
autorelease 方法,会将它的内存地址添加进自动释放池中。 pop 操作是传入一个POOL_BOUNDARY 的内存地址,从最后一个入栈的autorelease 对象开始,将自动释放池中的autorelease 对象全部释放(实际上是给它们发送一条release消息),直到遇到这个POOL_BOUNDARY 。
四、RunLoop 与 @autoreleasepool
学习这个知识点之前,需要先搞懂RunLoop的事件循环机制以及它的6种活动状态,参考文章: 《深入浅出 RunLoop(二):数据结构》 《深入浅出 RunLoop(三):事件循环机制》
iOS在主线程的RunLoop中注册了两个Observer:
- 第1个Observer监听了
kCFRunLoopEntry 事件,会调用objc_autoreleasePoolPush (); - 第2个Observer:
① 监听了kCFRunLoopBeforeWaiting 事件,会调用objc_autoreleasePoolPop ()、objc_autoreleasePoolPush (); ② 监听了kCFRunLoopBeforeExit 事件,会调用objc_autoreleasePoolPop ()。
五、Q&A
Q:释放NSAutoreleasePool 对象,使用[pool release] 与[pool drain] 的区别? A: Objective-C 语言本身是支持 GC 机制的,但有平台局限性,仅限于 MacOS 开发中,iOS 开发用的是 RC 机制。在 iOS 的 RC 环境下[pool release]和[pool drain]效果一样,但在 GC 环境下drain会触发 GC 而release不做任何操作。使用[pool drain]更佳,一是它的功能对系统兼容性更强,二是这样可以跟普通对象的release区别开。(注意:苹果在引入ARC时称,已在 OS X Mountain Lion v10.8 中弃用GC机制,而使用ARC替代)
Q: 子线程的autoReleasePool和主线程有什么关系呢? A: 子线程默认不自动创建pool,不过子线程一些临时对象如果显式调用了autorelease方法,内部会创建pool并把对象加入pool的(pool存储在对应线程的私有存储区里),不过pool里的对象释放时机却是等到线程退出才进行清理;所以线程内部建议我们手动@autoreleasepool{}这样释放时机控制在作用域内。
Q: ARC情况下,什么时候需要手动添加autoReleasePool? A: 苹果给出了三种需要手动添加@autoreleasepool的情况:
- 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
- 如果你编写的循环中创建了大量的临时对象;
你可以在循环内使用@autoreleasepool在下一次迭代之前处理这些对象。在循环中使用@autoreleasepool有助于减少应用程序的最大内存占用。 - 如果你创建了辅助线程。
一旦线程开始执行,就必须创建自己的@autoreleasepool;否则,你的应用程序将存在内存泄漏。
|