@property 的属性关键字有 nonatomic
、atomic
、readonly
、writeonly
、readwrite
、assign
、retain
、copy
、strong
、weak
、unsafe_unretained
、nonnull
、nullable
、null_resettable
。
下面介绍一些常用的关键字:
1.) nonatomic
和 atomic
:
不写的话默认就是 atomic ,默认关键字。atomic
和 nonatomic
的区别在于,系统自动生成的 getter/setter 方法不一样。如果你自己写 getter/setter,那 atomic/nonatomic/retain/assign/copy 这些关键字只起提示作用,写不写都一样。
对于atomic
的属性,系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。假设有一个atomic
的属性"name"
,如果线程 A 调[self setName:@"A"]
,线程 B 调[self setName:@"B"]
,线程 C 调[self name]
,那么所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行 getter/setter,其他线程就得等待。因此,属性 name 是读/写安全的。
但是,如果有另一个线程 D 同时在调[name release]
,那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。
如果 name
属性是 nonatomic
的,那么上面例子里的所有线程 A、B、C、D 都可以同时执行,可能导致无法预料的结果。如果是 atomic
的,那么 A、B、C 会串行,而 D 还是并行的。
2.)readonly
只读属性,有getter方法,但是直接调用setter方法会报错。
但是我们还是可以利用KVC
为readonly
属性赋值。
例如:定义一个ACLStudent
的类,该类里面有一个firstName
属性。
如果直接调用setter方法,会报错
改用KVC来赋值,就可以修改成功
当使用 setValue:forKey:
来设置对象的属性时,会以下面的优先顺序来寻找对应的 key
:
- 消息接收对象会查找是否存在满足
set<Key>:
格式的存取方法。 - 如果不存在满足条件的存取方法,且消息接收对象的类方法
+ (BOOL)accessInstanceVariablesDirectly
返回 YES,那么该对象会以_<key>
,_is<Key>
,<key>
,is<Key>
的顺序查找是否存在对应的key。 - 如果存在对应的存取方法或者找到对应的实例变量,那么就会改变该 key 所对应的值 value。必要的话,value 所对应的值会从对象中解析出来,如 Representing Non-Object Values 所描述的那样。
- 如果没有找到对应的存取方法或者实例变量,那么该消息对象的
setValue:forUndefinedKey:
将会调用。
对于上述第2点说明一下,如果我们不想让 setValue:forKey:
方法改变对象的属性值,那么重写其类方法 + (BOOL)accessInstanceVariablesDirectly
返回 NO (该方法默认返回 YES,即在不存在满足条件的存取方法时,允许直接访问属性对应的实例变量);在搜索实例变量时,会首先检查带下划线的实例变量,然后检查不带下划线的实例变量。
对于上述第3点举例说明,如果要修改对象的属性 NSInteger studentId
, 注意其是 NSInteger
类型,我们在调用 setValue:forKey:
方法时可以像这样
[self setValue:@(20) forKey:NSStringFromSelector(@selector(studentId))];
传入一个 NSNumber 对象也可以,Objective-C 会处理好一切。
对于上面的示例,使用 setValue:forKey:
实际修改的是 _firstName
实例变量的值。不要忘记,我们在声明一个 firstName
的属性时,编译器会为我们自动合成一个_firstName
的实例变量。
总结:
当我们声明一个 readonly 的属性,外部可能会通过 KVC 修改该属性值。
为了避免 KVC 修改属性值,须将定义属性所在类的类方法 + (BOOL)accessInstanceVariablesDirectly
重写,使其返回 NO.
3.)strong
和 weak
strong
: 持有对象,引用计数+1;
weak
: 不持有对象,引用计数不变,只适用于对象。比如代理(为了防止循环引用),UI控件(add到父视图上时,被父视图持有。对UI控件的引用,iOS会自动将其设置为弱变量(weak))
不同的是, 当一个对象不再有strong
类型的指针指向它的时候 它会被释放 ,即使还有weak
型指针指向它。
一旦最后一个strong
型指针离去 ,这个对象将被释放,所有剩余的weak
型指针都将被清除。
当weak
修饰的对象被别的变量释放,那么弱变量会被自动设置为nil,这样可以有效地防止崩溃
4.) retain
、 assign
、 unsafe_unretained
assign
: 修饰基础数据类型和C数据类型,当被释放后,变量不会被自动置为nil,会变成野指针。
unsafe_unretained
:unsafe_unretained
从命名就可以看出意义所在,unsafe即是不会自动设置为nil,如果对象被释放了,再进行访问,程序会crash;unretained
与weak
类似,不会影响对象的引用计数
retain
:释放旧对象,创建新对象。 引用计数会+1,强引用。
copy
: 拷贝一个新的对象,新对象的引用计数+1,释放旧对象。
5.) copy
首先我们看一下对象和指针,看图
中间这个 “=” 符号 将2段代码连在一起, 充当一个桥梁作用:
第一: 允许 p 指针 指向这个对象占用的内存区域的首地址(可以简洁的理解为 允许p指针指向对象)
第二: 将这个对象地址赋值给p指针
第三:后续所有对 “对象“的操作,都可以通过对p指针进行间接操作来完成。
copy
和 mutableCopy
copy
就是浅拷贝,mutableCopy
就是深拷贝吗?
任何对象都可以执行 copy
和 mutableCopy
?
看一下官方关于深拷贝和浅拷贝的解释图:
结论:
1.浅复制,复制的是指向对象的指针,并不会复制对象本身,不会创建一个新的对象;
2.,深复制,复制的是对象本身,会创建一个新的对象。
对引用计数的影响:
浅copy,类似strong,持有原始对象的指针,会使retainCount加一。浅copy和strong引用的区别仅仅是浅copy多执行一步copyWithZone:方法。
深copy,会创建一个新的对象,不会对原始对象的retainCount变化。
使用原则:
\ | copy |
mutableCopy |
---|---|---|
不可变对象 |
原来的对象,不可变 |
新对象 ,可变 |
可变对象 |
新对象,不可变 |
新对象,可变 |
1.针对不可变对象调用copy返回该对象本身,调用mutableCopy返回一个可变对象(新的);
2.针对可变对象调用copy返回一个不可变对象(新的),调用mutableCopy返回另外一个可变对象(新的)。
3.针对集合类对象(NSArray,NSMutableArray,NSDictionary,NSMutableDictionary,NSSet等)进行深拷贝,拷贝的是对象本身,变成一个新的对象,但是集合里面的元素,进行的是浅拷贝,拷贝的是地址。
注意:
使用copy
,需要该对象遵循NSCopying
协议;
使用mutableCopy
,需要该对象遵循NSMutableCopying
协议,否则会crash在copyWithZone
和mutableCopyWithZone
方法上。
例:
UIView及其父类 并没有像 数组 字典 字符串这些类一样 遵守 NSCopying, NSMutableCopying 协议,下面代码程序会crash。
@property(nonatomic, copy)NSMutableArray *myCopyArray;
self.myCopyArray = [NSMutableArray arrayWithObjects:@"1",@"2",@"3", nil];
[self.myCopyArray removeObjectAtIndex:0];
运行后crash:
-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x14fc18f20
所以使用的时候,要注意对象是可变的还是不可变的。
关于 copy 和 block
: