文章目录
AutoreleasePool引入
我们看一下objc_autoreleasePoolPush和objc_autoreleasePoolPop这两个函数的实现
他们都调用了AutoreleasePoolPage这个函数,我们看一下这个函数的实现
AutoreleasePoolPage的结构实现
//当只有一个池时,空的\池\占位符存储在TLS中
//已推送,且从未包含任何对象。这样可以节省内存
//当顶层(即libdispatch)推送和弹出池,但
//永远不要使用它们。
# 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; // 0xA3A3A3A3 after releasing
// AutoreleasePoolPage的大小,通过宏定义,可以看到是4096字节
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
//4096
#endif
static size_t const COUNT = SIZE / sizeof(id);
//定义内存大小 4096
magic_t const magic; // 16字节
// 一个AutoreleasePoolPage中会存储多个对象
// next指向的是下一个AutoreleasePoolPage中下一个为空的内存地址(新来的对象会存储到next处)
id *next; // 8字节 通过begin()方法初始化:
// 保存了当前页所在的线程(一个AutoreleasePoolPage属于一个线程,一个线程中可以有多个AutoreleasePoolPage)
pthread_t const thread; // 8字节 当前pool所处的线程
// AutoreleasePoolPage是以双向链表的形式连接
// 前一个节点
AutoreleasePoolPage * const parent; // 8字节
// 后一个节点
AutoreleasePoolPage *child; // 8字节
uint32_t const depth; // 4字节 page的深度,首次为0,以后每次初始化一个page都加1。
uint32_t hiwat; // 4字节 这个字段是high water的缩写,这个字段用来计算pool中最多存放的对象个数。在每次执行pop()的时候,会更新一下这个字段。
// SIZE-sizeof(*this) bytes of contents follow
//固定的内容占56字节,之外的可以放入page中的对象地址
AutoreleasePoolPage结构
在AutoreleasePoolPage中总共有4096个字节,有56个字节来存储结构体信息(如下图所示)其余用来存储添加到Page中的对象地址
- 结构体第一个成员变量是magic,用来检查这个节点是否已经被初始化了。(同isa一样)
- thread保存当前也所在的线程
- depth表示page的深度,首次为0,每个page的大小都是4096字节(16进制0x1000),每次初始化一个page,depth都加一。它并不影响整个自动释放池的实现,也不在关键方法的调用栈中。
- parent和child两个指针,其实是双向链表。在autoreleasePoolPage内部,各个autoreleasePoolPage之间的连接方式是双向链表
- next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如上图所示移动到下一个为空的内存地址中
- 关于pool与page的关系的理解
POOL_BOUNDARY(哨兵对象)
#define POOL_BOUNDARY nil
POOL_BOUNDARY 只是 nil 的别名,POOL_BOUNDARY直译过来就是POOL的边界。它的作用是隔开page中的对象。并不是每次push与pop之间存进的对象都刚好占满一个page,可能会不满,可能会超过。因此这个POOL_BOUNDARY帮助我们分隔每个@autoreleasepool块之间的对象(其实这句话我不太理解)
在每个自动释放池初始化调用objc_autoreleasePoolPush的时候,都会把一个POOL_SENTINELpush到自动释放池的栈顶,并且返回这个哨兵对象
而当方法objc_autoreleasePoolPop调用时,就会想自动释放池中的对象发送release消息,直到第一个POOL_SENTINEL
只有第一次push的时候会在page中插入一个POOL_BOUNDARY【或者page满了,或者没有hotPage需要使用新的page了】,并不是page的开头都一定是POOL_BOUNDARY
AutoreleasePool流程
先前我们说到过AutoreleasePool主要调用objc_autoreleasePoolPush和objc_autoreleasePoolPop这两个函数
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
objc_autoreleasePoolPush方法
我们先看一下objc_autoreleasePoolPush
中push的实现
static inline void *push()
{
id *dest;
// POOL_BOUNDARY就是nil
// 首先将一个哨兵对象插入到栈顶
if (DebugPoolAllocation) {
// 区别调试模式
// 调试模式下将新建一个链表节点,并将一个哨兵对象添加到链表栈中
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
//POOL_BOUNDARY就是nil
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
我们来看一下autoreleaseFast
这个方法
static inline id *autoreleaseFast(id obj)
{
// hotPage就是当前正在使用的AutoreleasePoolPage
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// 有hotPage且hotPage不满,将对象添加到hotPage中
return page->add(obj);
} else if (page) {
// 有hotPage但是hotPage已满
// 使用autoreleaseFullPage初始化一个新页,并将对象添加到新的AutoreleasePoolPage中
return autoreleaseFullPage(obj, page);
} else {
// 无hotPage
// 使用autoreleaseNoPage创建一个hotPage,并将对象添加到新创建的page中
return autoreleaseNoPage(obj);
}
}
上述方法分三种情况选择不同的代码执行:
-
- 有 hotPage 并且当前 page 不满
调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
- 有 hotPage 并且当前 page 不满
-
- 有 hotPage 并且当前 page 已满
调用 autoreleaseFullPage 初始化一个新的页
调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
- 有 hotPage 并且当前 page 已满
-
- 无 hotPage
调用 autoreleaseNoPage 创建一个 hotPage
调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
最后的都会调用 page->add(obj) 将对象添加到自动释放池中。
- 无 hotPage
-
hotPage 可以理解为当前正在使用的 AutoreleasePoolPage。
add添加对象
简化版:
id *add(id obj) {
id *ret = next;
*next++ = obj;
return ret;
}
//这其实就是一个压栈操作,将对象加入AutoreleasePoolPage,然后移动栈顶指针
autoreleaseNoPage无hotPage
id *autoreleaseNoPage(id obj) {
bool pushExtraBoundary = false;
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
// 将初始化的AutoreleasePoolPage设置成hotPage
setHotPage(page);
// Push the requested object or pool.
// 将对象添加到AutoreleasePoolPage中
return page->add(obj);
}
初始化一个新的AutoreleasePoolPage,将当前也标记为hotPage,然后先想这个page中添加一个哨兵对象。最后将obj添加到这个page中
autoreleaseFullPage当前hotPage对象
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
如果page有child(AutoreleasePoolPage未满),就用它来存放,如果没有,就使用构造器传入Parent新建一个来存放
autoreleaseFast参数
上面我们可以看到dest = autoreleaseFast(POOL_BOUNDARY);
也就是说push调用的autoreleaseFast参数只有POOL_BOUNDARY
此外请注意,push操作是不带参数的,等于就是初始化的时候调用一次。
那么真正压入对象的方法是什么呢
我们先写一个将对象压入自动释放池的操作
int main(int argc, const char * argv[]) {
id __strong obj0 = [[NSObject alloc] init];//生成对象A
id __autoreleasing obj2 = obj0;
return 0;
}
通过查看汇编发现它其实调用的是objc_retainAutorelease
方法,之后层层调用发现调用的是
static inline id autorelease(id obj)
{
printf("static inline id autorelease%p\n", obj);
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
会帮我们把对象当成参数传进去,然后都调用fast方法
objc_autoreleasePoolPop方法
static inline void pop(void *token) {
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;
page->releaseUntil(stop);
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}
可以看到pop是一个静态方法,其总共做了三件事
- 使用
pageForPointer
获取当前token所在的AutoreleasePoolPage
- 调用
releaseUntil
方法释放栈中的对象(是个while循环)、直到stop[token],stop就是传递的参数,一般为哨兵对象 - 调用child的
kill
方法
系统根据当前页的不同状态kill掉不同child的页面
releaseUntil
把page里的对象进行了释放,但是page本身也会占据很多空间,要通过kill()来处理
如果当前page小于一半满,则把当前页的所有孩子都杀掉,否则,留下一个孩子,从孙子开始杀。Apple假设,当前page一半都没满,说明已经够了,把接下来的全kill,如果超过一半,就认为下一页还有存在的必要,所以从孙子开始杀
这样子我认为是为了节约创建page操作,牺牲内存来提升性能。
token
- token是指向该pool的POOL_BOUNDARY指针
- token的本质就是指向哨兵对象的指针,存储着每次push时插入的POOL_BOUNDARY的地址
- 只有第一次push的时候会在page中插入一个POOL_BOUNDARY【或者page满了,或者没有hotPage需要使用新的page了】,并不是page的开头都一定是POOL_BOUNDARY
总结
总结一下:
- 调用push方法会将一个POOL_BOUNDARY(哨兵对象)入栈,并且返回其存放的内存地址,然后压入对象
- 调用pop方法时传入一个POOL_BOUNDARY(哨兵对象)的内存地址,会从最后一个入栈的对象开始发送release消息,释放栈中的对象,直到遇到这个POOL_BOUNDARY
- id *next指向了下一个能存放autorelease对象地址的区域
小tips:
-
关于哨兵对象(POOL_BOUNDARY)和next指针:
next指针只有一个,永远指向下一个能存放autoreleasepool的地址,而哨兵对象可以有很多个,每个autoreleasePool都对应一个哨兵对象,标示这个autoreleasePool对象从哪里开始存。 -
next和child:
next指向下一个能存放autoreleasepool的地址,child是autoreleasePoolPage的参数,指向下一个page。
问题: -
autoreleasePoolPage与RunLoop的关系
引言:
答:
RunLoop和AutoReleasePool是通过线程的方式一一对应的
在非手动添加Autorelease pool下,Autorelease对象是在当前runloop进入休眠等待前被释放的
当一个runloop在不停的循环工作,那么runloop每一次循环必定会经过BeforeWaiting(准备进入休眠):而去BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池,那么这两个方法来销毁要释放的对象,所以我们根本不需要担心Autorelease的内存管理问题 -
休眠具体在干嘛runloop
(后续补充)