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

上述代码就是手动创建变量的gettersetter的实现,gettersetter本质就是符合一定命名规范(前文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;
}

通过调用方式可以看出,settergetter本质就是实例方法,可以通过函数调用的方式来使用。
为了方便使用,Objective-C允许使用点语法来访问gettersetter

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;
}

使用点语法访问的方式本质还是调用了我们手动创建的settergetter
当有很多变量需要设置时,这样手工创建settergetter的方式难免很繁琐,因此合成存取方法就诞生了。

合成存取方法

@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框架的数据类型,如整形使用NSIntegerNSUInteger表示,时间间隔的浮点类型使用NSTimeInterval表示,这样代码数据类型更统一。

上面的代码使用@property声明两个属性nameage并为其设置了一些指示符(nonatomic,copy,assign等,下文会详细介绍)。
@synthesize表示为这两个属性自动生成名为_name_age的底层实例变量,并自动生成相关的gettersetter也可以不写编译器默认会自动生成'_属性名'的实例变量以及相关的gettersetter
这里所说的编译器自动生成的实例变量就如同我们在上文中手动创建settergetter时声明的变量_name_age。也就是说编译器会在编译时会自动生成并使用_name_age这两个变量来存储这两个属性,跟nameage没什么关系了,只是我们在上层使用这两个属性的时候可以用nameage的点语法来访问gettersetter。如果不想使用这两个名字用于底层的存储也可以任意命名,但最好按照官方的命名原则来命名。

也可以自定义getter和setter方法来覆盖编译器默认生成的方法,就如同手动创建gettersetter一样。

@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

还可以设置gettersetter对其重命名,这里不再赘述。

atomic/nonatomic

指定合成存取方法是否为原子操作,可以理解为是否线程安全,但在iOS上即时使用atomic也不一定是线程安全的,要保证线程安全需要使用锁机制,超过本文的讲解范围,可以自行查阅。
可以发现几乎所有代码的属性设置都会使用nonatomic,这样能够提高访问性能,在iOS中使用锁机制的开销较大,会损耗性能。

readwrite/readonly

readwrite是编译器的默认选项,表示自动生成gettersetter,如果需要gettersetter不写即可。
readonly表示只合成getter而不合成setter

assign、weak、unsafe_unretained

assign表示对属性只进行简单的赋值操作,不更改所赋的新值的引用计数,也不改变旧值的引用计数,常用于标量类型,如NSIntegerNSUIntegerCGFloatNSTimeInterval等。
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_unretainedassign的区别在于,unsafe_unretained只能修饰对象,不能修饰标量类型,而assign两者均可修饰。

为了防止多态的影响,对NSString进行修饰时一般使用copy

下文会对weakunsafe_unretainedcopy进行详细介绍。

strong、weak

strong表示属性对所赋的值持有强引用表示一种“拥有关系”(owning relationship),会先保留新值即增加新值的引用计数,然后再释放旧值即减少旧值的引用计数。只能修饰对象。如果对一些对象需要保持强引用则使用strong

weak表示对所赋的值对象持有弱引用表示一种“非拥有关系”(nonowning relationship),对新值不会增加引用计数,也不会减少旧值的引用计数。所赋的值在引用计数为0被销毁后,weak修饰的属性会被自动置为nil能够有效防止野指针错误。
weak常用在修饰delegate等防止循环引用的场景。

copy

copy修饰的属性会在内存里拷贝一份对象,两个指针指向不同的内存地址。
一般用来修饰有对应可变类型子类的对象。
如:NSString/NSMutableString,NSArray/NSMutableArray,NSDictionary/NSMutableDictionary等。
为确保这些不可变对象因为可变子类对象影响,需要copy一份备份,如果不使用copy修饰,使用strongassign等修饰则会因为多态导致属性值被修改。
这里的copy还牵扯到NSCopyingNSMutableCopying协议,在下文会有简要介绍。

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

对于可变对象类型,如NSMutableStringNSMutableArray等则不可以使用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_unretainedassign的区别在于,unsafe_unretained只能修饰对象,不能修饰标量类型,而assign两者均可修饰。

retain

在ARC环境下使用较少,在MRC下使用效果与strong一致。

copy的题外话

有时候我们需要copy一个对象,或是mutableCopy一个对象,这时需要遵守NSCopyingNSMutableCopying协议,来实现copyWithZone:mutableCopyWithZone:两个方法,而不是重写copymutableCopy两个方法。
Foundation框架中的很多数据类型已经帮我们实现了上述两个方法,因此我们可以使用copy方法和mutableCopy方法来复制一个对象,两者的区别在于copy的返回值仍未不可变对象,mutableCopy的返回值为可变对象。

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

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。

    原文作者:WWWWDotPNG
    原文地址: https://www.jianshu.com/p/646ae400fe7b
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