0%

OC内存管理之自动释放池实现原理

整了8个小时,基本上算整明白了。

环境:

NSObject.mm源码 版本为objc4-781。

对代码

1
2
3
4
5
6
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}

进行

1
clang -rewrite-objc main.m

得到

1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{
__AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_46_lys08y0137d41lbysrxxd0h80000gn_T_main_8ccfab_mi_0);
}
return 0;
}

看一下:

1
2
3
4
5
6
7
8
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};

里面有一个构造函数__AtAutoreleasePool()和一个析构函数~__AtAutoreleasePool()

上述代码等价于:

1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();

// do whatever you want

objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}

1.获取当前自动释放池的哨兵对象的指针 2.把当前自动释放池里的自动释放对象清空直到哨兵对象为止。

即你每次写@autoreleasepool

1
2
3
@autoreleasepool {
NSLog(@"Hello, World!");
}

就等价于:一对花括号,上花括号紧接着的是objc_autoreleasePoolPush操作,下括号上面紧接着的是objc_autoreleasePoolPop操作。

1
2
3
4
5
6
7
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();

// do whatever you want

objc_autoreleasePoolPop(atautoreleasepoolobj);
}

记住这一点很重要。

上述两个函数的实现:

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

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

AutoreleasePoolPage结构

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
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
82
83
84
85
86
87
88
89
90
/***********************************************************************
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.
**********************************************************************/

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 //4KB
#endif

private:
static pthread_key_t const key = AUTORELEASE_POOL_KEY; //AUTORELEASE_POOL_KEY:43
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const COUNT = SIZE / sizeof(id);

// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
# define EMPTY_POOL_PLACEHOLDER ((id*)1) //占位池,起到优化作用。因为很可能创建了自动释放池但实际没有使用到自动释放对象,使用占位池就可以避免这种损耗。

# define POOL_BOUNDARY nil //哨兵对象,清空本次自动释放池里的对象的截止位置。
...
...
...
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}

id * end() {
return (id *) ((uint8_t *)this+SIZE);
}

bool empty() {
return next == begin();
}

bool full() {
return next == end();
}

bool lessThanHalfFull() {
return (next - begin() < (end() - begin()) / 2);
}

id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret; //返回一个地址
}
}

class AutoreleasePoolPage;
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;

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)
{
}
};

一个线程的自动释放池登记了一堆指针,每个指针指向的要么是一个待释放的对象,要么是一个POOL_BOUNDARY(也称为哨兵对象),POOL_BOUNDARY是一个自动释放池的界限,这表明自动释放池可以嵌套。每个自动释放池都有一个token指针指向POOL_BOUNDARY。当池子被pop时,比哨兵对象后登记的对象都将被释放。自动释放池的底层由一个双链表 AutoreleasePoolPage 实现。每一个AutoreleasePoolPage对象占用4KB(也就是每一页最多注册128*4=512个对象)。page按需添加或删除。Thread-local storage(TLS,即线程局部存储)指向 hot page,hot page 是指最新添加的 autorelease 对象所在的那个 page。

magic:用于对当前 AutoreleasePoolPage 完整性的校验

next:指针变量,可以指向任意类型,指针变量自然它的值就是地址。它指向的是下一次添加对象时的存储位置。

thread:当前自动释放池所在的线程

parent:父AutoreleasePoolPage,第一个结点的 parent 值为 nil;

child:子AutoreleasePoolPage,最后一个结点的 child 值为 nil;

depth:双链表的深度

push

实现:

