一. 转成C++代码
我们都知道,在MRC中,当对象调用autorelease后,这个对象会在它所在的自动释放池结束后调用release方法,如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
return 0;
}
person指针指向的对象会在{}结束后调用release方法,但是它底层是怎么实现的呢?
将上面代码转成C++代码,如下:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
}
return 0;
}
上面代码,相信应该很容易理解,剔除没用的,如下:
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
......
}
上面定义了一个__autoreleasepool局部变量,搜索__AtAutoreleasePool定义,发现是个结构体,如下:
//C++的结构体和类很像,结构体中也可以定义函数,你可以认为它就是个类
struct __AtAutoreleasePool {
//构造函数,在创建结构体的时候调用
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
//析构函数,在结构体销毁的时候调用
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
根据上面代码,所以文章开头的代码其实就是这三行:
//构造函数
atautoreleasepoolobj = objc_autoreleasePoolPush();
//对象调用了autorelease
MJPerson *person = [[[MJPerson alloc] init] autorelease];
//析构函数
objc_autoreleasePoolPop(atautoreleasepoolobj);
现在我们知道了,autoreleasepool会在刚开始调用Push,结束调用Pop,想要知道这两个函数内部做了什么还要进去看看。
在objc4里面搜索这两个函数:
void *
objc_autoreleasePoolPush(void)
{
//调用C++类的push()方法
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
//调用C++类的pop()方法
AutoreleasePoolPage::pop(ctxt);
}
可以发现,分别是调用AutoreleasePoolPage类的push()和pop()方法。
小总结:
- 自动释放池的主要底层数据结构是:__AtAutoreleasePool结构体、AutoreleasePoolPage类
- 调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
二. AutoreleasePoolPage类
进入AutoreleasePoolPage类,简化后留下有用的东西:
class AutoreleasePoolPage
{
......
magic_t const magic;
id *next;
pthread_t const thread; //专属的线程
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
......
}
那么这些成员有什么用?
先看结论:
- 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放调用了autorelease方法的对象的地址
- 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
什么是双向链表?
就是A链表可以访问B链表,B链表也可以访问A链表。
为什么要设计成双向链表?
因为一个AutoreleasePoolPage对象只占用4096字节内存,如果存满了,就会创建一个新的AutoreleasePoolPage对象,然后这些AutoreleasePoolPage对象之间通过通过双向链表的形式连接在一起,如下图:
- 0x2000 - 0x1000 = 0x1000,转成10进制就是4096字节,一个AutoreleasePoolPage对象占用4096字节。
- 0x1038 - 0x1000 = 0x0038,转成10进制就是56字节,AutoreleasePoolPage对象内部的七个成员,每个成员占用8字节,所以一共占用56字节。
- 4096 - 56 = 4040,所以剩下的4040字节用来存放调用了autorelease方法的对象的地址,如果这个AutoreleasePoolPage对象里面不够存了,就会创建一个新的AutoreleasePoolPage对象。
上面的begin()函数是什么?同样在NSObject.mm里面找到源码:
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
对于begin(),就是this指针(就是自己的地址,如上面的0x1000)加上它自己有多大(就是他内部的七个成员变量的大小:56),返回的是一个地址,这个地址就是从什么地方开始存储调用了autorelease方法的对象的地址。
同理,对于end(),找到SIZE源码:
static size_t const SIZE = PAGE_MAX_SIZE;
#define PAGE_MAX_SIZE PAGE_SIZE
#define PAGE_SIZE I386_PGBYTES
#define I386_PGBYTES 4096
可以发现,SIZE就是4096字节,所以对于end(),就是自己地址加上4096,就得到结束的地方的地址。
parent指针指向上一个AutoreleasePoolPage对象的地址值,child指针指向下一个AutoreleasePoolPage对象的地址值。双向链表就是通过parent指针和child指针联系在一起的。
三. push()和pop()
那么push()和pop()函数里面究竟做了什么呢?
1. push()
调用push()方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址,那么这句话是什么意思呢?
在objc4搜索POOL_BOUNDARY:
define POOL_BOUNDARY nil //就相当于0
看下图:
就是将0这个值存放到0x1038的位置,然后把0x1038这个地址值返回,如下代码返回的就是0x1038。
atautoreleasepoolobj = objc_autoreleasePoolPush();
// atautoreleasepoolobj = 0x1038
接下来就开始执行代码:
MJPerson *person = [[[MJPerson alloc] init] autorelease];
当发现有一个对象调用了autorelease,就把这个对象的地址值接着0x1038往下存,当发现这个AutoreleasePoolPage对象不够存的时候,就会创建一个新的,然后用它的child指针指向这个新的AutoreleasePoolPage对象,然后用新的AutoreleasePoolPage对象存储,如下,当autoreleasepool里面有1000个对象的时候:
int main(int argc, const char * argv[]) {
@autoreleasepool {
for (int i = 0; i < 1000; i++) {
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
}
那么这1000个对象的地址在AutoreleasePoolPage对象里面存储的结构为:
当代码都执行完后,就会调用objc_autoreleasePoolPop()函数
2. pop()
调用pop()方法时传入一个POOL_BOUNDARY的内存地址(也就是上面说的0x1038),会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。
上面的next是什么呢?
id *next指向了下一个能存放autorelease对象地址的区域。
比如,一开始next指向0x1038,当调用push之后把POOL_BOUNDARY入栈,这时候next就指向0x1038下一格的位置,如果这时候有一个对象的地址存储到了AutoreleasePoolPage对象里面,那么next就指向0x1038下下一格的位置,以此类推。
四. autoreleasepool嵌套
如下代码:
01 int main(int argc, const char * argv[]) {
02 @autoreleasepool { // r1 = push()
03 MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
04 MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
05
06 @autoreleasepool { // r2 = push()
07 MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
08
09 @autoreleasepool { // r3 = push()
10 MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
11
12 } // pop(r3)
13 } // pop(r2)
14 } // pop(r1)
15 return 0;
16 }
第02行将POOL_BOUNDARY (r1)存进去
第03、04行分别将p1、p2存进去
第06行将POOL_BOUNDARY (r2)存进去
第07行将p3存进去
第09行将POOL_BOUNDARY (r3)存进去
第10行将p4存进去
第12行拿到r3,从最后一个进栈的对象(p4)开始release,一直到r3
第13行拿到r2,从最后一个进栈的对象(p3)开始release,一直到r2
第14行拿到r1,从最后一个进栈的对象(p2、p1)开始release,一直到r1
结合下图,更容易理解:
注意:
- 上面说的入栈并不是内存中的堆、栈那个栈,而是数据结构的那种栈。
- 我们知道栈是先进后出,比如上面的存储地址的过程,push进来和pop出去就达到了先进后出的效果。
使用打印验证:
以前说过,可以通过以下私有函数来查看自动释放池的情况:
extern void _objc_autoreleasePoolPrint(void);
_objc_autoreleasePoolPrint函数是私有的,使用extern声明这个函数,就可以直接调用了,如下:
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool { // r1 = push()
MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
@autoreleasepool { // r2 = push()
for (int i = 0; i < 600; i++) {
MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
}
@autoreleasepool { // r3 = push()
MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint(); //查看自动释放池的情况
} // pop(r3)
} // pop(r2)
} // pop(r1)
return 0;
}
打印:
objc[65684]: ##############
objc[65684]: AUTORELEASE POOLS for thread 0x1000aa5c0 //对应的线程
objc[65684]: 606 releases pending. //一共存了606个
objc[65684]: [0x101803000] ................ PAGE (full) (cold) //第一页 cold
objc[65684]: [0x101803038] ################ POOL 0x101803038 //POOL_BOUNDARY (r1)
objc[65684]: [0x101803040] 0x100541000 MJPerson
objc[65684]: [0x101803048] 0x100541420 MJPerson
objc[65684]: [0x101803050] ################ POOL 0x101803050 //POOL_BOUNDARY (r2)
objc[65684]: [0x101803058] 0x100540e10 MJPerson
objc[65684]: [0x101803ff0] 0x10053bd10 MJPerson
......省略
objc[65684]: [0x101802ef0] 0x10053bd20 MJPerson
objc[65684]: [0x101803ff8] 0x10053bd20 MJPerson
objc[65684]: [0x100806000] ................ PAGE (hot) //创建一个新的AutoreleasePoolPage对象 第二页 hot
objc[65684]: [0x100806038] 0x10053bd30 MJPerson
objc[65684]: [0x100806040] 0x10053bd40 MJPerson
......省略
objc[65684]: [0x101803048] 0x100541420 MJPerson
objc[65684]: [0x100806348] 0x10053c350 MJPerson
objc[65684]: [0x100806350] ################ POOL 0x100806350 //POOL_BOUNDARY (r3)
objc[65684]: [0x100806358] 0x10053c360 MJPerson
objc[65684]: ##############
一共存了606个,其中603个对象,3个POOL_BOUNDARY。从上面打印也可以看出,当AutoreleasePoolPage对象存不下时会创建一个新的AutoreleasePoolPage对象。其中第一页是cold是冷的意思,第二页是hot是热的意思,hot页是当前页的意思,以后release的时候就会从hot页开始。
五. 查看push()、autorelease、pop()源码
1. 先看push()
现在我们看push()和pop()函数的源码应该就很容易理解了:
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
//没有page对象就new一个,并将POOL_BOUNDARY传进去
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
//有page对象,直接将POOL_BOUNDARY传进去
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
进入autoreleaseFast函数:
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) { //如果有page并且没有满
return page->add(obj); //将POOL_BOUNDARY入栈
} else if (page) { //如果有page(满了)
return autoreleaseFullPage(obj, page);
} else { //如果没page
return autoreleaseNoPage(obj);
}
}
通过上面两段push()的源代码可知,如果有page就直接将POOL_BOUNDARY入栈,如果没有page,就创建page之后再将POOL_BOUNDARY入栈,验证了我们上面说的。
2. 再看autorelease
在NSObjec.mm -> autorelease -> rootAutorelease -> rootAutorelease2,进入rootAutorelease2函数:
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
//哪一个对象调用autorelease就将哪一个对象传进去,并调用AutoreleasePoolPage的autorelease方法
return AutoreleasePoolPage::autorelease((id)this);
}
可以看出,哪一个对象调用autorelease就将哪一个对象传进去,并调用AutoreleasePoolPage的autorelease方法,进入autorelease方法:
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj); //将对象地址值add到AutoreleasePoolPage里面去
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
可以发现,这里也调用了autoreleaseFast函数,autoreleaseFast函数的实现上面有,就是将对象地址值add到AutoreleasePoolPage里面去。
3. 再看pop()
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
......省略
page = pageForPointer(token);
stop = (id *)token; //token就是POOL_BOUNDARY的地址值,将token赋值给stop
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
} else {
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop); //释放对象,直到遇到stop为止
......省略
}
省略掉其他代码,只看上面的注释。
pop()函数需要传入一个参数,这个参数就是POOL_BOUNDARY的地址值,最后调用releaseUntil释放对象,直到遇到stop为止,进入releaseUntil函数:
void releaseUntil(id *stop)
{
//使用while循环不断取出page里面的东西
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
//将取出的东西release掉
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
上面代码使用while循环不断取出page里面存储的对象,然后将取出的对象release掉,和我们上面讲的一样。
六. 总结
下面代码:
@autoreleasepool {
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
底层就是下面三行:
//构造函数
atautoreleasepoolobj = objc_autoreleasePoolPush();
//对象调用了autorelease
MJPerson *person = [[[MJPerson alloc] init] autorelease];
//析构函数
objc_autoreleasePoolPop(atautoreleasepoolobj);
- 自动释放池的主要底层数据结构是:__AtAutoreleasePool结构体、AutoreleasePoolPage类,调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。
- 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放调用了autorelease方法的对象的地址,所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。
- 调用push()方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址。
- 当发现有一个对象调用了autorelease,就把这个对象的地址值接着POOL_BOUNDARY往下存,当发现这个AutoreleasePoolPage对象不够存的时候,就会创建一个新的,然后用它的child指针指向这个新的AutoreleasePoolPage对象,然后用新的AutoreleasePoolPage对象存储。
- 调用pop()方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。
七. RunLoop与autorelease
1. 面试题1
调用autorelease的对象在什么时机会被调用release?
① 如果有@autoreleasepool{}
创建一个新项目,修改为MRC,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"111");
@autoreleasepool {
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
NSLog(@"333");
}
打印:
111
-[MJPerson dealloc]
333
根据我们上面学的知识,很好理解,因为使用了autoreleasepool,所以autoreleasepool里面调用了autorelease方法的对象会在{}结束之后释放,所以才是上面打印。
② 如果没写@autoreleasepool{}
那如果没写@autoreleasepool呢?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"111");
MJPerson *person = [[[MJPerson alloc] init] autorelease];
NSLog(@"333");
}
打印:
111
333
-[MJPerson dealloc]
你可能会想,上面的代码没autoreleasepool,但是整个程序的main函数里面不是有一个autoreleasepool吗,那上面那些代码是不是被main函数的autoreleasepool管理呢?
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
显然不是的,如果上面的代码是被main函数的autoreleasepool管理的,那么程序退出之前这个autoreleasepool是不会结束的,对象就不会被释放,但是上面打印的结果表明对象的确被释放了,说明上面那些代码不是被main函数的autoreleasepool管理的。
可能你还会想,那person对象会不会是在viewDidLoad方法调用完毕再释放的呢?
这个更好验证:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"111");
MJPerson *person = [[[MJPerson alloc] init] autorelease];
NSLog(@"333");
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
打印:
111
333
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[MJPerson dealloc]
-[ViewController viewDidAppear:]
可以发现,是在viewWillAppear之后才释放的,可能你会越来越迷糊,那person对象究竟是在什么时候被释放呢?
其实这个问题和RunLoop有关:
打印NSLog(@"%@",[NSRunLoop mainRunLoop]),打印结果比较多,抽取我们需要的两个监听器,如下:
observers = (
"<CFRunLoopObserver 0x600002314be0 [0x10aac2ae8]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10d690c9d), context = <CFArray 0x600001c5cf00 [0x10aac2ae8]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7ffc78003058>\n)}}",
......省略
"<CFRunLoopObserver 0x600002314c80 [0x10aac2ae8]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10d690c9d), context = <CFArray 0x600001c5cf00 [0x10aac2ae8]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7ffc78003058>\n)}}"
),
RunLoop的状态如下,下面会用到
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 1
kCFRunLoopBeforeTimers = (1UL << 1), 2
kCFRunLoopBeforeSources = (1UL << 2), 4
kCFRunLoopBeforeWaiting = (1UL << 5), 32
kCFRunLoopAfterWaiting = (1UL << 6), 64
kCFRunLoopExit = (1UL << 7), 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
观察打印结果里面的activities
第一个observer的activities = 0x1,就是1,说明第一个observer监听kCFRunLoopEntry状态。
第一个observer的activities = 0xa0,转成十进制是160,正好是32+128,说明第二个observer监听kCFRunLoopBeforeWaiting和kCFRunLoopExit状态。
看着MJ老师的图,我们进行总结:
总结:
- iOS在主线程的Runloop中注册了2个Observer,当第1个Observer监听到了进入状态(kCFRunLoopEntry),就会调用objc_autoreleasePoolPush()
- 当第2个Observer监听到了即将休眠状态(kCFRunLoopBeforeWaiting)就会调用objc_autoreleasePoolPop()和objc_autoreleasePoolPush()
- 当第2个Observer监听到了即将退出状态(kCFRunLoopBeforeExit)就会调用objc_autoreleasePoolPop()
这样,整个RunLoop运行循环中push和pop就能完全对得上。
现在就能回答刚才的问题了,如果没写@autoreleasepool{},由于整个程序没有退出,autoreleasepool里面调用了autorelease方法的对象会在RunLoop休眠之前被释放。
- (void)viewDidLoad {
[super viewDidLoad];
// 这个Person什么时候调用release,是由RunLoop来控制的
// 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
再次观察上面的打印信息:
111
333
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[MJPerson dealloc]
-[ViewController viewDidAppear:]
既然person对象会在RunLoop休眠之前被释放,那么可以看出viewDidLoad和viewWillAppear处在同一次运行循环中(因为一次休眠到下一次休眠是一个循环)。
2. 面试题2
ARC中,方法里有局部对象,出了方法后会立即释放吗?
这个问题,我们猜想有两种可能:
- 如果ARC生成的代码是直接在方法完成之前给对象调用了一次[person release],那么对象就会在方法结束之后立马释放。
- 如果ARC生成的代码是直接在对象后面加autorelease,那么对象就会在RunLoop休眠之前被释放。
我们实验一下,运行如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
MJPerson *person = [[MJPerson alloc] init];
NSLog(@"%s", __func__);
// ARC中就相当于在这里生成一行 [person release];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
打印:
-[ViewController viewDidLoad]
-[MJPerson dealloc]
-[ViewController viewWillAppear:]
-[ViewController viewDidAppear:]
可以发现,viewDidLoad执行完后对象立马就被释放了,说明ARC中,方法里有局部对象,出了方法后会立即释放,因为就相当于在方法的最后加一行release代码。
Demo地址:autorelease