Objective-C的@property的详细解读

文章目录
  1. @property的前世今生
  2. @property与@synthesize
  3. @property与取值、设值方法
  4. @property的特质
    1. 原子性
    2. 可读、可写权限
    3. 内存管理方式
    4. setter和getter方法名
    5. 特别注意
  5. @property与KVO

@property在Objective-C中,我们称为属性。它与实例变量有密不可分的关系,从而会涉及到内存管理、KVO等诸多细节问题。

看了很多代码,不少人对使用@property的使用习惯和认识也有一些不合理的地方,所以写这篇文章来谈谈@property的正确使用姿势。

鄙人水平有限,若文章内容存在错误,欢迎您在评论中指正,我会感激不尽。

@property的前世今生

在Java、C++中我们一般都会为实例变量添加访问权限声明@public@private@protect,但在Objective-C中,我们通常不会为实例变量添加权限声明的。由于Objective-C是一门动态的语言,允许开发者随时(甚至在运行时)向应用添加实例变量,所以在Objective-C中,没有采用这种方法。

面向对象思想有一个非常重要的性质,封装性。上述的权限声明正是为了保障封装性而产生的。即对外只公开需要公开的属性和方法,内部隐藏起实现细节和并没有必要对外公开的属性和方法。而为了这个封装性,通常情况下我们还会定义为此定义settergetter方法。

@property是一条编译器指令,用于编译器为实例变量自动生成settergetter方法。通常在Objective-C中,我们会在头文件中将需要暴露出来的实例变量的属性@property给出,不希望暴露的,可将该实例变量的属性@property写在类的实现文件的扩展(extension)中。

举个栗子:

这是.h头文件的内容,希望暴露出的属性写在这里,其他类可以通过.语法来访问namebirthday两个属性。

1
2
3
4
5
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end

对于不希望暴露给外界的属性,写在实现文件中,以extension的形式出现。.m实现文件内容如下:

1
2
3
4
5
6
7
8
9
@interface Student ()
@property (nonatomic, copy) NSString *girlFriendName;
@end
@implementation Student
@end

这样,我们可以在所有import了.h文件的地方使用name这个字符串,权限类似于C++中的Public;girlFriendName只能在Student类的内部使用,即使是Student的子类也不能使用,权限类似于C++中的Private。

@property与@synthesize

在有些教程中,我们会发现有提到@property@synthesize要配对使用。然后我们尝试不写@synthesize,于是发现仍能正常执行,甚至正常通过“.”点语法(语法糖)和取值、设值方法来访问实例变量。那么问题来了,@synthesize的意义到底是什么呢?

查了一下资料,在Xcode 4.0版本之前,确实是不写@synthesize不行,后来版本中的LLVM优化了这个语法,只写@property不写@synthesize也可以自动合成settergetter方法。自动合成的属性所对应的实例变量名就是属性名在前面加“_”,如果我们不想使用这个实例变量名,就可以使用@synthesize来指定实例变量名字。如@synthesize content = textContent;。但实例变量中使用下划线作为开头,已经成了广大开发者约定俗成的习惯,所以如果没有特殊需求,建议还是遵循规范,不需要写@synthesize,直接声明属性@property

@property与取值、设值方法

这一块直接举个例子,一目了然地说明:

1
2
3
4
5
6
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *birthday;
@end

这样一段属性声明,等同于自动合成的结果:

1
2
3
4
5
6
7
8
@interface Student : NSObject
- (NSString *)name;
- (NSDate *)birthday;
- (void)setName:(NSString *)name;
- (void)setBirthday:(NSDate *)birthday;
@end

关于点语法,其实只是Objective-C提供的语法糖。我们在使用点语法访问属性的时候,其实是调用了取值方法“getter”。

@property的特质

我们在前面的例子里,@property后跟了一个括号,里面写了几个关键字,这几个关键字正是@property的特质。特质规定了自动合成的“setter”与“getter”的具体细节。特质具有四类:原子性、可读可写、内存管理、取设值方法名。

原子性

在操作系统的学习中我们了解到,一个操作具有原子性表明这个操作不可分割,即最基本的操作。这关乎到线程是否安全的问题:多个线程可能同时访问一个属性。如果一个属性不具备原子性,就不会使用同步锁。在iOS开发时,我们极大多数情况都用nonatomic非原子性,而在OS X开发时,会使用到atomic原子性。这样做的原因是在iOS中使用同步锁,性能开销较大,在极少数需要加锁保护线程安全的时候,我们还需要更底层的锁机制。