1
2
3
4
5
6
7
8
9
10
11
12
static inline void *push() 
{
id *dest;
if (slowpath(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; //返回的是page上的一个地址。
}

push操作就是调用autoreleaseFast添加一个POOL_BOUNDARY哨兵对象并返回哨兵对象所在的内存地址或者一个EMPTY_POOL_PLACEHOLDER占位地址。所谓的哨兵对象就是一个nil对象,因此page的这一栏的内容其实是空的。

第一次执行push的时候返回的其实是一个占位地址EMPTY_POOL_PLACEHOLDER,其他时候才是page上某一栏的内存地址了。这里的EMPTY_POOL_PLACEHOLDER是一个性能优化,因为有可能我们虽然写了 @autoreleasepool{} 但里面不一定会使用到自动释放对象,这个时候就没必要创建一个page,即page是懒加载的,真正往里面注册时才会创建page。懒加载后,系统也是先添加一个哨兵对象再添加传入的对象。

与push操作相对的是pop操作,pop操作会一直release对象直到POOL_BOUNDARY哨兵对象(该对象就是push操作时返回的)。

完成添加操作的函数autoreleaseFast:

autoreleaseFast

实现:

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

autoreleaseFast函数返回的就是对象被添加到page上时的内存地址。

整个逻辑分为三个部分:

  1. 首先查找是否存在hotPage
  2. hotPage存在且没有满,则直接添加到page上
  3. hotPage存在但已经满了,则调用autoreleaseFullPage,创建新的一页page,然后将obj添加到新的page上
  4. hotPage不存在,表明当前还没有page,则调用autoreleaseNoPage,创建第一个page,将obj添加到page上

接下来具体看一下各子函数:

hotPage

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static inline AutoreleasePoolPage *hotPage() 
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}

static pthread_key_t const key = AUTORELEASE_POOL_KEY;
define AUTORELEASE_POOL_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)
#define __PTK_FRAMEWORK_OBJC_KEY3 43

// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
# define EMPTY_POOL_PLACEHOLDER ((id*)1)

大致流程:

  1. 从线程的TLS读取AUTORELEASE_POOL_KEY的值,如果等于EMPTY_POOL_PLACEHOLDER,表明当前没有page,因此返回nil。
  2. 如果不是EMPTY_POOL_PLACEHOLDER,则表明当前线程有正在使用的未满的page,返回它。正在使用的未满的page也叫做hotPage。hotPage不一定是双链表的最后一页,因为pop的时候可能会保留一页空的page。只能说hotPage跟双链表的最后一页很近。

这里是get hotPage,我们可以看下是如何set hotPage的:

setHotPage

实现:

1
2
3
4
5
static inline void setHotPage(AutoreleasePoolPage *page) 
{
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}

逻辑很简单就是把page的地址存储到线程的TLS。

与hotPage对应的是codePage

codePage

实现:

1
2
3
4
5
6
7
8
9
10
11
static inline AutoreleasePoolPage *coldPage() 
{
AutoreleasePoolPage *result = hotPage();
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}

其实就是找到双链表的第一个page。即双链表的第一个page也叫coldPage。

接着未完成的分析,autoreleaseFullPage

autoreleaseFullPage

自动释放池页满了的处理。

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());

setHotPage(page);
return page->add(obj);
}

大致逻辑:

  1. 从当前page开始沿着双链表往后查找,直到查找到一个未满的page。如果查找到末尾也没有则新创建一个page。

  2. 设置page为hotPage。

  3. 将obj添加到page上。

由此可以看到如果注册的对象特别多,那么一个自动释放池可以横跨很多个page。

接着分析autoreleaseNoPage:

autoreleaseNoPage

没有自动释放池页的处理,需要加载一个。

实现:

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
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;
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;
}
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;
}
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);
setHotPage(page);

// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}

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

static inline bool haveEmptyPoolPlaceholder()
{
id *tls = (id *)tls_get_direct(key);
return (tls == EMPTY_POOL_PLACEHOLDER);
}

static inline id* setEmptyPoolPlaceholder()
{
ASSERT(tls_get_direct(key) == nil);
tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
return EMPTY_POOL_PLACEHOLDER;
}

逻辑也很简单:

  1. 调用haveEmptyPoolPlaceholder,先判断TLS中存储的是否等于EMPTY_POOL_PLACEHOLDER。
  2. 如果等于EMPTY_POOL_PLACEHOLDER,说明可以创建第一个page了。
  3. 创建一个page,并设置为hotPage。
  4. 如果是第一个page则还需要添加POOL_BOUNDARY用于pop的停止标志。
  5. 添加obj到page上
  6. 接第一步如果不等于EMPTY_POOL_PLACEHOLDER,说明当前双链表为空没有page。如果obj等于POOL_BOUNDARY,则走 else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) 分支,调用setEmptyPoolPlaceholder往TLS上写入占位符EMPTY_POOL_PLACEHOLDER并返回EMPTY_POOL_PLACEHOLDER,第一次push的时候走的就是这里。接下来是比较特殊的一种情况,如果obj不等于POOL_BOUNDARY 生产环境下代码并不会走 else if (obj != POOL_BOUNDARY && DebugMissingPools) 分支,因此会创建一个page,并将对象添加到page上,只不过这种情况下不会有哨兵对象。没有哨兵对象会不会出问题呢?从后续的pop方法可以看到不会有问题。

