你曾经好奇过 Apple 是怎么处理 atomic 的设置/读取属性的么?至今为止,你可能听说过自旋锁 (spinlocks),信号量(semaphores),锁 (locks),@synchronized 等,Apple 用的是什么呢?因为 Objctive-C 的 runtime 是开源的,所以我们可以一探究竟。
在MRC下, 一个非原子的 setter 看起来是这个样子的:
- (void)setUserName:(NSString *)userName {
if (userName != _userName) {
[userName retain/copy]; // 根据属性的内存管理语义
[_userName release];
_userName = userName;
}
}
这是一个MRC下的retain/release 的版本,ARC 生成的代码和这个看起来也是类似的。当我们看这段代码时,显而易见要是 setUserName: 被并发调用的话会造成麻烦。我们可能会释放 _userName 两次,这回使内存错误,并且导致难以发现的 bug。
对于任何没有手动实现的属性,编译器都会生成一个 objc_setProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 的调用。在我们的例子中,这个调用的参数是这样的:
一共6个参数, 下面有具体实现
objc_setProperty_non_gc(self, _cmd,
(ptrdiff_t)(&_userName) - (ptrdiff_t)(self),
userName, NO, NO);
ptrdiff_t 可能会吓到你,但是实际上这就是一个简单的指针算术,因为其实 Objective-C 的类仅仅只是 C 结构体而已。
objc_setProperty 调用的是如下方法:
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);
}
其实方法实际做的事情非常直接,
非原子性, 用一个临时变量存放旧值,用新值给这个内存区域赋值释放旧值原子性的情况 先用旧值作为key, 从字典中取出一把锁加锁用一个临时变量存放旧值,用新值给这个内存区域赋值解锁释放旧值加锁使用了 PropertyLocks 中的自旋锁spinlock_t中的 1 个来给 *** 作上锁。这是一种务实和快速的方式, set/get本身是一个非常轻量级的 *** 作, 忙等待就行了.
我当时的runtime版本是objc4-750, 看到这个自旋锁之后还在想这个不是已经不推荐了吗, 怎么苹果自己还在用, 又跑到官网上看了一个最新的objc4-818.2确认了一下, 最新的818.2也是使用的自旋锁, 看来苹果对这个atomic不怎么上心了. Runtime官方开源地址
PropertyLocks是一个hashMap,类似于字典,内部使用一个固定长度的数组存放锁,runtime初始化的时候就会创建完成锁并放入到数组中, 在iOS真机上数组数量固定为8,其他设备为64, 也就是说, iOS的PropertyLocks最多提供8个自旋锁给属性的atomic使用.
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);
}
get方法也很简单,通过偏移量取出对应地址的值,
如果是nonatomic的话, 直接返回, 结束如果是atomic的话, 会先从一个字典中取锁, 以返回值作为key取出对应的锁,用取出的锁进行加锁,对返回值进行一次retain解锁把返回值加入自动释放池, 返回,通过set/get源码的阅读, 我们也可以理解为什么苹果不推荐使用atomic了.
PropertyLocks是一个全局生效的字典, 最多提供8个自旋锁给atomic加锁解锁, 当一个项目中有几万个属性都是原子性的时候, 很多属性都会对应到同一把锁上, 那么这个属性就得等其他毫不相关的属性完成读写, 自己才能进行 *** 作, 而且set/get是一个特别高频的 *** 作.PropertyLocks使用的是自旋锁, 自旋锁的特点是忙等待, 当有几万的属性对应到8把锁上, 忙等待就是一个非常常见的 *** 作, 忙等待对cpu的消耗很大, 手机发烫, 效率变低, 而且这样的忙等待是无意义的.atomic只能针对特定场景保证线程安全, 存在局限性. 只能保证set/get的线程安全, 对于更大范围的线程安全是无法保证的.举一个很简单的例子,假设定义属性 NSInteger i 是原子的,对i进行 i = i + 1;
这个 *** 作就是不安全的。因为原子性只能保证读写安全,而该表达式需要三步 *** 作:
1.先进行get, 读取i的值存入寄存器;
2.将寄存器的值加1;
3.使用寄存器修改后的值给i赋值;
atomic只能保证1和3是线程安全的, 如果在第1步完成的时候,i被其他线程修改了,那么表达式执行的结果就会与预期的不一样,也就是不安全的。所以要解决这样的线程安全问题, 只能对 整个表达式进行加锁, 单纯对i 设置atomic达不到预期的.
综上3点, atomic在大范围使用时效率低下, 而且效果不太好, 存在局限性, 这可能就是苹果不推荐atomic的原因了.
虽然这些方法没有定义在任何公开的头文件中,但我们还是可用手动调用他们。我不是说这是一个好的做法,但是知道这个还是蛮有趣的,而且如果你想要同时实现原子属性和自定义的 setter 的话,这个技巧就非常有用了。
// 手动声明运行时的方法
extern void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset,
id newValue, BOOL atomic, BOOL shouldCopy);
extern id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic);
#define PSTAtomicRetainedSet(dest, src) objc_setProperty(self, _cmd, (ptrdiff_t)(&dest) - (ptrdiff_t)(self), src, YES, NO)
#define PSTAtomicAutoreleasedGet(src) objc_getProperty(self, _cmd, (ptrdiff_t)(&src) - (ptrdiff_t)(self), YES)
为何不用 @synchronized ?
你也许会想问为什么苹果不用 @synchronized(self) 这样一个已经存在的运行时特性来锁定属性?? 主要原因还是效率问题.
你可以看看苹果的源代码,就会发现其实发生了很多的事情。Apple 使用了最多三个加/解锁序列,还有一部分原因是他们也添加了异常开解(exception unwinding)机制。相比于更快的自旋锁方式,@synchronized实现要慢得多。由于设置某个属性一般来说会相当快,因此自旋锁更适合用来完成这项工作。@synchonized(self) 更适合使用在你需要确保在发生错误时代码不会产生死锁,而是抛出异常的时候。
多线程下出错案例分析
if (self.contents) {
CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL,
(__bridge CFStringRef)self.contents, NULL);
// 渲染字符串
}
多线程下存在contents属性在通过检查之后却又被设成了nil而导致EXC_BAD_ACCESS崩溃。捕获这个变量就可以简单修复这个问题。
NSString *contents = self.contents;
if (contents) {
CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL,
(__bridge CFStringRef)contents, NULL);
// 渲染字符串
}
类似的问题也可能出现在block中, 但是这时候使用局部变量接收不能解决问题, 还是需要通过传统的加锁/解锁来处理, 明明判断block有值才执行,为什么还是crash
数组,字典尽量用不可变的版本,没有多线程并发的问题,
如果需要添加/删除 *** 作,可以使用局部变量作为可变版本,可变版本的修改完成后进行copy.
注意:对不可变的属性进行赋值的 *** 作也要保证线程安全
// 方案1:使用atomic,下面的方法就没有必要加锁了,
// 方案2:此处用nonatomic,在set/get的地方加锁,
// 一开始尝试了只在set加锁,get不加锁,发现不可以,必须set/get都加锁才能线程安全
@property (nonatomic, strong) NSArray *dataArray;
- (void)addDelegate:(id)delegate {
@synchronized(self) {
NSMutableArray *tempArray = [NSMutableArray arrayWithArray:self.dataArray];
[tempArray addObject:delegate];
self.dataArray = [tempArray copy];
}
}
- (void)removeDelegate:(id)delegate {
@synchronized(self) {
NSMutableArray *tempArray = [NSMutableArray arrayWithArray:self.dataArray];
[tempArray removeObject:delegate];
self.dataArray = [tempArray copy];
}
}
- (void)removeAllDelegates {
@synchronized(self) {
self.dataArray = nil;
}
}
- (void)callDelegate {
NSArray *array = nil;
@synchronized(self) {
array= [NSArray arrayWithArray:self.dataArray];
}
[array enumerateObjectsUsingBlock:^(id delegate, NSUInteger idx, BOOL *stop) {
// 调用delegate
}];
}
参考文章: iOS 记住这些方法,轻松设计自己的线程安全类
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)