您的位置:首页 > 移动开发 > IOS开发

iOS @property探究(一): 基础详解

2017-03-21 16:12 302 查看

你要知道的@property都在这里

**转载请注明出处 http://blog.csdn.net/u014205968/article/details/64443443

**

本文大纲

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
(vs
copy
),
weak
,
strong
,
atomic
(vs
nonatomic
),
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
的返回值为可变对象。

copymutableCopy
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探究(二): 深入理解

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  objective-c