setEmptyPoolPlaceholder

实现:

1
2
3
4
5
6
7
8
9
10
11
12
static inline bool haveEmptyPoolPlaceholder()
{
id *tls = (id *)tls_get_direct(key);
return (tls == EMPTY_POOL_PLACEHOLDER);
}

static inline id* setEmptyPoolPlaceholder()
{
ASSERT(tls_get_direct(key) == nil);
tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
return EMPTY_POOL_PLACEHOLDER;
}

autorelease—给对象发送一条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;
}

我们给对象发送autorelease消息时,就是调用autoreleaseFast(obj)将对象添加到双链表上。注意没有retain操作。

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
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.
page = coldPage();
token = page->begin(); //coldPage是双链表的第一页,这里将token设置为begin位置。
} else {
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);
}

可以看到整个过程如下:

  1. 判断传入的token是否等于EMPTY_POOL_PLACEHOLDER,第一次push的时候就是EMPTY_POOL_PLACEHOLDER。
  2. 如果等于EMPTY_POOL_PLACEHOLDER,则获取hotPage,如果hotPag等于nil,则表明 @autoreleasepool{} 内没有使用到自动释放对象,优化成功。将TLS对应key置为nil退出。如果hotPag不等于nil,说明有使用到自动释放对象。则获取coldPage,及token。
  3. 接第一步如果传入的token不等于EMPTY_POOL_PLACEHOLDER(这种情况一般是嵌套了自动释放池,上面已经提及只有第一次push的时候才会返回EMPTY_POOL_PLACEHOLDER,后面再push的时候返回的将是page上的一个地址),则根据token找到它的那一个page。
  4. 判断token是否指向哨兵对象,一般情况下都会指向哨兵对象,但是也有一些特殊情况下不会指向哨兵对象。其中 if (stop == page->begin() && !page->parent) 即token指向第一个对象并且page是第一页则可以正常popPage(比如没有先push操作就开始注册对象),其他情况就是badPop。
  5. 调用popPage开始释放。

pop的主要逻辑其实是根据传入的token,找到token所在的那一个page。这个page+token就是本次释放的结束地点。

Q:如何根据传入的token,找到token所在的那一个page?

这里稍微看一下如何根据token查找出token所在的page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//根据token查找出token所在的page
static AutoreleasePoolPage *pageForPointer(const void *p)
{
return pageForPointer((uintptr_t)p);
}

static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE;

ASSERT(offset >= sizeof(AutoreleasePoolPage));

result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();

return result;
}

result = p - (p % SIZE);这就是token所在的page的地址。为什么可以这么计算?主要还是得益于内存对齐,page的起始地址肯定是SIZE的整数倍。这里不可谓不妙。

举个简单的例子验证一下:

假设第一页的地址为100,一页可以登记10个对象,问token为132所在的page的地址是多少?

因为第一页是100,那么第二页就是110,第三页就是120,第四页就是130,于是第四页的第二个位置就是132。因此132所在的page的地址就是130。

根据上面的算法 132 % 10 = 2,132-2 = 130。也可以得到130。

Q:为啥要去查找token所在的page?

这里不是很理解,我获取到hotPage后就开始清除,然后根据双链表一直往上清除,最终肯定会到达token所在的page的。感觉根本不需要先去查找token所在的page啊?

因为后续调用的releaseUntil是定义在AutoreleasePoolPage里的方法,而releaseUntil里的while条件是this->next != stop.

1
2
3
while (this->next != stop) {

}

this指代的是当前page,所以方法的对象只能是token所在的page,如果是其他的page那么停止的位置就不对了。

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
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
page->kill();
setHotPage(nil);
} else if (page->child) { //前面的分支是debug用的,所以主要看这里
// 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();
}
}
}

