一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情。
page有一个printAll()方法,可以打印池子中的对象。我们在执行完page->add(POOL_BOUNDARY)和page->add(obj)后分别打印数据如下:
(lldb) p this->printAll()
AUTORELEASE POOLS for thread 0x1000ebe00
1 releases pending.
[0x104811000] ................ PAGE (hot) (cold)
[0x104811038] ################ POOL 0x104811038
(lldb) p this->printAll()
AUTORELEASE POOLS for thread 0x1000ebe00
2 releases pending.
[0x104811000] ................ PAGE (hot) (cold)
[0x104811038] ################ POOL 0x104811038
[0x104811040] 0x1007217a0 __NSBundleTables
两次打印对比发现,第二次打印比第一次多了个__NSBundleTables的实例对象。其中POOL 0x104811038就是哨兵对象,看日志这里的哨兵对象也计入了releases pending对象个数的统计中。在add(obj)函数中ret的是哨兵对象的地址。
来到了最关键的add方法,简化该函数如下:
id *add(id obj){
unprotect();
id *ret;
if(....){
....
ret = (id *)topEntry;
goto done;
}
ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
...
done:
protect();
return ret;
}
在简化后的函数中,返回的地址是ret,(ret = next,即是上一次操作之后的next指向的地址),*next = obj且next++。我们在main中添加如下代码:
printf(">>>>>>\n");
@autoreleasepool {
NXPerson *person = [[[NXPerson alloc] init] autorelease];
NSObject *obj = [[[NSObject alloc] init] autorelease];
}
printf("<<<<<<<\n");
在AutoreleasePoolPage的add()函数末尾,return之前添加如下代码:
this->printAll();
printf("ret=%p, %p, next=%p\n\n", ret, *ret, next);
然后运行查看打印的日志,日志很长,这里只留存main函数中标记的开始结束之间的部分。
多个对象是连续存放的,__AtAutoreleasePool构造函数返回的是插入的哨兵对象的地址,保存在atautoreleasepoolobj上,如果一个页已经满了,则会新开辟一个AutoreleasePoolPage结构,继续插入对象。
__AtAutoreleasePool的析构函数函最终会调用AutoreleasePoolPage::pop(ctxt),这里的ctx即是构造函数返回的哨兵对象的地址, pop函数简化后的代码如下,
static inline void pop(void *token){
...
AutoreleasePoolPage *page = pageForPointer(token);//拿到当前哨兵对象岁在的页
id *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消息,传入的stop是哨兵对象的地址
...
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();//如果当前存放的对象少于一半,删除子节点中的内容。【保留了parent?-this-child节点】
}
else if (page->child->child) {
page->child->child->kill();//存放对象多余一半,且子节点的子节点存在,删除子节点的子节点。【保留下了parent-this-child-child节点】
}
}
}
pop()函数:内部直接调用popPage函数。popPage函数中,releaseUntil出栈操作当前对象。因为哨兵对象在该也存在,原则上该哨兵对象后面的所有对象都应该清理掉。这里是根据清理后的页的数据量大小选择性的保留1到2个空页。
void releaseUntil(id *stop) {
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
//只要如果当前页空了,还没找到哨兵对象,则去父级去找(设置父级为热页)
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
//实际打印测试中,这行代码会清楚掉页上--page->next上的对象
AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;
id obj = (id)entry->ptr;
int count = (int)entry->count;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
for (int i = 0; i < count + 1; i++) {
objc_release(obj);//向非哨兵对象发送release消息。为啥要这么多次??
}
}
}
setHotPage(this);
}
releaseUntil()函数:从next指针倒着清理,向被清理的对象发送release消息,直到找到传入的哨兵对象的指针地址才会退出。如果在当前页没有找到会去父页中继续寻找。
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);
}
kill函数会通过while循环找到最子级的页面page。然后从最子级的页面开始(deathptr = page),保存父级页面到page = page->parent,首先清理掉父节点下保存的child指针,然后删除当前子节点。注意:这里根据do-while循环的规则。如果当前page没有child,则会将当前的自己delete掉;如果有child,则会将child及其子节点delete掉,并不会delete掉自身(deathptr != this)。
上面我们推测说一页最多放505个对象(含哨兵对象):如果我们在一个@autoreleasepool中for循环出505个对象,那么应该会有两个页:第一个页中有一个哨兵对象+504个对象;第二页中只有一个对象。