0%

内存管理-自动释放池

autoreleasepool

新建一个工程,在main.m中就有一个autoreleasepool

1
2
3
4
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
}

通过clang编译一下

1
2
3
$ clang -rewrite-objc main.m -o main.cpp
// 或者
$ xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m -o main-arm64.cpp

我们打开对应的cpp文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct __AtAutoreleasePool {
//构造函数
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
// 析构函数
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
{
// 是一个结构体
__AtAutoreleasePool __autoreleasepool;
}
return 0;
}

也就是说:

1
2
3
@autoreleasepool {}
//等价于
{__AtAutoreleasePool __autoreleasepool; }

__AtAutoreleasePool是一个结构体,有构造函数析构函数,在结构体定义的对象在作用域结束后,会自动调用析构函数。

在源码中有这么一段话:

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.
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.
The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased objects are stored.

通过上述描述,可以大概的知道以下几点:

通过描述,有以下几点说明

  1. 自动释放池 是一个 关于指针的栈结构
  2. 其中的指针是指要释放的对象或者 pool_boundary 哨兵(现在经常被称为 边界)
  3. 自动释放池是一个页的结构(虚拟内存中提及过) ,而且这个页是一个双向链表(表示有父节点 和 子节点,在类中有提及,即类的继承链)
  4. 自动释放池和线程有关系

接下来看一下源码中的实现:

AutoreleasePoolPage

1
2
3
4
5
6
7
8
9
10
11
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

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

都有一个AutoreleasePoolPage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
`class AutoreleasePoolPage : private AutoreleasePoolPageData`

struct AutoreleasePoolPageData
{
//用来校验AutoreleasePoolPage的结构是否完整
magic_t const magic; //16个字节,以结构体中的变量对齐后的值为准,m[4] = 4*4 =16字节
//指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()
__unsafe_unretained id *next;//8字节
//指向当前线程
pthread_t const thread;//8字节
//指向父节点,第一个结点的parent值为nil
AutoreleasePoolPage * const parent;//8字节
//指向子节点,最后一个结点的child值为nil
AutoreleasePoolPage *child;//8字节
//表示深度,从0开始,往后递增1
uint32_t const depth;//4字节
//表示high water mark 最大入栈数量标记
uint32_t hiwat;//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)
{
}
};

接下来我们在回过头看看objc_autoreleasePoolPush

objc_autoreleasePoolPush

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//入栈
static inline void *push()
{
id *dest;
//判断是否有pool
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page
// 自动释放池从新池页面开始
//如果没有,则创建
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
//压栈一个POOL_BOUNDARY,即压栈哨兵
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}

autoreleaseNewPage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//创建新页
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
//获取当前操作页
AutoreleasePoolPage *page = hotPage();
//如果存在,则压栈对象
if (page) return autoreleaseFullPage(obj, page);
//如果不存在,则创建页
else return autoreleaseNoPage(obj);
}

//******** hotPage方法 ********
//获取当前操作页
static inline AutoreleasePoolPage *hotPage()
{
//获取当前页
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
//如果是一个空池,则返回nil,否则,返回当前线程的自动释放池
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}

//******** autoreleaseNoPage方法 ********
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
ASSERT(!hotPage());

bool pushExtraBoundary = false;
//判断是否是空占位符,如果是,则压栈哨兵标识符置为YES
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
//如果对象不是哨兵对象,且没有Pool,则报错
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
objc_thread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
//如果对象是哨兵对象,且没有申请自动释放池内存,则设置一个空占位符存储在tls中,其目的是为了节省内存
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {//如果传入参数为哨兵
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();//设置空的占位符
}

// We are pushing an object or a non-placeholder'd pool.

// Install the first page.
//初始化第一页
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
//设置page为当前聚焦页
setHotPage(page);

// Push a boundary on behalf of the previously-placeholder'd pool.
//压栈哨兵的标识符为YES,则压栈哨兵对象
if (pushExtraBoundary) {
//压栈哨兵
page->add(POOL_BOUNDARY);
}

// Push the requested object or pool.
//压栈对象
return page->add(obj);
}