这个函数主要是调用releaseUntil进行释放,释放之后当前page后面的page肯定是空的了,因此需要把后面的空page删除,直至剩一个空的page。

releaseUntil

实现:

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
void releaseUntil(id *stop) 
{
// 注释很清楚:不使用递归是怕把栈给搞爆了,所以使用了循环。
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage

while (this->next != stop) { //一直释放到stop的位置。
// Restart from hotPage() every time, in case -release
// autoreleased more objects
AutoreleasePoolPage *page = hotPage(); //先获取hotPage.

// fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) { //如果是空的,说明这一页已经清空,则继续往上找,最终page会等于this。随着释放this->next最终会等于stop.
page = page->parent;
setHotPage(page);
}

page->unprotect();
id obj = *--page->next; //因为next指向的是下一个空的位置,所以这里要先--,移动到有注册对象的位置。
memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); //将这一栏填充SCRIBBLE==0xA3
page->protect();

if (obj != POOL_BOUNDARY) { //这里很妙,如果obj不是nil也会释放掉。因为有可能page里就没有POOL_BOUNDARY对象。不过这种情况一般不会出现,因为系统在新开线程的时候会帮你执行push操作所以基本上都会存在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
}

大致逻辑:

  1. 获取hotPage
  2. 然后从hotPage处开始一直释放注册到page上的对象,直到哨兵对象处结束。本次自动释放池的作用域结束。
  3. 设置当前page为hotPage。

autoreleasepool嵌套情况

1
2
3
4
5
6
7
8
9
10
11
@autoreleasepool {
NSLog(@"Hello, World!");

@autoreleasepool {
NSLog(@"Hello, World!");

@autoreleasepool {
NSLog(@"Hello, World!");
}
}
}

等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void * atautoreleasepoolobj1 = objc_autoreleasePoolPush();

...
void * atautoreleasepoolobj2 = objc_autoreleasePoolPush();

...
void * atautoreleasepoolobj3 = objc_autoreleasePoolPush();

...

objc_autoreleasePoolPop(atautoreleasepoolobj3);

objc_autoreleasePoolPop(atautoreleasepoolobj2);

objc_autoreleasePoolPop(atautoreleasepoolobj1);

pop atautoreleasepoolobj3时,就是从hotPage到POOL_BOUNDARY3截止,

pop atautoreleasepoolobj2时,就是从hotPage到POOL_BOUNDARY2截止,

pop atautoreleasepoolobj1时,就是从hotPage到POOL_BOUNDARY1截止。

应用程序启动时,主线程在进入runloop前会执行push操作创建好自动释放池,当退出runloop时就会执行pop操作销毁自动释放池。这里可以参考runloop原理。

总结

稍微总结一下:

自动释放池底层是由一个双链表数据结构实现的,双链表的每一个元素被称为AutoreleasePoolPage,每个page可以注册一定数量的对象。在MRC时代我们可以给对象发送一条autorelease消息将其注册到自动释放池里,而在ARC时代我们可以将一个对象赋值给一个 __autoreleasing 修饰符的指针变量: id __autoreleasing obj1 = obj; 将其注册到自动释放池里。

@autoreleasepool{} 其实是push-pop对的语法糖:

1
2
3
4
5
6
7
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();

// do whatever you want

objc_autoreleasePoolPop(atautoreleasepoolobj);
}

push函数除第一次调用返回的是占位符外,后续调用返回的是page上的一个位置地址。第一次push时,系统并不会马上创建自动释放池,而是等到真正注册自动释放的对象时才创建。

pop函数就是将hotpage到哨兵对象之间的所有注册的对象清除并发送release消息释放对象。

在子线程中,系统会在创建线程时调用push,在线程销毁时调用pop,因此如果你没有显示创建自动释放池,那么在这期间产生的自动释放对象需要等到线程销毁时才会被释放,而这可能会造成内存峰值压力。

问题

Q0:MRC下给对象发送-autorelease消息会发生什么?

调用栈:最终就是被注册到自动释放池里。注意这里并没有retain操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (id)autorelease {
return _objc_rootAutorelease(self);
}