可读、可写权限

这个特质有两个选项,readwritereadonly,分别对应读写权限和只读权限。只读权限编译器并不会合成“setter”。默认情况,该特质为readwrite

内存管理方式

setter方法中赋值的方式,决定了内存管理中引用计数的变化。

这个特质有非常多的选项,我们依次说明。

  • assign:只对简单数据类型进行赋值操作。简单数据类型包括NSIntegerCGFloat等。
  • weak:不对该属性具备拥有关系。此时的setter方法并不会改变对象的引用计数,如果该对象的拥有者全都release了,那么该对象会彻底释放,该weak指针会被置为空nil
  • strong:对该属性具备拥有关系。此时的setter方法会retain新的对象,release旧的对象。如果旧的对象之前引用计数为1,那么在这次重新赋值时就会被彻底释放。
  • copy:在setter中,会把该对象复制一份并赋给该属性。我们说复制有两种,『浅拷贝』和『深拷贝』,『浅拷贝』是指对指针的复制,对象本身的内存区域没有变化,新指针指向的还是原有内存区域;『深拷贝』是指对内存区域存储的数据进行的复制,指针会指向新的内存区域。特别注意的是,如果是对可变对象的使用copy特质不会改变引用计数,为深拷贝;如果是不可变对象使用copy特质,就是浅拷贝,引用计数++,此时和strong完全相同。通常情况下,我们对可变对象使用copy特质。当类型为NSString时,我们也应该对其加以copy特质,因为setter中的新值可能是一个NSMutableString
  • unsafe_unretained:不对该属性具备拥有关系,但和weak的区别在于指针所指向的对象被彻底释放时,该属性的指针不会被置为空nil
  • retain:这是以前在MRC内存管理方式下的属性,在setter方法中对新值retain。ARC中已经弃用!

关于可变对象要使用copy特质:为了保证封装性,setter方法是属性的设值方法,但可变对象可能会在不经过setter方法的情况下,改变自身的值,这样破坏了原有的封装性。所以要对可变对象做copy操作,深拷贝出一份不可变的对象。

setter和getter方法名

可以通过这个特质来指定取值、设值方法的方法名。

用法直接举例:

1
@property (nonatomic, assign, getter = isOpen) BOOL open;

setter设值方法用法和上面例子用法相同,只是不太常用。

特别注意

这些特质都指明了自动合成取设值方法的诸多细节,如果我们开发者想自己实现取设值方法,必须要遵守@property中声明的特质,否则会造成误会,甚至奇怪的bug。

@property与KVO

KVO全称是Key-Value Observing,是Objective-C语言中的一种核心机制,我们一般翻译为键值观察。至于KVO的实现方式,相信很多人也知道,就是继承该类并重写setter方法,当调用setter方法的时候,通知监听器回调。前文说到了,@property可以自动合成setter,所以这里正是它和KVO之间千丝万缕的关系。

我们访问变量有几种方式,遵循Objective-C调用方法的方式访问[self variableName]是一种,self.variableName语法糖是一种,直接访问实例变量_variableName也是一种,这三种方式有什么区别呢?

  • 方法调用[self variableName]

    如前文所讲,这种方式正是使用了getter方法,通过getter返回值的来访问到实例变量的值,遵循良好的封装性。

  • 点语法糖self.variableName

    正如它的名字一样,这种方式其实只是一种语法糖,其背后根本上还是调用getter方法。

  • 直接访问实例变量_variableName不经过取设方法,直接访问实例变量所在内存位置。优点在于不经过方法调用,速度更快。

同样看一下设值的方式呢,也有如下三种:

  • 方法调用[self setVariableName]

    使用setter来设值。

  • 点语法糖self.variableName = @"talisk"

    语法糖,背后同样是setter方法。

  • 直接访问实例变量_variableName = @"talisk"

    不经过设值方法,将新值赋给实例变量所在内存区域。优点还是不经过方法调用,速度更快。然而问题也很明显,用这种方式改变值KVO失效,跳过了setter中内存管理的特质。比如你给某个字符串赋了mutable可变字符串,若该属性内存管理特质为copy,此时通过setter设值会深拷贝一份不可变字符串,但直接访问该实例变量不通过setter会导致没有拷贝出一份不可变字符串。

对于这样的问题,我们大概可以总结出最佳使用姿势

在需要设值的地方,我们通过setter设值方法设值;当需要取值的时候,我们采用直接访问实例变量的方式。

另外要注意,初始化方法中,没有特殊需要,应该尽量直接访问实例变量。