Objective-C的@property的详细解读
@property在Objective-C中,我们称为属性。它与实例变量有密不可分的关系,从而会涉及到内存管理、KVO等诸多细节问题。
看了很多代码,不少人对使用@property
的使用习惯和认识也有一些不合理的地方,所以写这篇文章来谈谈@property
的正确使用姿势。
鄙人水平有限,若文章内容存在错误,欢迎您在评论中指正,我会感激不尽。
@property的前世今生
在Java、C++中我们一般都会为实例变量添加访问权限声明@public
、@private
和@protect
,但在Objective-C中,我们通常不会为实例变量添加权限声明的。由于Objective-C是一门动态的语言,允许开发者随时(甚至在运行时)向应用添加实例变量,所以在Objective-C中,没有采用这种方法。
面向对象思想有一个非常重要的性质,封装性。上述的权限声明正是为了保障封装性而产生的。即对外只公开需要公开的属性和方法,内部隐藏起实现细节和并没有必要对外公开的属性和方法。而为了这个封装性,通常情况下我们还会定义为此定义setter
和getter
方法。
@property是一条编译器指令,用于编译器为实例变量自动生成setter
和getter
方法。通常在Objective-C中,我们会在头文件中将需要暴露出来的实例变量的属性@property
给出,不希望暴露的,可将该实例变量的属性@property
写在类的实现文件的扩展(extension)中。
举个栗子:
这是.h头文件的内容,希望暴露出的属性写在这里,其他类可以通过.语法来访问name
和birthday
两个属性。
|
|
对于不希望暴露给外界的属性,写在实现文件中,以extension的形式出现。.m实现文件内容如下:
|
|
这样,我们可以在所有import了.h文件的地方使用name
这个字符串,权限类似于C++中的Public;girlFriendName
只能在Student
类的内部使用,即使是Student
的子类也不能使用,权限类似于C++中的Private。
@property与@synthesize
在有些教程中,我们会发现有提到@property
与@synthesize
要配对使用。然后我们尝试不写@synthesize
,于是发现仍能正常执行,甚至正常通过“.”点语法(语法糖)和取值、设值方法来访问实例变量。那么问题来了,@synthesize
的意义到底是什么呢?
查了一下资料,在Xcode 4.0版本之前,确实是不写@synthesize
不行,后来版本中的LLVM优化了这个语法,只写@property
不写@synthesize
也可以自动合成setter
和getter
方法。自动合成的属性所对应的实例变量名就是属性名在前面加“_”,如果我们不想使用这个实例变量名,就可以使用@synthesize
来指定实例变量名字。如@synthesize content = textContent;
。但实例变量中使用下划线作为开头,已经成了广大开发者约定俗成的习惯,所以如果没有特殊需求,建议还是遵循规范,不需要写@synthesize
,直接声明属性@property
。
@property与取值、设值方法
这一块直接举个例子,一目了然地说明:
|
|
这样一段属性声明,等同于自动合成的结果:
|
|
关于点语法,其实只是Objective-C提供的语法糖。我们在使用点语法访问属性的时候,其实是调用了取值方法“getter”。
@property的特质
我们在前面的例子里,@property
后跟了一个括号,里面写了几个关键字,这几个关键字正是@property
的特质。特质规定了自动合成的“setter”与“getter”的具体细节。特质具有四类:原子性、可读可写、内存管理、取设值方法名。
原子性
在操作系统的学习中我们了解到,一个操作具有原子性表明这个操作不可分割,即最基本的操作。这关乎到线程是否安全的问题:多个线程可能同时访问一个属性。如果一个属性不具备原子性,就不会使用同步锁。在iOS开发时,我们极大多数情况都用nonatomic
非原子性,而在OS X开发时,会使用到atomic
原子性。这样做的原因是在iOS中使用同步锁,性能开销较大,在极少数需要加锁保护线程安全的时候,我们还需要更底层的锁机制。
可读、可写权限
这个特质有两个选项,readwrite
和readonly
,分别对应读写权限和只读权限。只读权限编译器并不会合成“setter”。默认情况,该特质为readwrite
。
内存管理方式
setter方法中赋值的方式,决定了内存管理中引用计数的变化。
这个特质有非常多的选项,我们依次说明。
assign
:只对简单数据类型进行赋值操作。简单数据类型包括NSInteger
、CGFloat
等。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方法名
可以通过这个特质来指定取值、设值方法的方法名。
用法直接举例:
|
|
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设值方法设值;当需要取值的时候,我们采用直接访问实例变量的方式。
另外要注意,初始化方法中,没有特殊需要,应该尽量直接访问实例变量。