id
objc_object::rootAutorelease2()
{
ASSERT(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}


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);
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)test_mrc_autorelease {
XQUser *user = [[XQUser alloc] initWithName:@"李白"];
NSLog(@"%ld", (long)CFGetRetainCount(user)); //1

[user autorelease];
NSLog(@"%ld", (long)CFGetRetainCount(user)); //1

[user autorelease];
NSLog(@"%ld", (long)CFGetRetainCount(user)); //1

//最终崩溃,因为过度释放。
}

而在ARC中__autoreleasing修饰符,如果是创建一个对象并赋值给__autoreleasing 的指针变量,则仅仅是将对象注册到自动释放池。其他情况则会retain对象再注册到自动释放池中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@autoreleasepool {
id obj = [NSObject new];
{
id __autoreleasing obj1 = obj; //2
NSLog(@"obj1:%@", obj1);
}
}

id obj = msg_Send(NSObject, "new");
id obj1 = objc_retainAutorelease(obj); //retain对象+注册对象到自动释放池
NSLog(@"obj1:%@", obj1);
objc_storeStrong(&obj, nil);

id
objc_retainAutorelease(id obj)
{
return objc_autorelease(objc_retain(obj));
}

Q1:如何获取创建的自动释放池?

自动释放池都是new出来的,获取的时候怎么找到这个自动释放池呢?从上面的代码可以看到new了一个自动释放池页后,会调用setHotPage(page);这个函数就是往线程的TLS上写入key(key=43)—page地址。这样下次我们只需要读取TLS上的key就可以得到当前正在使用的自动释放池页。系统内部会一直更新key的值,确保key始终指向当前正在使用的自动释放池页。有了hotPage的地址,再加上双链表就可以找到第一页coldPage的地址。

1
2
3
4
5
static inline void setHotPage(AutoreleasePoolPage *page) 
{
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}

注意一个线程只会有一个双链表自动释放池。push的作用:

1.懒加载page,如果此时还没有page或者page满了则会创建一个新的page链接在之前的双链表上,而不是创建另一个双链表。

2.返回一个token地址。

@autoreleasepool嵌套,只是返回不同的token地址。没有创建很多个自动释放池。

Q2:在子线程中没有显示写 @autoreleasepool{} 然后创建了自动释放对象,会造成内存泄漏吗?

1
2
3
4
5
6
7
8
- (void)doSome {
[NSThread detachNewThreadSelector:@selector(test_child_thread_autorelease_obj_dealloc) toTarget:self withObject:nil];
}

- (void)test_child_thread_autorelease_obj_dealloc {
__autoreleasing Animal *ani = [[Animal alloc] init];
[ani eat];
}

使用NSThread创建的线程或者使用gcd,使用pthread等里都不会。基本上使用苹果的框架都不会。

NSAutoreleasePool的说明:

1
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.

在runtime加载时会调用AutoreleasePoolPage的静态方法init进行初始化:

1
2
3
4
5
6
void arr_init(void) 
{
AutoreleasePoolPage::init();
SideTablesMap.init();
_objc_associations_init();
}

自动释放池的init:

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
static void init() //静态方法
{
int r __unused = pthread_key_init_np(AutoreleasePoolPage::key,
AutoreleasePoolPage::tls_dealloc);
ASSERT(r == 0);
}

...
#if !VARIANT_DYLD
// XXX: key should be pthread_key_t
int
pthread_key_init_np(int key, void (*destructor)(void *))
{
int res = EINVAL; // Returns EINVAL if key is out of range.
if (key >= __pthread_tsd_first && key < __pthread_tsd_start) {
_PTHREAD_LOCK(__pthread_tsd_lock);
_pthread_key_set_destructor(key, destructor);
if (key > __pthread_tsd_max) {
__pthread_tsd_max = key;
}
_PTHREAD_UNLOCK(__pthread_tsd_lock);
res = 0;
}
return res;
}
#endif // !VARIANT_DYLD

绑定了一个key—tls_dealloc,线程退出时会执行tls_dealloc。

