Objective-C 属性
这一部分知识分三段:
-
属性的概念
-
属性的特质
-
实例变量的访问
一、属性的概念
基础概念
属性Property
是Objective-C
中的一个特性,用来封装对象中的数据。OC
对象通常会把各种需要的数据封装为对应的实例变量,实例变量通常通过getter
方法和setter
方法来读取和写入变量数据。 而属性最终是通过实例变量来实现,其遵循@property
语法。
访问属性(读取和写入)
请看下面两段代码
@interface Person : NSObject
@property NSString *name;
@property NSString *mobile;
@end
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (NSString *)name;
- (void)SetName:(NSString *)name;
- (NSString *)mobile;
- (void)setMobile:(NSString *)mobile;
@end
上面两段代码的效果是一样的。
所以访问属性,也有两种方式,如下
Person *person = [[Person alloc]init];
NSString *name = person.name;//点语法
NSString *name2 = [person name];//直接调用存取方法
其实,点语法的本质也是调用存取方法,编译器会把点语法转化为对存取方法的调用。
在使用属性之后,编译器在编译期
会自动合成访问属性所需要的方法,所以在编辑时看不到这些方法。
我们可以通过@synthesize
来给实例变量设置指定的名字,如下
#import "Person.h"
@implementation Person
@synthesize name = _personName;
@synthesize mobile = _personMobile;
@end
当然我们还可以通过@dynamic
来告诉编译器:不要创建实现属性所需要的实例变量,也不要为其创建存取方法。如下,
@implementation Person
@dynamic name,mobile;
@end
编译器不会为上面的属性创建存取方法,使用代码访问这些属性也不会出现警告提示,但这可能会造成一些潜在的问题。
二、属性的特质
使用属性时,指定的特质会影响编译器所生成的存取方法。请参考下面的代码
@property (nonatomic,readwrite,copy) NSString *testString;
上面的属性就指定了三项特质。
属性的特质分为四类:
原子性
在默认的情况下,由编译器合成的存取方法会通过锁定机制
来确保其原子性(atomicity
),如果属性具备nonatomic
特质,则不会使用同步锁。如果某一属性不具备nonatomic
特质,那它就是原子的(atomic
)。
atomic
和nonatomic
的区别
1.具备
atomic
特质的获取方法会通过锁定机制来确保其操作的原子性,也就是说,如果多个线程同时读写同一属性,不论什么时候,总能看到有效的值。
2.如果不加锁或者使用nonatomic
,当一个线程正在改写某一属性值时,另一属性这时候进行读取操作,把还没有修改好的值读取出来,这时该线程读取的值可能是不正确的
3.因为在iOS
中使用同步锁对性能的开销比较大,所以开发iOS
程序中所有的属性都声明为nonatomic
。
读写权限
- 具备
readwrite
特质的属性拥有读取方法 getter
和设置方法 setter
。若这个属性由@synthesize
实现,则编译器会自动生成这两个方法。 - 具备
readonly
特质的属性只拥有读取方法
,且当属性由@synthesize
实现实现,编译器才会为其合成读取方法。
内存管理语义
属性用于封装对象的数据,而数据则要有“具体的所有权语义(concret ownership semantic)
”。内存管理语义仅会影响设置方法 setter
。假如我们用设置方法设定一个新的值,它应该保留(retain)
这个值还是只将其赋值给底层变量就好?编译器在自动合成存取方法时,要根据特质来决定所生成的代码。如果自己编写存取方法,则必须要和属性所具备的特质相符
。
-
assign
用于修饰纯量类型(scalar type)
,设置方法只会针对该类型的简单赋值操作。(纯量类型 ,比如CGFloat
NSInteger
)。
-
strong
表明属性定义了一种拥有关系
(owning relationship
)。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
-
weak
表明属性定义了一种非拥有关系
(nonowning relationship
),为这种属性赋值时,设置方法既不保留新值,也不会释放旧值。这与 assign
类似,但在属性所指的对象为被释放时,属性值也会清空 (nil out
)。
-
unsafe_unretained
和assign
的语义相同,但是它用于修饰非纯量类型
的对象类型
(object type
),用于表达一种非拥有关系
(不保留
,unretained
),当目标对象被释放时,属性值不会被自动清空(不安全
,unsafe
),这一点与weak
有区别。
-
copy
表达的所属关系与strong
类似,但设置方法并不会保留新值,而是将其复制copy
一份。当所修饰属性为NSString *
时,会用它来保存封装的值,因为传递给设置方法的新值很可能会指向一个NSMutableString
类的实例,此时若不拷贝字符串,那么设置完属性之后,字符串的值很可能会被修改。所以这时候就要拷贝一份不可变(immutable
)的字符串,来确保对象中的字符串值稳定不会被后续修改。*只要实现属性所用的对象是可变的(mutable)
,就应该在设置新属性值时拷贝一份。
至于属性block
为什么要用copy
修饰,可以参考这篇文章
方法名
通过getter=<name>
或setter=<name>
来为读取方法或设置方法指定名称。
如下
@property (nonatomic,assign,getter=isOn) BOOL on;
讲完上面的一大堆,有一点是必须要注意的:在设置属性所对应的实例变量时,一定要遵循使用的修饰符所对应的语义。
接着请看下面代码
@interface Person : NSObject
@property (copy,readonly) NSString *name;
- (instancetype)initWithName:(NSString *)userName;
@end
@implementation Person
- (instancetype)initWithName:(NSString *)userName{
self = [super init];
if (self) {
_name = [userName copy];
}
return self;
}
@end
为什么在初始化方法中不应该直接调用存取方法?请参考下面的实例变量的访问。
如上,还有一点必须要注意,在初始化方法中设置好属性值之后就不再改变的话就应该使用内存管理语义中的readonly
来修饰。
三、实例变量的访问
之所以 “在对象外部总是通过属性来访问实例变量,而在对象内部,读取实例变量时采用直接访问的方式,设置实例变量采取属性的方式”,是因为:
- 由于不经过
Objective-C
的方法派发(method dispatch
)步骤,所以直接访问实例变量的速度较快。这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。 - 直接访问实例变量时,不会调用它的设置方法,这就绕开了为相关属性所定义的内存管理语义。
eg
,在arc
下直接访问一个copy
修饰的属性,不会copy
该属性,只会保留新值释放旧值。 - 直接访问实例变量,不会触发
KVO 键值观察通知
。 - 通过属性来访问有助于排查与之有关的问题,因为可以给
getter
或setter
方法中添加断点 breakpoint
来监控改属性的调用以及访问的时机。
第一个需要注意的问题就是上面已提出的问题:为什么在初始化方法中不应该直接调用存取方法?
在初始化方法中直接访问实例变量,是因为子类可能会重写(
override
)设置方法。在基类中可能会将某一属性设置为nil
,当子类重写设置方法之后,此时若是通过设置方法来操作,调用的将会是子类的设置方法,这个时候,就有可能抛出异常。
但是有一种情况又必须在初始化方法中调用setter
方法,即,
如果待初始化的实例变量生命在父类中,我们又无法在子类中直接访问该实例变量的话,就需要调用
setter
方法了。
第二个需要注意的问题就是懒加载(lazy initialization)
:
在这种情况下,必须通过
getter
方法来访问实例变量,不能直接访问实例变量,不然实例变量永远不会初始化。
如下代码,
- (ComplexData *)complexData{
if (_complexData == nil) {
_complexData = [[ComplexData alloc]init];
}
return _complexData;
}
如果没有调用getter
方法就直接访问ComplexData
的实例变量 ,就会看到尚未初始化设置好的complexData
,所以懒加载下必须通过存取方法来访问complexData
属性。
关于属性的内容暂时就这么多了,如有不足的地方,还请多多指教,在此先谢谢了。