Objective-C中,在声明特点时,通常会不假思索的运用nonatomic来润饰特点,但去面试的时分,总是被问到atomic,使咱们不知从何说起,或者说只能简单的答复一句,atomic润饰的特点是「原子性」的,不能同时读写等。本篇文章就来探究一下atomic背后的原理。
1.寻觅进口
由于声明特点这个语法是OC特有的,根本能够确认,对特点的操作是在objc源码中完成,所以,为了便利探究,咱们直接在objc4-756.2源码中来寻觅处理特点的代码。
打开objc-756.2源码,测验全局查找setproperty关键字:
盯梢其完成,发现所有为特点赋值的办法终究都会走到reallySetProperty()这个函数:
在工程中新建一个Person类,为了便利研讨,Person只要一个name特点,留意它是atomic的:
// Person.h
@interface Person : NSObject
@property (atomic, copy) NSString *name;
@end
然后在main()函数中实例化一个Person并为name特点赋值:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
p.name = @"Objc";
}
return 0;
}
然后在reallySetProperty()这个函数进口打上断点,运转程序,验证一下设置特点时是否会走到这个函数:
能够看到,的确从-[Person setName:]调用到了reallySetProperty()这个函数。
2.源码分析
2.1 赋值
reallySetProperty()的源码:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
参数解析:
-
self:这儿便是main()函数中实例化的Person目标p
-
-cmd:这儿便是setName:
-
newValue:这便是要赋的新值"Objc"
-
offset:指针偏移量。目标p的指针是它第一个成员isa的地址,而咱们的Person只要一个特点,系统会依据这个p的指针 +offset来找到特点对应成员变量_name的内存地址。isa是个指针,所以这儿offset = 8
-
atomic:是否是原子性的,此处为YES
-
copy:是否拷贝,此处为YES
-
mutableCopy:是否可变类型拷贝,此处为NO
源码分析:
- 假如
offset = 0,便是修改isa,因此调用object_setClass()
- 依据偏移量获取特点成员地址
slot
- 依据是否
copy或mutableCopy来拷贝newValue或者objc_retain(newValue)
- 记载
oldValue,在赋值完新值之后释放旧值objc_release(oldValue)
- 假如是非
atomic的,直接对特点赋值
- 假如是
atomic的,先加锁,在赋值,再解锁
其间,加锁运用的spinlock_t大家能够百度下,本文不再赘述。
可见,所谓atomic润饰的特点,只是在赋值的时分加了一把锁。
2.2 取值
取值调用的是objc_getProperty():
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);
}
- 假如不是
atomic的,直接依据指针和偏移量取到值返回
- 假如是
atomic的,依然是「加锁获取」,而且进行了objc_retain()操作,返回时返回的是主动释放目标objc_autoreleaseReturnValue()
3.留意事项
atomic润饰特点加的锁,仅仅是在setter和getter内部加的锁,确保取值/赋值时的线程安全性,可是假如像下面这样运用并不能确保线程安全:
@interface RootViewController ()
@property (atomic) NSInteger number;
@end
@implementation RootViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 1000; i++) {
self.number = self.number + 1;
NSLog(@"number: %ld", self.number);
}
});
// 线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 1000; i++) {
self.number = self.number + 1;
NSLog(@"number: %ld", self.number);
}
});
}
@end
特点是atomic润饰的,按理说应该是线程安全的,两个线程各对number做了1000次循环+1,终究numer的值应该是2000,但输出的值却不是期望的2000。
// 终究的几行输出
2020-03-07 22:37:21.713683+0800 TestObjC[23813:2171198] number: 1986
2020-03-07 22:37:21.714004+0800 TestObjC[23813:2171198] number: 1987
2020-03-07 22:37:21.714267+0800 TestObjC[23813:2171198] number: 1988
2020-03-07 22:37:21.714541+0800 TestObjC[23813:2171198] number: 1989
2020-03-07 22:37:21.714844+0800 TestObjC[23813:2171198] number: 1990
2020-03-07 22:37:21.715027+0800 TestObjC[23813:2171198] number: 1991
2020-03-07 22:37:21.715442+0800 TestObjC[23813:2171198] number: 1992
以上是输出的终究几行,终究的值只加到1992。这是由于两个线程在并发的调用setter和getter,在setter和getter内部是加了锁,可是在做+1操作的时分并没有加锁,导致在某一时刻,线程一调用了getter取到值,线程2刚好紧跟着调用了getter,取到相同的值,然后两个线程对取到的值分别+1,再分别调用setter,使得两次setter其实赋值了持平的值。
- 因此运用
atomic润饰特点时对特点的操作是否是线程安全的,虽然在内部加了锁,但并不能确保肯定的线程安全。



