一 .autorelease对象的释放时机
二.AutoreleasePoolPage对象
三. autorelease与RunLoop的关系
一 .autorelease对象的释放时机
@autoreleasepool
即自动释放池,在自动释放池中声明的对象如果调用了autorelase
方法,那么当@autoreleasepool
代码块执行完之后,在块中调用过autorelease
方法的对象都会自动调用一次release方法。
比如下面的代码:
打印结果:
可以看到打印结果也验证了我们上面所说的,但是事实确实如此么?或者说Person对象是在执行到第23行的时候调用了release方法 这个说法准确么?
为了更好的说明问题我们再来看一个小例子:
上面的代码中Person
对象是在第28行销毁么?
我们来看一下打印结果就知道了:
可以看到
Person
对象并没有在viewDidLoad
执行完销毁,为什么会这样呢?
为了搞明白这些我们有必要来对 autorelease
的内部做一些深入的探究.....
通过下面的命令将我们的.m文件转化成.cpp文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
打开main.cpp
文件,拖到最后,我们可以看到生成的C++代码如下所示:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
Person *per = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_9n_sqylftm50nv_y8jbf4089_fr0000gn_T_main_885529_mi_1);
}
NSLog((NSString *)&__NSConstantStringImpl__var_folders_9n_sqylftm50nv_y8jbf4089_fr0000gn_T_main_885529_mi_2);
return 0;
}
其实重点就是 __AtAutoreleasePool
这个对象,我们来看看该对象的结构:
struct __AtAutoreleasePool {
//构造函数,在创建该结构体的时候调用
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
//析构函数,在销毁该结构体的时候调用
//将objc_autoreleasePoolPush()返回的结果传入到objc_autoreleasePoolPop()中
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
它其实就是一个结构体对象,内部包括了一个构造函数和一个析构函数.
objc_autoreleasePoolPush()
和 objc_autoreleasePoolPop()
又是什么呢?
我们可以从Runtime源码中看到他们的实现如下:
通过上图我们可以看到这个两个方法内部分别调用了
AutoreleasePoolPage
对象的push()
和pop()
方法,所以调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
2. AutoreleasePoolPage
关于AutoreleasePoolPage
对象我们需要知道下面两点:
- 每个
AutoreleasePoolPage
对象占用4096
个字节的内存,除了用来存放它内部的成员变量外,剩下的空间用来存放autorelease对象的地址 - 所有的AutoreleasePoolPage对象都是通过双向链表的形式连接在一起
内部结构:
通过源码我们可以看到AutoreleasePoolPage
对象的大概结构如下图所示:
-
id *next
指向了下一个能存放autorelease对象地址的区域 如下图:
parent
父节点 指向前一个page-
child
子节点 指向下一个page ,如下图
POOL_BOUNDARY
是一个边界对象 nil,之前的源代码变量名是POOL_SENTINEL
哨兵对象,用来区别每个page即每个AutoreleasePoolPage
边界
AutoreleasePoolPage::push()
调用push方法会将一个POOL_BOUNDARY
入栈,并且返回其存放的内存地址.
push就是压栈的操作,先加入边界对象,然后添加person1
对象,然后是person2
对象...以此类推
AutoreleasePoolPage::pop(ctxt);
调用pop方法时传入一个POOL_BOUNDARY
的内存地址,会从最后一个入栈的对象开始发送release
消息,直到遇到这个POOL_BOUNDARY
(因为是双向链表,所以可以向上寻找)
3. autorelease与RunLoop
通过以上的分析我们大概了解了autorelease的内部结构以及执行原理,但是你还是没有说明使用autorelease的对象是在什么时候释放的啊?别着急 慢慢来....
在应用程序刚启动的时候我们打印当前的RunLoop
对象 (关于RunLoop
对象的使用以及分析可以看看我的这篇文章RunLoop的使用)
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@",[NSRunLoop currentRunLoop]);
}
部分结果如下:
由上图我们可以看到iOS应用启动后RunLoop
会注册两个Observer来管理和维护AutoreleasePool
我们知道 RunLoop
有如下这几种状态:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 0 进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 2 即将开始Timer处理
kCFRunLoopBeforeSources = (1UL << 2), // 4 即将开始Source处理
kCFRunLoopBeforeWaiting = (1UL << 5), // 32 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 64 从休眠状态唤醒
kCFRunLoopExit = (1UL << 7), // 128 退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
可以看到 activities = 0x1
监测的是kCFRunLoopEntry
也就是进入RunLoop的状态,此时它会回调objc_autoreleasePoolPush()
方法向当前的AutoreleasePoolPage
增加一个POOL_BOUNDARY
标志创建自动释放池。
而activities = 0xa0
监测的是kCFRunLoopBeforeWaiting
和kCFRunLoopExit
两种状态.
在kCFRunLoopBeforeWaiting
(即将进入休眠)时会调用objc_autoreleasePoolPop()
和objc_autoreleasePoolPush()
方法. 系统会根据情况从最新加入的对象一直往前清理直到遇到POOL_BOUNDARY
标志
而在即将退出RunLoop时会调用objc_autoreleasePoolPop()
方法释放自动释放池内对象。
所以autorelease的释放时机取决于RunLoop的运行状态.