原子(atom)指化学反应不可再分的基本微粒,原子在化学反应中不可分割,而在程序中一般是指不可被打断或者干扰的操作。
序言
OC中的属性可以修饰成nonatomic和atomic,即原子和非原子属性。atomic属性设计的出发点是保证多线程下使用属性的安全性,由于看不到编译器对于该语法的实际处理,对于内部的实现,流行的观点是这样的:通过对属性的set/get方法加锁实现读写的互斥来保证线程安全。但是仅仅加锁就能保证我们多线程下使用属性的安全性吗?答案是否定的。
先来看看Atomic属性的加锁版本
要保证线程安全,首先需要保证读写互斥,即读的时候不能改值,改值的时候不能读值,否则,我们读到的有可能是一个被释放的对象地址。由于ARC中加入了很多编译期语法,会对于分析问题带来干扰,我们这里的测试代码都运行于MRC下。
static NSString *const objLock = @"objLock";
@interface ViewController ()
@property (retain, atomic) NSObject *obj;
@end
@implementation ViewController
- (void)setObj:(NSObject *)obj {
@synchronized(objLock) {
[obj retain];
[_obj release];
_obj = obj;
}
}
- (NSObject *)obj {
@synchronized(objLock) {
return _obj;
}
}
@end
加锁后确实能够保持读写互斥,并且读取的时刻内存一定有效。但是这种保证,并不具备任何实用性。obj2读取到self.obj之后便走出了互斥锁的作用域,而后依然可能被释放,obj2将不再可用。
NSObject *obj2 = self.obj;
[obj2 retain];
obj2...
引用计数器的短暂维持,保证数据交接过程的安全
要保证obj2不会被其他线程意外释放,还要保证self.obj被调用后,到retain执行完这段时间内,obj的引用计数器一直大于0。这种场景下可以通过retain+autorelease来短暂维持_obj对象。
@interface ViewController ()
@property (retain, atomic) NSObject *obj;
@end
@implementation ViewController
- (void)setObj:(NSObject *)obj {
@synchronized(objLock) {
[obj retain];
[_obj release];
_obj = obj;
}
}
- (NSObject *)obj {
@synchronized(objLock) {
[_obj retain];
[_obj autorelease];
return _obj;
}
}
@end
调用atomic属性时引用计数器的“异常”
在调用atomic属性时,我们会发现对象的引用计数器“异常”的增加,通过对上面atomic实现原理的分析,现在就很容易理解这种现象了。
@interface ViewController ()
@property (retain, atomic) NSObject *obj;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSObject *obj = [NSObject new];
self.obj = obj;
[obj release];
// 每调用一次self.obj会发现retainCount加1,
// 原因在于atomic类型get方法内部对_obj做了retain+autorelease来保证交接过程中对象的有效性,
self.obj;
[self printRetainCount:_obj];
self.obj;
[self printRetainCount:_obj];
// 加入自动释放池时retainCount表现正常
@autoreleasepool {
self.obj;
}
[self printRetainCount:_obj];
@autoreleasepool {
self.obj;
}
[self printRetainCount:_obj];
// 打印结果:
// 2018-03-22 15:02:31.797026+0800 MRCAtomic[78797:8115885] NSObject reference count: 2
// 2018-03-22 15:02:31.797190+0800 MRCAtomic[78797:8115885] NSObject reference count: 3
// 2018-03-22 15:02:31.797288+0800 MRCAtomic[78797:8115885] NSObject reference count: 3
// 2018-03-22 15:02:31.797396+0800 MRCAtomic[78797:8115885] NSObject reference count: 3
}
- (void)printRetainCount:(NSObject *)obj {
NSLog(@"%@ reference count: %d", NSStringFromClass(obj.class), (int)[obj retainCount]);
}
@end
看看objc源码,验证下我们的推断
找到get方法的源码和上述推测一致
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
为什么要在MRC下做分析
ARC是一种编译期语法,编译器在编译的时候,自动加入了retain和release代码,从而自动管理了对象在所有者变化过程中引用计数器的变化,正是因为编译器的介入,这些代码对于开发人员来说是不可见的,不利于问题的定量分析,同时一些细节的特性也使得ARC有别与MRC。
例如,在ARC下,返回值为对象的方法或者函数,将会在函数return前将返回值retain一次。当从这样的函数或方法接收到返回结果时,ARC会在其包含的完整表达式结尾处释放该值,但必须遵守本地值的通常优化。
这样的函数编译器会加上
ns_returns_retained
属性修饰,如下:
id foo(void) __attribute((ns_returns_retained));
- (id) foo __attribute((ns_returns_retained));
这种retain和release策略使得对象在持有者变换过程中不被释放,在ARC下,调用方法获取对象比直接使用成员变量更加安全,例如,特殊情况下,我们会遇到block执行代码中释放block自身的情况,这样的代码很容易产生安全隐患。
@property (copy, nonatomic) void(^block)();
__weak typeof(self) weakself = self;
self.block = ^(){
[obj fun1];
// 释放block
self->_block = nil;
[obj fun2];
};
// 某个时刻运行了block
// 属性调用block不会崩溃,等价于MRC下 Block_copy(_block); _block(); Block_release(_block);
// 实际这种情况下,block内部并不能释放自己
self.block();
// 可能会崩溃,self->_block = nil;可能会使得block释放,从而释放掉block引用的资源,obj成为野指针,运行到[obj fun2]将会崩溃。
_block();
block的本质是函数调用:block所定义的函数+函数所引用的外部变量,Block_copy(...)实际上是强引用或者拷贝block所引用的外部对象和变量。block代码则保存在代码区不需要再额外分配内存。调用的时候则是把引用的变量传入函数,并执行函数。本质上还是一个函数调用的过程,不过增加了一层外部引用资源的维护工作。
为了实现ARC编译器做了大量的工作,这里不再一一描述,更多细节请参考:
1、Clang documentaion Objective-C Automatic Reference Counting (ARC)
2、Transitioning to ARC Release Notes