Property 总结
iOS 开发最常用的property 包含以下三类:
- 线程安全 atomic/nonatomic
- 内存管理 assign/copy/strong/weak/retain(MRC)/unsafe_unretained(MRC)
- 读取权限 readonly/readwrite/setter=?/getter=?
线程安全
atomic/nonatomic
指定自动合成的存取方法是否为原子操作
什么是原子性?
原子操作是不可分割的操作,在原子操作执行完毕之前,其不会被其它任务或事件终端
被标注atomic会保证property的频繁操作的原子性,可以避免由两个操作对同一个property同时进行操作而造成的错误。
atomic与nonatomic内部实现的区别只是atomic对象setter和getter会加一个缩,而nonatomic没有
@property (nonatomic) NSObject *nonatomicObj;
@property (atomic) NSObject *atomicObj;
- (void)setNonatomicObj:(NSObject *)nonatomicObj{
if (_nonatomicObj != nonatomicObj) {
[_nonatomicObj release];
_nonatomicObj = [nonatomicObj retain];
}
}
- (NSObject *)nonatomicObj{
return _nonatomicObj;
}
- (void)setAtomicObj:(NSObject *)atomicObj{
@synchronized(self) {
if (_atomicObj != atomicObj) {
[_atomicObj release];
_atomicObj = [atomicObj retain];
}
}
}
- (NSObject *)atomicObj{
@synchronized(self) {
return _atomicObj;
}
}
原子性是不是代表线程安全
苹果开发文档已经明确指出:Atomic不能保证对象多线程的安全。所以Atomic 不能保证对象多线程的安全。它只是能保证你访问的时候给你返回一个完好无损的Value而已。举个例子:
如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,有3种可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值。所以atomic可并不能保证对象的线程安全。
自旋锁与atomic
多线程中常用的锁分为自旋锁和互斥锁
维基百科上对自旋锁的解释:
自旋锁 是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种 忙等 (忙碌等待)。一旦获取了自旋锁,线程会一直持有该锁,直至显式释放自旋锁。
获取、释放自旋锁,实际上是读写自旋锁的存储内存或寄存器。因此这种读写操作必须是原子的(atomic)。通常用 test-and-set 原子操作来实现。
自旋锁的核心就是忙等,尝试自定义一个自旋锁如下:
struct spinlock {
int flag;
};
@implementation TPSpinLock {
spinlock _lock;
}
- (instancetype)init {
self = [super init];
if (self) {
_lock = spinlock{0};
}
return self;
}
- (void)lock {
while (test_and_set(&_lock.flag, 1)) {
// wait
}
}
- (void)unlock {
_lock.flag = 0;
}
int test_and_set(int *old_ptr, int _new) {
int old = *old_ptr;
*old_ptr = _new;
return old;
}
@end
如上述代码,我们自定义了test_and_set方法,当线程1进行lock操作的时候会传入flag = 0,test_and_set方法返回0的同时并将flag = 1,这个时候线程2 执行lock的时候一直返回1,那么就一直执行while(1)处于等待状态,直到线程1执行unlock将flag = 0 这个时候就打破while循环,线程2就能继续执行并加锁。
总结:
- 自旋锁 不同于互斥锁 如果访问的资源被占用,它会处于 忙等 状态。自旋锁由于一直处于忙等状态所以他在线程锁被释放的时候会立即获取而不用唤醒,所以其执行效率是很高的,尤其是在多核的cpu上运行效率很高,但是其忙等的状态会消耗cpu的性能,所以其性能比互斥锁要低很多。
- atomic 的底层实现,老版本是自旋锁,新版本是互斥锁。
- atomic 并不是绝对线程安全,它能保证代码进入 getter 和 setter 方法的时候是安全的,但是并不能保证多线程的访问情况下是安全的,一旦出了 getter 和 setter 方法,其线程安全就要由程序员自己来把握,所以 atomic 属性和线程安全并没有必然联系。
内存管理
assign
assign标识对属性只进行简单的赋值操作,不更改操作对象的引用计数。该关键字既可以修饰值类型,也可用于修饰指针类型对象。
当修饰对象时,如果该对象被释放,编译器不会将该属性置为nil,指针仍然指向之前修饰的内存,这时访问该属性会产生野指针。
strong、weak
strong表示属性对修饰的对象有一个强引用,会先保留新值,然后释放旧值。只能修饰对象
weak表示属性对修饰的对象有一个弱引用,对于新值不会增加引用计数,对于旧值也不会减少引用计数。当修饰的对象被释放时,weak修饰的属性被自动被置为nil,能够有效防止野指针错误。
weak常用于防止循环引用
copy
copy修饰的属性在setter方法中会设置拷贝后的指针。
常用于修饰不可变类型的容器,或修饰block
unsafe_unretained
使用unsafe_unretained修饰时效果与assign相同,不会增加引用计数,当所赋的值被销毁时不会被置为nil可能会发生野指针错误。unsafe_unretained与assign的区别在于,unsafe_unretained只能修饰对象,不能修饰标量类型,而assign两者均可修饰。
retain
MRC下使用,作用同string
读写权限
readonly、readwrite
标识属性仅可读,可读写
getter=?、setter=?
修改属性的getter、setter
@property 深入理解
@property = ivar + getter + setter
其中ivar是实例变量,编译器会帮我们自动生成名字为‘_属性名’这样的实例变量,同时自动生成getter和setter方法
编译器帮我们做的事情
当我们声明一个属性后,编译器会将当前关键字修饰的属性自动生成setter方法和getter方法。
@property (nonatamic, copy) NSString *name;
等价于
- (void)setName:(NSString *)name {
_name = name.copy;
}
- (NSString *)name {
return _name;
}
@property (nonatamic, strong) NSMutableArray *array;
等价于
- (void)setArray:(NSMutableArray *)array {
[array retain];
_array = array;
[array release];
}
- (NSMutableArray *)array {
return _array;
}
编译器以上的工作我们称之为属性自动合成
@synthesis
iOS6以后LLVM编译器引入property autosynthesis,即属性自动合成,就是编译器会为每个@property添加@synthesis,如下
@synthesize propertyName = _propertyName;
如果不存在_propertyName实例变量,则会创建一个_propertyName,如果已经存在,则不会添加实例变量
@synthesis使用场景
以下场景自动合成会失效,需要手动合成
什么情况下自动合成会失效 ?
同时重写了属性的setter和getter时;
重写了只读属性的getter时;
使用了@dynamic时;
在 @protocol 中定义的所有属性;
在 category 中定义的所有属性;
父类已有的属性, 子类重载的属性不会自动合成;
- 场景1:同时重写setter、getter
当同时重写一个属性的getter和setter或者属性是readonly时, 重写了getter方法,编译器无法定位到默认的变量_property,此时需要手动定义一个变量
@synthesize property = _property; 指定一个变量来绑定到属性上。
- 场景2:指定属性对应的ivar,修改生成的成员变量名字
synthesize还有一个作用就是给属性对应的成员变量起一个不同的名字,
@synthesize property = _newName;
这样做之后, 在类中只能使用_newName/self.property来存取值, _property不可用。(奇怪的用法, 一般不这么用, 可以直接给属性起名为newName) - 场景3:修改父类属性的读写权限,不推荐
父类有一个只读的属性,比如name, 子类想修改这个name的值, 然后在子类的.m中 写了@synthesize name = _name;然后子类在合适的地方进行修改.
看上去没有问题, 但是编译器实际生产了2个_name变量, 一个是父类的_name变量, 一个是子类合成的_name - 场景4:实现了带有property属性的protocol或者category协议中可以写属性, 但是不会ivar. 以下图的系统协议为例, 我们写的类遵守了协议, xcode就会报警告说需要添加 @synthesize beginTime ; 添加之后警告消除. (这种场景不需要特别关注, 根据xcode提示处理即可.)
@dynamic
@dynamic 告诉编译器,不要为property声明的属性添加setter/getter方法, 由用户自己实现,该属性的getter和setter方法可能不在本类,而在其他地方(比如父类或者在运行时中提供, 如果不实现的话, 运行时会有Unrecoginzed Selector Crash)