iOS @property探究(一): 基础详解
你要知道的@property都在这里
转载请注明出处 http://www.jianshu.com/p/646ae400fe7b
本文大纲
- Apple Adopting Modern Objective-C翻译
- @property基本用法
- @property修饰符详解
- @property进阶话题: 深入代码理解
Apple在Adopting Modern Objective-C一文中介绍了现代化OC的写法,其中就介绍尽量使用@property定义类的属性,先来看看苹果是怎么介绍property的。
Apple Official Property Introduction
Objective-C的属性(property)是通过用@property
定义的公有或私有的方法。例如:
@property(readonly, getter=isBlue) BOOL blue;
属性捕获了对象的状态。它们反映了对象的固有属性(intrinsic attributes)以及对象与其他对象之间的关系。属性(property)提供了一种安全、便捷的方式来与这些属性(attribute)交互,而不需要手动编写一系列的访问方法,如果需要的话可以自定义getter和setter方法来覆盖编译器自动生成的相关方法。
尽量多的使用属性(property)而不是实例变量(attribute)因为属性(property)相比实例变量有很多的好处:
- 自动合成getter和setter方法。当声明一个属性(property)的时候编译器默认情况下会自动生成相关的getter和setter方法
- 更好的声明一组方法。因为访问方法的命名约定,可以很清晰的看出getter和setter的用处。
- 属性(property)关键词能够传递出相关行为的额外信息。属性提供了一些可能会使用的特性来进行声明,包括
assign
(vscopy
),weak
,strong
,atomic
(vsnonatomic
),readwrite
,readonly
等。
属性方法遵守一个简单的命名约定。getter的名字与属性名相同(如:属性名为date
则getter的名字也为date
),setter的名字则是属性名字加上set
前缀并采用驼峰命名规则(如:属性名为date
则setter的名字为setDate
)。布尔类型的属性还可以定义一个以is
开头的getter方法,如:
@property (readonly, getter=isBlue) BOOL blue;
如果按照上面的方法声明则以下所有访问方式都正确:
if (color.blue) {}
if (color.isBlue) {}
if ([color isBlue]) {}
当决定什么东西可以作为一个属性的时候,需要注意以下这些不属于属性:
- init方法
- copy和mutableCopy方法
- 类工厂方法
- 开启某项操作并返回一个BOOL结果的方法
- 明确的改变了一个getter的内部状态的副作用方法
除此之外,在你的代码中使用属性特性的时候请考虑以下规则:
- 一个可读写(read/write)的属性有两个访问方法。setter方法是有一个参数的无返回值方法,getter方法是没有参数的且有一个返回值的方法,返回值类型与属性声明的类型一致。如果将这组方法转换成一个属性,就可以用readwrite关键字来标记它(默认即为
readwrite
可不写)。 - 一个只读(read-only)的属性只有一个访问方法。即getter方法,它不接受任何参数,并且返回一个值。如果将这个方法转换成一个属性,就可以用readonly关键字标记它。
- getter方法应当是幂等(idempotent)的(如果一个getter方法被调用两次,那么第二次调用时返回的结果应该和第一调用时返回的结果相同)。然而,如果一个getter方法每次调用时,是被用于计算结果,这是可以接受的。
如何适配
识别出一组可以被转换成一个属性的方法,如这些方法:
- (NSColor *)backgroundColor;
- (void)setBackgroundColor:(NSColor *)color;
用@property语法和适当的关键字将它们定义成一个属性:
@property (copy) NSColor *backgroundColor;
有关属性关键词和其他注意事项,可以阅读Encapsulating Data。
或者,你也可以使用Xcode中的modern Objective-C转换器来自动转换你的代码。参考Refactoring Your Code Using Xcode。
@property基本用法
手工创建getter与setter
@interface Person : NSObject
{
NSString *_name;
NSUInteger _age;
}
- (void)setName:(NSString*)name;
- (NSString*)name;
- (void)setAge:(NSUInteger)age;
- (NSUInteger)age;
@end
@implementation Person
- (void)setName:(NSString*)name {
_name = [name copy];
}
- (NSString*)name {
return _name;
}
- (void)setAge:(NSUInteger)age {
_age = age;
}
- (NSUInteger)age {
return _age;
}
@end
上述代码就是手动创建变量的getter
和setter
的实现,getter
和setter
本质就是符合一定命名规范(前文Apple Official Property Introduction有讲解)的实例方法。
具体使用方法如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
//函数调用name的setter
[p setName:@"Jiaming Chen"];
//函数调用age的setter
[p setAge:22];
//函数调用name和age的getter,输出 Jiaming Chen 22
NSLog(@"%@ %ld", [p name], [p age]);
}
return 0;
}
通过调用方式可以看出,setter
和getter
本质就是实例方法,可以通过函数调用的方式来使用。
为了方便使用,Objective-C允许使用点语法来访问getter
和setter
。
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
//使用点语法访问name的setter
p.name = @"Jiaming Chen";
//使用点语法访问age的setter
p.age = 22;
//使用点语法访问name和age的getter,输出 Jiaming Chen 22
NSLog(@"%@ %ld", p.name, p.age);
}
return 0;
}
使用点语法访问的方式本质还是调用了我们手动创建的setter
和getter
。
当有很多变量需要设置时,这样手工创建setter
和getter
的方式难免很繁琐,因此合成存取方法就诞生了。
合成存取方法
@interface Person : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
在声明一个属性(property)的时候尽量使用Foundation
框架的数据类型,如整形使用NSInteger
或NSUInteger
表示,时间间隔的浮点类型使用NSTimeInterval
表示,这样代码数据类型更统一。
上面的代码使用@property
声明两个属性name
和age
并为其设置了一些指示符(nonatomic
,copy
,assign
等,下文会详细介绍)。
@synthesize
表示为这两个属性自动生成名为_name
和_age
的底层实例变量,并自动生成相关的getter
和setter
也可以不写编译器默认会自动生成'_属性名'
的实例变量以及相关的getter
和setter
。
这里所说的编译器自动生成的实例变量就如同我们在上文中手动创建setter
和getter
时声明的变量_name
和_age
。也就是说编译器会在编译时会自动生成并使用_name
和_age
这两个变量来存储这两个属性,跟name
和age
没什么关系了,只是我们在上层使用这两个属性的时候可以用name
和age
的点语法来访问getter
和setter
。如果不想使用这两个名字用于底层的存储也可以任意命名,但最好按照官方的命名原则来命名。
也可以自定义getter和setter方法来覆盖编译器默认生成的方法,就如同手动创建getter
和setter
一样。
@interface Person : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
//编译器会帮我们自动生成_name和_age这两个实例变量,下面代码就可以正常使用这两个变量了
@synthesize name = _name;
@synthesize age = _age;
- (void)setName:(NSString*)name {
//必须使用_name来赋值,使用self.name来设置值时编译器会自动转为调用该函数,会导致无限递归
//使用_name则是直接访问底层的存储属性,不会调用该方法来赋值
//这里使用copy是为了防止NSMutableString多态
_name = [name copy];
}
- (NSString*)name {
//必须使用_name来访问属性值,使用self.name来访问值时编译器会自动转为调用该函数,会造成无限递归
return _name;
}
@end
使用自定义的getter和setter一般是用来实现懒加载(lazy load),在很多情况下很常用,比如:创建一个比较大的而又不一定会使用的对象,可以按照如下方法编写。
@property (nonatomic, strong) CustomObject *customObject;
@synthesize customObject = _customObject;
- (CustomObject*) customObject {
if (_customObject == nil) {
//初始化操作,会调用setter方法
self.customObject = [[CustomObject alloc] init];
//如果按照如下方法编写不会调用setter方法,如果自定义setter方法需要完成一些事情建议使用self.customObject的方式来设置
//_customObject = [[CustomObject alloc] init];
}
return _customObject;
}
@property指示符
在声明属性的时候一般会带上几个指示符,常用指示符有
atomic nonatomic
readwrite readonly
assign
strong
weak
copy
unsafe_unretained
retain
还可以设置getter
和setter
对其重命名,这里不再赘述。
atomic/nonatomic
指定合成存取方法是否为原子操作,可以理解为是否线程安全,但在iOS上即时使用atomic
也不一定是线程安全的,要保证线程安全需要使用锁机制,超过本文的讲解范围,可以自行查阅。
可以发现几乎所有代码的属性设置都会使用nonatomic
,这样能够提高访问性能,在iOS中使用锁机制的开销较大,会损耗性能。
readwrite/readonly
readwrite
是编译器的默认选项,表示自动生成getter
和setter
,如果需要getter
和setter
不写即可。
readonly
表示只合成getter
而不合成setter
。
assign、weak、unsafe_unretained
assign
表示对属性只进行简单的赋值操作,不更改所赋的新值的引用计数,也不改变旧值的引用计数,常用于标量类型,如NSInteger
,NSUInteger
,CGFloat
,NSTimeInterval
等。
assign
也可以修饰对象如NSString
等类型对象,上面说过使用assign
修饰不会更改所赋的新值的引用计数,也不改变旧值的引用计数,如果当所赋的新值引用计数为0对象被销毁时属性并不知道,编译器不会将该属性置为nil
,指针仍旧指向之前被销毁的内存,这时访问该属性会产生野指针错误并崩溃,因此使用assign
修饰的类型一定要为标量类型。
@interface Person : NSObject
@property (nonatomic, assign) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
//这里使用NSMutableString而不使用NSString是因为NSString会缓存字符串,后面置空的时候实际没有被销毁
NSMutableString *s = [[NSMutableString alloc] initWithString:@"Jiaming Chen"];
//设置p.name不会增加s的引用计数,只是单纯将s指向的地址赋给p.name
p.name = s;
//输出两个变量的内存地址,可以看出是一致的
NSLog(@"%p %p", p.name, s);
//这里可以正常访问name
NSLog(@"%@ %ld", p.name, p.age);
//将上述字符串置空,引用计数为0,对象被销毁
s = nil;
//查看其地址时仍然可以访问到,表示其仍然指向那一块内存
NSLog(@"%p", p.name);
//访问内容时发生野指针错误,程序崩溃。因为对象已经被销毁
NSLog(@"%@ %ld", p.name, p.age);
}
return 0;
}
使用weak
修饰的时候同样不会增加所赋的新值的引用计数,也不减少旧值的引用计数,但当该值被销毁时,weak
修饰的属性会被自动赋值为nil
,这样就可以避免野指针错误。
使用unsafe_unretained
修饰时效果与assign
相同,不会增加引用计数,当所赋的值被销毁时不会被置为nil
可能会发生野指针错误。unsafe_unretained
与assign
的区别在于,unsafe_unretained
只能修饰对象,不能修饰标量类型,而assign
两者均可修饰。
为了防止多态的影响,对NSString
进行修饰时一般使用copy
。
下文会对weak
、unsafe_unretained
和copy
进行详细介绍。
strong、weak
strong
表示属性对所赋的值持有强引用表示一种“拥有关系”(owning relationship),会先保留新值即增加新值的引用计数,然后再释放旧值即减少旧值的引用计数。只能修饰对象。如果对一些对象需要保持强引用则使用strong
。
weak
表示对所赋的值对象持有弱引用表示一种“非拥有关系”(nonowning relationship),对新值不会增加引用计数,也不会减少旧值的引用计数。所赋的值在引用计数为0被销毁后,weak
修饰的属性会被自动置为nil
能够有效防止野指针错误。
weak
常用在修饰delegate等防止循环引用的场景。
copy
copy
修饰的属性会在内存里拷贝一份对象,两个指针指向不同的内存地址。
一般用来修饰有对应可变类型子类的对象。
如:NSString/NSMutableString
,NSArray/NSMutableArray
,NSDictionary/NSMutableDictionary
等。
为确保这些不可变对象因为可变子类对象影响,需要copy
一份备份,如果不使用copy
修饰,使用strong
或assign
等修饰则会因为多态导致属性值被修改。
这里的copy
还牵扯到NSCopying
和NSMutableCopying
协议,在下文会有简要介绍。
@interface Person : NSObject
//使用strong修饰NSString
@property (nonatomic, strong) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
NSMutableString *s = [[NSMutableString alloc] initWithString:@"Jiaming Chen"];
//将可变字符串赋值给p.name
p.name = s;
//输出的地址和内容均一致
NSLog(@"%p %p %@ %@", p.name, s, p.name, s);
//修改可变字符串s
[s appendString:@" is a good guy"];
//再次输出p.name被影响
NSLog(@"%p %p %@ %@", p.name, s, p.name, s);
}
return 0;
}
copy
还被用来修饰block
,在ARC环境下编译器默认会用copy
修饰, 一般情况下在block
需要捕获外界数据时该block
就会被分配在堆区,但在MRC环境下由于手动管理引用计数,block
一般被分配在栈区,需要copy
到堆区来防止野指针错误。由于牵扯block
相关知识,有兴趣可以看博客另一篇文章iOS block探究(二): 深入理解
对于可变对象类型,如NSMutableString
、NSMutableArray
等则不可以使用copy
修饰,因为Foundation
框架提供的这些类都实现了NSCopying
协议,使用copy
方法返回的都是不可变对象,如果使用copy
修饰符在对可变对象赋值时则会获取一个不可变对象,接下来如果对这个对象进行可变对象的操作则会产生异常,因为OC
没有提供mutableCopy
修饰符,对于可变对象使用strong
修饰符即可。具体栗子如下:
@interface Person : NSObject
//使用copy修饰NSMutableString
@property (nonatomic, copy) NSMutableString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
NSMutableString *s = [[NSMutableString alloc] initWithString:@"Jiaming Chen"];
//将可变字符串赋值给p.name
p.name = s;
//输出的地址不一致,内容一致
NSLog(@"%p %p %@ %@", p.name, s, p.name, s);
//修改p.name,此时抛出异常
[p.name appendString:@" is a good guy."];
}
return 0;
}
上面的栗子使用copy
修饰可变对象,在进行赋值的时候会通过copy
方法获取一个不可变对象,因此p.name
的地址和s
的地址不同,而p.name
运行时类型为NSString
,调用appendString:
方法会抛出异常。
所以,针对不可变对象使用copy
修饰,针对可变对象使用strong
修饰。
unsafe_unretained
使用unsafe_unretained
修饰时效果与assign
相同,不会增加新值的引用计数,也不会减少旧值的引用计数(unretained)当所赋的值被销毁时不会被置为nil
可能会发生野指针错误(unsafe)。unsafe_unretained
与assign
的区别在于,unsafe_unretained
只能修饰对象,不能修饰标量类型,而assign
两者均可修饰。
retain
在ARC环境下使用较少,在MRC下使用效果与strong
一致。
copy的题外话
有时候我们需要copy
一个对象,或是mutableCopy
一个对象,这时需要遵守NSCopying
和NSMutableCopying
协议,来实现copyWithZone:
和mutableCopyWithZone:
两个方法,而不是重写copy
和mutableCopy
两个方法。
Foundation
框架中的很多数据类型已经帮我们实现了上述两个方法,因此我们可以使用copy
方法和mutableCopy
方法来复制一个对象,两者的区别在于copy
的返回值仍未不可变对象,mutableCopy
的返回值为可变对象。
type | copy | mutableCopy |
---|---|---|
NS* | 浅拷贝,只拷贝指针,地址相同 | 单层深拷贝,拷贝内容,地址不同 |
NSMutable* | 单层深拷贝,拷贝内容,地址不同 | 单层深拷贝,拷贝内容,地址不同 |
由上述表格可以看出,对于不可变类型,使用copy
方法时是浅拷贝,只拷贝指针,因为内容是不会变化的。使用mutableCopy
时由于返回可变对象因此需要一份拷贝,供其他对象使用。对于可变类型,不管是copy
还是mutableCopy
均会进行深拷贝,所指向指针不同。
前文介绍copy
修饰符的时候讲过,在修饰NSString
这样的不可变对象的时候使用copy
修饰,但其实当给对象赋一个NSString
时仍旧只复制了指针而不是拷贝内容,原因同上。
@interface Person : NSObject
//使用copy修饰
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
NSString *s = @"Jiaming Chen";
p.name = s;
//p.name的地址与s地址相同,不可变对象copy是浅拷贝
NSLog(@"%p %p %@ %@", p.name, s, p.name, s);
}
return 0;
}
@property进阶:深入理解
由于篇幅有限,本文只用于介绍property基本用法,博客另一篇文章会深入讲解property的实现机制,有兴趣可自行查阅iOS @property探究(二): 深入理解
备注
由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。