引用计数:
-当我们创建一个实例对象,它的引用计数为1;
-当我们向一个对象发送retain消息,它的引用计数+1;
-当我们向一个对象发送release消息,它的引用计数-1;
-当我们向一个对象发送autorelease消息,它的引用计数会在当前自动释放池的末尾-1;
-当一个对象的引用计数减到0,它的内存会被回收。
在MRC下如下代码:
@autoreleasepool{
NSObject *obj = [[NSObject alloc] init] autorelease];
}
通过clang
命令可以转换成cpp
源代码,在代码中包含__AtAutoreleasePool
结构体如下:
//自动释放池结构体
struct __AtAutoreleasePool {
__AtAutoreleasePool(){/*构造函数*/
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool(){/*析构函数*/
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void *atautoreleasepoolobj;
};
该结构体有一个void *
类型成员变量atautoreleasepoolobj
,保存了构造函数中调用objc_autoreleasePoolPush()
的返回值,在析构函数中调用objc_autoreleasePoolPop(atautoreleasepoolobj)
,将该值传递了进去。
搜索源码,可以看到objc_autoreleasePoolPush()
内部调用了AutoreleasePoolPage::push()
,objc_autoreleasePoolPop(ctxt)
内部调用了AutoreleasePoolPage::pop(ctxt)
。
重点来到了AutoreleasePoolPage
,AutoreleasePoolPage
继承自AutoreleasePoolPageData
,它的结构如下:
struct AutoreleasePoolPageData{
magic_t const magic; // 16
__unsafe_unretained id *next; // 8
pthread_t const thread; // 8
AutoreleasePoolPage * const parent; // 8
AutoreleasePoolPage *child; // 8
uint32_t const depth; // 4
uint32_t hiwat; // 4
}
class AutoreleasePoolPage : private AutoreleasePoolPageData{}
- magic:用来校验AutoreleasePoolPage的结构是否完整
- next:指向栈顶,也就是最新入栈的autorelease对象的下一个位置
- thread:指向当前线程
- parent:指向父节点
- child:指向子节点
- depth:表示链表的深度,也就是链表节点的个数
- hiwat:表示high water mark(最高水位标记)
通过数据结构,我们可以看到AutoreleasePoolPage
是一个双向链表结构,parent
指向了父节点,child
指向了子节点。那么整个的自动释放池到底是怎么运作起来的呢?接下来我们将重点分析一下AutoreleasePoolPage::push()
和AutoreleasePoolPage::pop(ctxt)
这2个方法。
static inline void *push() {
id *dest = autoreleaseFast(POOL_BOUNDARY);
return dest;
}
去掉
DebugPoolAllocation
调试的if分支,内部调用了autoreleaseFast(POOL_BOUNDARY)
,返回了dest
。# define POOL_BOUNDARY nil
,则表示push
的时候传入了的POOL_BOUNDARY
是一个nil,POOL_BOUNDARY
也叫哨兵对象。
static inline id *autoreleaseFast(id obj){
AutoreleasePoolPage *page = hotPage();//获取hotPage
if (page && !page->full()) {
//page存在且没有满,则添加obj(这里obj是POOL_BOUNDARY)
return page->add(obj);
}
else if (page) {
//page存在且已满,传入obj和page(这里obj是POOL_BOUNDARY)
return autoreleaseFullPage(obj, page);
}
else {
//page不存在的情况下
return autoreleaseNoPage(obj);
}
}
这个函数做的是一个初步判定的工作:如果当前
hotPage
存在且未满,则page-add(obj)
;如果page
存在且已满则调用autoreleaseFullPage(obj, page)
;page
不存在的情况下调用autoreleaseNoPage(obj)
。按照逻辑来时首次应当是不存在hotPage
的,所以我们先看看autoreleaseNoPage(obj)
。
static __attribute__((noinline)) id *autoreleaseNoPage(id obj){
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
//是否有占位
pushExtraBoundary = true;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
//设置一个占位
return setEmptyPoolPlaceholder();
}
//有占位后就初始化一个page。并设置为hotPage
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
//将哨兵对象加入作为边界的标记
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
//将请求的对象加入
return page->add(obj);
}
打印obj对象如下:
(lldb) po obj
<__NSBundleTables: 0x1007217a0>
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(token, page, stop);
}
templatestatic void popPage(void *token, AutoreleasePoolPage *page, id *stop){
page->releaseUntil(stop);//向对象发送release消息,传入的stop是哨兵对象的地址
...
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();//如果当前存放的对象少于一半,删除子节点中的内容。【保留了parent-this节点】
}
else if (page->child->child) {
page->child->child->kill();//存放对象多余一半,且子节点的子节点存在,删除子节点的子节点。【保留下了parent-this-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个对象;第二页中只有一个对象。