其中autoreleaseNoPage方法中发现当前线程的自动释放池是通过AutoreleasePoolPage创建的,其定义中有构造方法,而构造方法的实现是通过父类AutoreleasePoolPageData的初始化方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//**********AutoreleasePoolPage构造方法**********
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),//开始存储的位置
objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
newParent,
newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
newParent ? newParent->hiwat : 0)
{
if (parent) {
parent->check();
ASSERT(!parent->child);
parent->unprotect();
//this 表示 新建页面,将当前页面的子节点 赋值为新建页面
parent->child = this;
parent->protect();
}
protect();
}

//**********AutoreleasePoolPageData初始化方法**********
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)
{
}

看一下几个参数:

1
2
3
4
5
AutoreleasePoolPageData(begin(),//开始存储的位置
objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
newParent,
newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
newParent ? newParent->hiwat : 0)
  • begin():

    1
    2
    3
    4
    5
    //页的开始位置
    id * begin() {
    //等于 首地址+56(AutoreleasePoolPage类所占内存大小)
    return (id *) ((uint8_t *)this+sizeof(*this));
    }
  • objc_thread_self(): 表示的是当前线程,而当前线程时通过tls获取

  • newParent: 父节点

  • 后面两个参数是通过父节点的深度、最大入栈个数计算depth以及hiwat

自动释放池内存结构

由于在ARC模式下,是无法手动调用autorelease,所以将Demo切换至MRC模式。
Build Settings -> Objectice-C Automatic Reference Counting设置为NO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 自动释放池打印
extern void _objc_autoreleasePoolPrint(void);

//************运行代码************
int main(int argc, const char * argv[]) {
@autoreleasepool {
//循环创建对象,并加入自动释放池
for (int i = 0; i < 5; i++) {
NSObject *objc = [[NSObject alloc] sutorelease];
}
//调用
_objc_autoreleasePoolPrint();
}
}

查看自动释放池的内存结构,发现page的开始与第一个对象的地址差是0x38,转换成十进制刚好是56,也就是 AutoreleasePoolPage自己本身的内存大小。

但是打印的是5个对象,但是这里有6个。第一个pool是啥?

哨兵对象。只在第一页有,防止释放时越界。

接着我们修改i的最大值为505。发现第二页开始有1个数据了。
接着在继续修改最大值为504+506,发现第三页也开始有1个数据了。

所以第一页有哨兵对象,可以存504个对象。第二页之后可以存505个对象。

在源码中AutoreleasePoolPage->size可以看到一页的大小。

1
2
3
4
5
public:
static size_t const SIZE = PAGE_MIN_SIZE;

#define PAGE_MIN_SHIFT 12
#define PAGE_MIN_SIZE (1 << PAGE_MIN_SHIFT)

size = 1<<12 = 4096。

首地址是从0x38开始的,也就是56个字节,4096-46 = 4040 = 505 * 8
所以一页有506个对象。

autoreleaseFast压栈

push方法的第二步,第一步是创建,第二步是直接存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
}

当前页没有满,则添加add

1
2
3
4
5
6
7
8
9
10
11
12
// 添加释放对象
id *add(id obj)
{
ASSERT(!full());
unprotect();
//传入对象存储的位置
id *ret = next; // faster than `return next-1` because of aliasing
//将obj压栈到next指针位置,然后next进行++,即下一个对象存储的位置
*next++ = obj;
protect();
return ret;
}

如果页满了则处理autoreleaseFullPage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//添加自动释放对象,当页满的时候调用这个方法
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
ASSERT(page == hotPage());
ASSERT(page->full() || DebugPoolAllocation);

//do-while遍历循环查找界面是否满了
do {
//如果子页面存在,则将页面替换为子页面
if (page->child) page = page->child;
//如果子页面不存在,则新建页面
else page = new AutoreleasePoolPage(page);
} while (page->full());

//设置为当前操作页面
setHotPage(page);
//对象压栈
return page->add(obj);
}

压栈流程

  1. 如果页存在,且未满,则通过add方法压栈对象
  2. 如果页存在,且满了,则通过autoreleaseFullPage方法安排新的页面
  3. 如果页不存在,则通过autoreleaseNoPage方法创建新页

objc_autoreleasePoolPop

1
2
3
4
// 析构函数
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}

在析构函数中传了一个参数,就是atautoreleasepoolobj对象,这样就可以和创建的autoreleasepool关联上了。接下来看一下pop的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//出栈
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);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
//如果当前页存在,则将当前页设置为coldPage,token设置为coldPage的开始位置
page = coldPage();
token = page->begin();
} else {
//获取token所在的页
page = pageForPointer(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);
}

