准备
一、Autorelease简介
AppKit
和UIKit
框架在事件循环(RunLoop
)的每次循环开始时,在主线程创建一个自动释放池,并在每次循环结束时销毁它,在销毁时释放自动释放池中的所有autorelease
对象。
通常情况下我们不需要手动创建自动释放池,但是如果我们在循环中创建了很多临时的autorelease
对象,则手动创建自动释放池来管理这些对象可以很大程度地减少内存峰值。
二、自动释放池 原理分析
2.1 @autoreleasepool{} 分析
先创建一个mac
工程,main.m
中代码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
执行clang -rewrite-objc main.m -o main.cpp
命令得到main.cpp
文件:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
}
return 0;
}
- 代码被转化为
C++
源码以后,@autoreleasePool
块其实是__AtAutoreleasePool
结构体的创建,创建时会调用构造函数
,作用域结束时会调用析构函数
。
再看下__AtAutoreleasePool
结构体:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
- 构造函数会调用
objc_autoreleasePoolPush
函数,并返回边界对象atautoreleasepoolobj
。 - 析构函数会调用
objc_autoreleasePoolPop
函数,并传入边界对象atautoreleasepoolobj
。
所以,我们可以将main
函数的代码简化如下:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ {
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
2.2 objc_autoreleasePoolPush与objc_autoreleasePoolPop
打开objc4-818.2 源码 ,查看objc_autoreleasePoolPush
和objc_autoreleasePoolPop
函数:
void * objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
2.3 AutoreleasePoolPage 结构分析
AutoreleasePoolPage
是一个C++
类,打开objc
源码,可以看到它继承自AutoreleasePoolPageData
,结构如下:
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
public:
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MIN_SIZE; // 4096 size and alignment, power of 2
#endif
private:
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const COUNT = SIZE / sizeof(id);
static size_t const MAX_FAULTS = 2;
# define EMPTY_POOL_PLACEHOLDER ((id*)1) //空池占位
# define POOL_BOUNDARY nil //边界对象(即哨兵对象)
...
}
struct AutoreleasePoolPageData
{
magic_t const magic; // 用来校验 AutoreleasePoolPage 的结构是否完整 16字节
__unsafe_unretained id *next; // 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向begin() 8字节
pthread_t const thread; // 指向当前线程 8字节
AutoreleasePoolPage * const parent; // 指向父结点,第一个结点的parent值为nil 8字节
AutoreleasePoolPage *child; // 指向子结点,最后一个结点的child值为nil 8字节
uint32_t const depth; // 代表深度,从0开始,往后递增1 4字节
uint32_t hiwat; // 代表high water mark 最大入栈数量标记 4字节
// 构造函数
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
: magic(), next(_next), thread(_thread),
parent(_parent), child(nil),
depth(_depth), hiwat(_hiwat)
{
}
};
- 自动释放池是由若干个
AutoreleasePoolPage
组成的双向链表结构,AutoreleasePoolPage
中拥有parent
和child
指针,分别指向上一个和下一个page
。 - 当前一个
page
的空间被占满(每个AutorelePoolPage
的大小为4096字节)时,就会新建一个AutorelePoolPage
对象并连接到链表中,后来的 Autorelease对象也会添加到新的page
中。 - 另外,当
next== begin()
时,表示AutoreleasePoolPage
为空;当next == end()
,表示AutoreleasePoolPage
已满。
为什么多页?
- 操作过程需要加锁解锁,如果所有页面都在一页,操作非常复杂,一个对象进行操作其他的都得等待。
- 已满的页面都不进行操作了,只对没满的那个进行操作,效率比较高。
- 不用非得是一片连续的内存。
2.4 AutoreleasePoolPage 空间大小
上面的AutoreleasePoolPage
结构中,我们看到结构体空间大小为PAGE_MIN_SIZE
:
#define PAGE_MIN_SHIFT 12
#define PAGE_MIN_SIZE (1 << PAGE_MIN_SHIFT)
1
<<12
等于4096
,也就是4
k。
下面进行验证:
- 示例中一共有
505
个对象,添加了504
个以后又重新创建了一个hot page
,说明一个page
最多只能添加504
个对象。 504*8
+56
(成员变量大小) +8
(边界对象) =4096
,和上面说的大小一致。
2.5 哨兵对象/边界对象(POOL_BOUNDARY)
上面的AutoreleasePoolPage
结构中,我们看到了有一个边界对象(哨兵对象):POOL_BOUNDARY
- 边界对象其实就是
nil
的别名,而它的作用事实上也就是为了起到一个标识的作用。 - 每当调用
objc_autoreleasePoolPush
方法时,会将POOL_BOUNDARY
放到当前page
的栈顶,并且返回这个边界对象。 - 而在调用
objc_autoreleasePoolPop
方法时,又会将边界对象以参数传入,这样自动释放池就会向释放池中对象发送release
消息,直至找到第一个边界对象为止。
验证
苹果提供了一个调试函数:_objc_autoreleasePoolPrint
,可以用来打印自动释放池的创建信息,看下面的测试代码:
extern void _objc_autoreleasePoolPrint(void); // 需要用extern修饰才能在外部访问
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[[NSObject alloc] init] autorelease];
_objc_autoreleasePoolPrint();
}
return 0;
}
查看打印结果:
2021-09-20 18:19:42.833783+0800 KCObjcBuild[10265:184146] objc:<NSObject: 0x10127ea90>
objc[10265]: ##############
objc[10265]: AUTORELEASE POOLS for thread 0x1000ebe00
objc[10265]: 2 releases pending.
objc[10265]: [0x10200b000] ................ PAGE (hot) (cold)
objc[10265]: [0x10200b038] ################ POOL 0x10200b038
objc[10265]: [0x10200b040] 0x10127ea90 NSObject
objc[10265]: ##############
- 可以看到自动释放池添加的对象被打印了出来,其中
x10127ea90 NSObject
是我们创建的objc
对象,还有一个POOL 0x10200b038
地址就是哨兵对象。
2.6 objc_autoreleasePoolPush
经过前面的分析,objc_autoreleasePoolPush
最终调用的是 AutoreleasePoolPage
的push
方法,该方法的具体实现如下:
static inline void *push()
{
return autoreleaseFast(POOL_BOUNDARY);
}
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);// 没有page,走这个流程
}
}
id *add(id obj)
{
id *ret;
ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
}
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
do {
if (page->child) page = page->child; // 无限递归找到最后一个
else page = new AutoreleasePoolPage(page); // 新建page
} while (page->full());
setHotPage(page); // 设置为hotPage
return page->add(obj);
}
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// 通过构造函数新建page
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page); // 设置为hotPage
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY); // 先添加哨兵
}
// 再添加obj
return page->add(obj);
}
看上面代码,push
函数中调用了autoreleaseFast
函数,函数中又分三种情况:
- 当前
page
存在且不满,调用page->add(obj)
方法将对象添加至page
的栈中,即next
指向的位置。 - 当前
page
存在但是已满,调用autoreleaseFullPage
初始化一个新的page
,调用page->add(obj)
方法将对象添加至page
的栈中。 - 当前
page
不存在时,调用autoreleaseNoPage
创建一个hotPage
,先将边界对象(POOL_BOUNDARY
)添加至page
的栈中,再调用page->add(obj)
方法将对象添加至page
的栈中。
再看下AutoreleasePoolPage
的构造函数:
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),
objc_thread_self(), // 当前线程
newParent,
newParent ? 1+newParent->depth : 0, // +1操作
newParent ? newParent->hiwat : 0)
{
if (objc::PageCountWarning != -1) {
checkTooMuchAutorelease();
}
if (parent) {
parent->check();
ASSERT(!parent->child);
parent->unprotect();
parent->child = this; // 给父类的子类赋值本身
parent->protect();
}
protect();
}
id * begin() {
// 从成员变量下面开始添加
return (id *) ((uint8_t *)this+sizeof(*this));
}
2.7 objc_autoreleasePoolPop
objc_autoreleasePoolPop
最终调用的是 AutoreleasePoolPage
的pop
方法,并传入边界对象(POOL_BOUNDARY
),该方法的具体实现如下:
static inline void
pop(void *token) // POOL_BOUNDARY的地址
{
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);// 通过POOL_BOUNDARY找到对应的page
stop = (id *)token;
return popPage<false>(token, page, stop);
}
template<bool allowDebug>
static void popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
page->releaseUntil(stop); // 向栈中的对象发送release消息,直到遇到第一个哨兵对象
// memory: delete empty children
// 删除空掉的节点
if (allowDebug && DebugPoolAllocation && page->empty()) {
AutoreleasePoolPage *parent = page->parent; // 拿到parent
page->kill(); // 本身杀掉
setHotPage(parent); // 把parent设置为hotPage
} else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) {
page->kill();
setHotPage(nil);
} else if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
void releaseUntil(id *stop)
{
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
if (obj != POOL_BOUNDARY) {
for (int i = 0; i < count + 1; i++) {
objc_release(obj);
}
}
}
setHotPage(this);
}
void kill()
{
AutoreleasePoolPage *page = this;
while (page->child) page = page->child;
AutoreleasePoolPage *deathptr;
// 循环置空操作
do {
deathptr = page;
page = page->parent;
if (page) {
page->unprotect();
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
上述代码中,先根据传入的边界对象地址找到边界对象所处的page
,然后选择当前page
中最新加入的对象一直向前清理,直到边界所在的位置;清理的方式是向这些对象发送一次release
消息,使其引用计数减一;
另外,清空page
对象还会遵循一些原则:
- 如果当前的
page
中存放的对象少于一半,则子page
全部删除; - 如果当前的
page
存放的多余一半(意味着马上将要满),则保留一个子page
,节省创建新page
的开销;
2.8 自动释放池可嵌套 验证
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[[NSObject alloc] init] autorelease];
NSLog(@"objc:%@",objc);
@autoreleasepool {
NSObject *objc2 = [[[NSObject alloc] init] autorelease];
NSLog(@"objc2:%@",objc2);
_objc_autoreleasePoolPrint();
}
}
return 0;
}
2021-09-20 21:54:23.661658+0800 KCObjcBuild[16800:319949] objc:<NSObject: 0x101a58ab0>
2021-09-20 21:54:23.663090+0800 KCObjcBuild[16800:319949] objc2:<NSObject: 0x1006282b0>
objc[16800]: ##############
objc[16800]: AUTORELEASE POOLS for thread 0x1000ebe00
objc[16800]: 4 releases pending.
objc[16800]: [0x102016000] ................ PAGE (hot) (cold)
objc[16800]: [0x102016038] ################ POOL 0x102016038
objc[16800]: [0x102016040] 0x101a58ab0 NSObject
objc[16800]: [0x102016048] ################ POOL 0x102016048
objc[16800]: [0x102016050] 0x1006282b0 NSObject
objc[16800]: ##############
- 根据打印结果可以看到,自动释放池依次添加了
0x102016038
边界对象 ->objc
对象 ->0x102016048
边界对象 ->objc2
对象。
三、autorelease 对象在什么时候释放?
autorelease
对象的释放有系统干预释放
和手动干预释放
两种情况。
- 系统干预释放是不指定
@autoreleasepool
,所有autorelease
对象都由主线程的RunLoop
创建的@autoreleasepool
来管理。 - 手动干预释放就是将
autorelease
对象添加进我们手动创建的@autoreleasepool
中。
3.1 系统干预释放
我们先创建一个iOS
工程,示例代码如下,并查看运行结果:
@implementation SSLPerson
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
SSLPerson *person = [[[SSLPerson alloc] init] autorelease];
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
@end
运行结果:
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[SSLPerson dealloc]
-[ViewController viewDidAppear:]
- 可以看到,调用了
autorelease
方法的person
对象并没有在viewDidLoad
方法结束后释放,而是在viewWillAppear
方法结束后才释放,说明在viewWillAppear
方法结束的时候,调用了pop()
方法释放了person
对象。其实这是由RunLoop
控制的,下面来讲解一下RunLoop
和@autoreleasepool
的关系。
3.2 RunLoop 与 @autoreleasepool
RunLoop
中对自动释放池
的操作可以用下图来表示:
kCFRunLoopEntry
:在即将进入RunLoop
时,会自动创建一个__AtAutoreleasePool
结构体对象,并调用objc_autoreleasePoolPush()
函数。kCFRunLoopBeforeWaiting
:在RunLoop
即将休眠时,会自动销毁一个__AtAutoreleasePool
对象,调用objc_autoreleasePoolPop()
。然后创建一个新的__AtAutoreleasePool
对象,并调用objc_autoreleasePoolPush()
。kCFRunLoopBeforeExit
,在即将退出RunLoop
时,会自动销毁最后一个创建的__AtAutoreleasePool
对象,并调用objc_autoreleasePoolPop()
。
所以,在iOS
工程中系统干预释放的autorelease
对象的释放时机是由RunLoop
控制的,会在当前RunLoop
每次循环结束时释放。以上person
对象在viewWillAppear
方法结束后释放,说明viewDidLoad
和viewWillAppear
方法在同一次循环里。
3.3 手动干预释放
再来看一下手动干预释放的情况:
@implementation SSLPerson
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
SSLPerson *person = [[[SSLPerson alloc] init] autorelease];
}
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
@end
运行结果:
-[SSLPerson dealloc]
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[ViewController viewDidAppear:]
可以看到,手动添加到指定的@autoreleasepool
中的autorelease
对象,在@autoreleasepool
大括号结束时就会释放了,不受RunLoop
控制。