线程退出时会进行一些清理工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PTHREAD_NORETURN
static void
_pthread_exit(pthread_t self, void *exit_value)
{
struct __darwin_pthread_handler_rec *handler;

// Disable signal delivery while we clean up
__disable_threadsignal(1);

// Set cancel state to disable and type to deferred
_pthread_setcancelstate_exit(self, exit_value);

while ((handler = self->__cleanup_stack) != 0) {
(handler->__routine)(handler->__arg);
self->__cleanup_stack = handler->__next;
}
_pthread_tsd_cleanup(self); //清除tsd

// Clear per-thread semaphore cache
os_put_cached_semaphore(SEMAPHORE_NULL);

_pthread_terminate_invoke(self, exit_value);
}

其中一个就是_pthread_tsd_cleanup:

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
void
_pthread_tsd_cleanup(pthread_t self)
{
#if !VARIANT_DYLD

// unless __pthread_key_legacy_behaviour == 1, use the new pthread key
// destructor order: (dynamic -> static) x5 -> (GC x5)

if (__pthread_key_legacy_behaviour == 0) {
_pthread_tsd_cleanup_new(self);
} else {
_pthread_tsd_cleanup_legacy(self);
}
#endif // !VARIANT_DYLD
}

static void
_pthread_tsd_cleanup_new(pthread_t self)
{
int j;

// clean up all keys
for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++) {
pthread_key_t k;
for (k = __pthread_tsd_start; k <= self->max_tsd_key; k++) {
_pthread_tsd_cleanup_key(self, k);
}

for (k = __pthread_tsd_first; k <= __pthread_tsd_max; k++) {
_pthread_tsd_cleanup_key(self, k);
}
}

self->max_tsd_key = 0;
}

static void
_pthread_tsd_cleanup_key(pthread_t self, pthread_key_t key)
{
void (*destructor)(void *);
if (_pthread_key_get_destructor(key, &destructor)) {
void **ptr = &self->tsd[key];
void *value = *ptr;
if (value) {
*ptr = NULL;
if (destructor) { //其中一个就是tls_dealloc
destructor(value);
}
}
}
}

最终会调用自动释放池的tls_dealloc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void tls_dealloc(void *p) 
{
if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
// No objects or pool pages to clean up here.
return;
}

// reinstate TLS value while we work
setHotPage((AutoreleasePoolPage *)p);

if (AutoreleasePoolPage *page = coldPage()) {
if (!page->empty()) objc_autoreleasePoolPop(page->begin()); // pop all of the pools
if (slowpath(DebugMissingPools || DebugPoolAllocation)) {
// pop() killed the pages already
} else {
page->kill(); // free all of the pages
}
}

// clear TLS value so TLS destruction doesn't loop
setHotPage(nil);
}

调用objc_autoreleasePoolPop将其中的对象释放掉。

这里还有一个关键点,与objc_autoreleasePoolPop相对应的objc_autoreleasePoolPush是什么时候调用的?

我们可以打断点查看一下:

可以看到在子线程启动的时候系统内部会执行objc_autoreleasePoolPush,帮你创建好一个自动释放池。所以即使在子线程里不显式使用 @autorelease{} ,创建的自动释放对象最终也会销毁,但是需要等到线程退出时才会进行。所以最好还是创建自己的自动释放池。

其他

1、C++中结构体可以继承类,而类也可以继承结构体(多少有点震惊),这时的访问权限取决于子类或子结构体而非父类或父结构体。

例如:

struct A {};

struct B : A {}; //public继承

class C : A {}; //private继承

2、C++对象的创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc, const char * argv[]) {
// insert code here...
std::cout << "Hello, World!\n";
{
struct MyStruct {
MyStruct() {std::cout << "初始化\n";}
~MyStruct() {std::cout << "销毁\n";}
};
struct MyStruct sd; //这里就已经分配栈内存了。初始化方法也会调用(多少有点震惊)。类似于int a;
}

return 0;
}

//打印
Hello, World!
初始化
销毁

参考

iOS 自动释放池原理探究

黑幕背后的Autorelease

自动释放池的前世今生 —— 深入解析 autoreleasepool

NSAutoreleasePool

iOS内存管理-深入解析自动释放池 挺不错的

觉得文章有帮助可以打赏一下哦!