看一下popPage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//出栈页面
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
if (allowDebug && PrintPoolHiwat) printHiwat();
// 出栈当前操作页面对象
page->releaseUntil(stop);

// memory: delete empty children 删除空子项
if (allowDebug && DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
//调试期间删除每个特殊情况下的所有池
//获取当前页面的父节点
AutoreleasePoolPage *parent = page->parent;
//将当前页面杀掉
page->kill();
//设置操作页面为父节点页面
setHotPage(parent);
}
else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
//特殊情况:调试丢失的自动释放池时删除pop(top)的所有内容
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full 如果页面已满一半以上,则保留一个空子级
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}

stop就是自动释放池创建的位置。拿到stop的位置后,释放到stop位置之后的所有对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//释放到stop位置之前的所有对象
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// 不是递归的:我们不想破坏堆栈
// if a thread accumulates a stupendous amount of garbage
//判断下一个对象是否等于stop,如果不等于,则进入while循环
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects 每次从hotPage()重新启动,以防-release自动释放更多对象
//获取当前操作页面,即hot页面
AutoreleasePoolPage *page = hotPage();

// fixme I think this `while` can be `if`, but I can't prove it
//如果当前页是空的
while (page->empty()) {
//将page赋值为父节点页
page = page->parent;
//并设置当前页为父节点页
setHotPage(page);
}

page->unprotect();
//next进行--操作,即出栈
id obj = *--page->next;
//将页索引位置置为SCRIBBLE,表示已经被释放
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();

if (obj != POOL_BOUNDARY) {
//释放
objc_release(obj);
}
}
//设置当前页
setHotPage(this);

#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
ASSERT(page->empty());
}
#endif
}

再看一下kill

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//销毁
void kill()
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
AutoreleasePoolPage *page = this;
//获取最后一个页
while (page->child) page = page->child;

AutoreleasePoolPage *deathptr;
do {
deathptr = page;
//子节点 变成 父节点
page = page->parent;
if (page) {
page->unprotect();
//子节点为nil
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}

autorelease pop流程

  1. 是否是空页,做容错处理
  2. releaseUntil(stop),按页倒序进行,循环释放所有对象,直到位置执行stop。先从child开始
  3. 页中的对象释放之后,page执行kill,循环删除child节点

autorelease底层

看一下mrc下autorelease的源码:

1
2
3
4
5
6
7
8
9
id
objc_autorelease(id obj)
{
//如果不是对象,则直接返回
if (!obj) return obj;
//如果是小对象,也直接返回
if (obj->isTaggedPointer()) return obj;
return obj->autorelease();
}
  • 如果是小对象,则直接return,不处理。
  • 如果是对象,执行obj->autorelease()
1
2
3
4
5
6
7
8
9
10
11
inline id 
objc_object::autorelease()
{
ASSERT(!isTaggedPointer());
// 自定义对象
if (fastpath(!ISA()->hasCustomRR())) {
return rootAutorelease();
}
// 系统
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(autorelease));
}

自定义对象,执行rootAutorelease

1
2
3
4
5
6
7
8
9
inline id 
objc_object::rootAutorelease()
{
// 是否为小对象
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

return rootAutorelease2();
}

对象执行rootAutorelease2

1
2
3
4
5
6
7
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
ASSERT(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}

最后执行的还是AutoreleasePoolPage这个对象,调用autorelease

1
2
3
4
5
6
7
8
9
public:
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;
}

这里调用的就是autoreleaseFast。就又回到了上面的压栈流程。

总结

  • @autoreleasepool {} 等价于 {__AtAutoreleasePool __autoreleasepool; },这是构造和析构函数
  • autorelease push压栈流程
    1. 如果页存在,且未满,则通过add方法压栈对象
    2. 如果页存在,且满了,则通过autoreleaseFullPage方法安排新的页面
    3. 如果页不存在,则通过autoreleaseNoPage方法创建新页
  • autorelease pop流程
    1. 是否是空页,做容错处理
    2. releaseUntil(stop),按页倒序进行,循环释放所有对象,直到位置执行stop。先从child开始
    3. 页中的对象释放之后,page执行kill,循环删除child节点
  • mrc下 autorelease原理
    1. 判断是否为小对象,是直接return
    2. 执行autoreleaseFast,执行autorelease push压栈流程