1.下面代码执行结果如何
// Person.h
@interface Person : NSObject
@property (copy, nonatomic) NSMutableArray *data;
@end
// 调用
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *p = [[Person alloc] init];
p.data = [NSMutableArray array];
[p.data addObject:@"jack"];
[p.data addObject:@"rose"];
NSLog(@"end");
}
运行结果
- (void)setData:(NSArray *)data {
if (_data != data) {
[_data release];
_data = [data copy];
}
}
分析:因为
data
是copy
属性,所以在其set
方法里先执行判断,然后执行release
操作,最后执行copy
操作,变成了一个不可变
对象。
二 copy
- 拷贝的目的:产生一个副本对象,跟源对象互不影响
- 修改了源对象,不会影响副本对象
- 修改了副本对象,不会影响源对象
iOS提供了2个拷贝方法
-
copy
不可变拷贝,产生不可变副本 -
mutableCopy
可变拷贝,产生可变副本
深拷贝和浅拷贝
-
深拷贝
内容拷贝,产生新的对象 -
浅拷贝
指针拷贝,没有产生新的对象
copy和mutableCopy 图解
1.
copy
都是不可变拷贝
,产生不可变副本
。mutableCopy
都是可变拷贝
,产生可变副本
。
2.除了不可变对象
的copy
是浅拷贝
,其他都是深拷贝
。
三 引用计数的存储
在64bit中,引用计数可以直接存储在优化过的isa
指针中,也可能存储在SideTable
类中
-
refcnts
是一个存放着对象引用计数的散列表
四 weak实现原理 - dealloc
- 当一个对象要释放时,会自动调用
dealloc
,接下的调用轨迹是
1.dealloc
2._objc_rootDealloc
3.rootDealloc
4.object_dispose
5.objc_destructInstance
、free
五 自动释放池
自动释放池的主要底层数据结构是:
__AtAutoreleasePool
、AutoreleasePoolPage
调用了
autorelease
的对象最终都是通过AutoreleasePoolPage
对象来管理的__AtAutoreleasePool
结构体
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
下面将代码进行转换
@autoreleasepool {
Person *p4 = [[[MJPerson alloc] init] autorelease];
}
将上述代码转成C++代码
{
__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"));
}
去除一些不必要的代码后变成下面这个样子
{
__AtAutoreleasePool __autoreleasepool;
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
又因为__AtAutoreleasePool
是一个结构体,所以创建时会调用其构造函数__AtAutoreleasePool()
,当离开其作用域后,会调用其析构函数~__AtAutoreleasePool()
,所以上面的代码又可以转换成下面的代码
atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
5.0 自动释放池
- 自动释放池的主要底层数据结构是:
__AtAutoreleasePool
、AutoreleasePoolPage
- 调用了
autorelease
的对象最终都是通过AutoreleasePoolPage
对象来管理的
源码分析
- clang重写
@autoreleasepool
- objc4源码:
NSObject.mm
变量说明
-
magic
用来校验 AutoreleasePoolPage 的结构是否完整 -
next
指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() -
thread
指向当前线程 -
parent
指向父结点,第一个结点的 parent 值为 nil -
child
指向子结点,最后一个结点的 child 值为 nil -
depth
代表深度,从 0 开始,往后递增 1 -
hiwat
代表 high water mark
5.1 AutoreleasePoolPage的结构
- 每个
AutoreleasePoolPage
对象占用4096
字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease
对象的地址 - 所有的
AutoreleasePoolPage
对象通过双向链表
的形式连接在一起
atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
上图的执行步骤说明
- 调用
push
方法会将一个POOL_BOUNDARY
入栈,并且返回其存放的内存地址,即返回给atautoreleasepoolobj
。 - 调用
pop
方法时传入一个POOL_BOUNDARY
的内存地址,会从最后一个入栈的对象开始发送release
消息,直到遇到这个POOL_BOUNDARY
-
id *next
指向了下一个能存放autorelease
对象地址的区域
代码例子如下
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool { // r1 = push()
Person *p1 = [[[Person alloc] init] autorelease];
Person *p2 = [[[Person alloc] init] autorelease];
@autoreleasepool { // r2 = push()
for (int i = 0; i < 5; i++) {
Person *p3 = [[[Person alloc] init] autorelease];
}
@autoreleasepool { // r3 = push()
Person *p4 = [[[Person alloc] init] autorelease];
_objc_autoreleasePoolPrint();
} // pop(r3)
} // pop(r2)
} // pop(r1)
return 0;
}
执行结果
- 因为只打印了一个
PAGE
,所以说明他们是在同一个AutoreleasePoolPage
,只是每次一个新的autoreleasepool
,都会插入一个POOL_BOUNDARY
。- 每次释放对象时,都是从后往前释放,直到遇到
POOL_BOUNDARY
为止。
代码例子二
int main(int argc, const char * argv[]) {
@autoreleasepool { // r1 = push()
@autoreleasepool {
MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint();
}
return 0;
}
}
执行结果
代码例子三
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;
}
执行结果
5.2 Runloop和Autorelease
iOS在主线程的Runloop中注册了2个Observer
第1个Observer监听了
kCFRunLoopEntry
事件,会调用objc_autoreleasePoolPush()
第2个Observer
<1> 监听了kCFRunLoopBeforeWaiting
事件,会调用objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
<2> 监听了kCFRunLoopBeforeExit
事件,会调用objc_autoreleasePoolPop()
5.3 autorelease对象在什么时机会被调用release
实践内容可以参考 你真的懂iOS的autorelease吗?
代码例子如下
- MRC环境下
- (void)viewDidLoad {
[super viewDidLoad];
// 这个Person什么时候调用release,是由RunLoop来控制的
// 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release
MJPerson *person = [[[MJPerson alloc] init] autorelease];
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
运行结果如下
- 得出结论,
autorelease
并不是根据对象的作用域
来决定释放时机。- 实际上,
autorelease
释放对象的依据是Runloop
,简单说,runloop
就是iOS
中的消息循环机制,当一个runloop
结束时系统才会一次性清理掉被autorelease
处理过的对象,其实本质上说是在本次runloop迭代结束时
清理掉被本次迭代期间被放到autorelease pool
中的对象的。至于何时runloop结束并没有固定的duration。- 本次runloop迭代休眠之前调用了
objc_autoreleasePoolPop()
方法,然后调用release
,从而释放Person
对象。
5.4 方法里有局部对象, 出了方法后会立即释放吗
- ARC环境下
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
通过打印结果可知,当
person
对象出了其作用域后就销毁,即系统会在它出作用域的时候,自动调用其release
方法。
扩展
既然由runloop
来决定对象释放时机而不是作用域,那么,在一个{}
内使用循环大量创建对象就有可能带来内存上的问题,大量对象会被创建而没有及时释放,这时候就需要靠我们人工的干预autorelease
的释放了。
上文有提到autorelease pool
,一旦一个对象被autorelease
,则该对象会被放到iOS的一个池:autorelease pool
,其实这个pool
本质上是一个stack
,扔到pool中的对象等价于入栈
。我们把需要及时释放掉的代码块放入我们生成的autorelease pool
中,结束后清空这个自定义的pool
,主动地让pool
清空掉,从而达到及时释放内存的目的。优化代码如下
@autoreleasePool{
//domeSomeThing;
}
什么时候用@autoreleasepool
根据 Apple的文档 ,使用场景如下:
- 写基于命令行的的程序时,就是没有UI框架,如
AppKit
等Cocoa
框架时。 - 写循环,循环里面包含了大量临时创建的对象。(本文的例子)
- 创建了新的线程。(非
Cocoa
程序创建线程时才需要) - 长时间在后台运行的任务。
autorelease 机制
基于UI framework
。因此写非UI framework的程序时,需要自己管理对象生存周期。autorelease
触发时机发生在下一次runloop
的时候。因此如何在一个大的循环里不断创建autorelease对象
,那么这些对象在下一次runloop回来之前将没有机会被释放,可能会耗尽内存。这种情况下,可以在循环内部
显式使用@autoreleasepool {}
将autorelease
对象释放。- 自己创建的线程。
Cocoa
的应用都会维护自己autoreleasepool
。因此,代码里spawn
的线程,需要显式添加autoreleasepool
。注意:如果是使用POSIX API
创建线程,而不是NSThread
,那么不能使用Cocoa
,因为Cocoa
只能在多线程(multithreading)
状态下工作。但可以使用NSThread创建一个马上销毁的线程,使得Cocoa进入multithreading状态。
上述结论来自 guijiewan 的CSDN 博客 ,什么时候应该使用Autorelease
什么对象会加入Autoreleasepool中
- 使用
alloc
、new
、copy
、mutableCopy
的方法进行初始化时,由系统管理对象,在适当的位置release。 - 使用
array
会自动将返回值的对象
注册到Autoreleasepool
。 -
__weak
修饰的对象,为了保证在引用时不被废弃,会注册到Autoreleasepool
中。 -
id的指针
或对象的指针
,在没有显示指定时会被注册到Autoleasepool
中。
本文参考MJ底层原理教程,非常感谢
本文参考Autorelease Pool学习笔记,非常感谢。
优秀文章推荐 - Objective-C Autorelease Pool 的实现原理
- 多多点赞,打赏更好,您的支持是我写作的动力。
项目连接地址 - MemoryManage-CADisplayLink+Timer
项目连接地址 - MemoryManage-Copy
项目连接地址 - MemoryManager_autorelease1
项目连接地址 - MemoryManager_autorelease