探究Objective-C属性关键字

在使用Objective-C时,频繁用到属性关键字。我们应该理解每种属性的意义,并了解一些偏底层的实现,故在此对OC的属性关键字做个浅析。

基础概念:ivar、getter、setter

在C语言中,我们通常是直接操作成员变量。而在Objective-C中,使用了“属性”这一概念来封装对象中的数据,OC对象会把需要的数据保存为各种实例变量,同时通过“存取方法”(Access Method)来进行访问,也就是常说的getter和setter。

所以,ivar是对象的各种实例变量,getter用于获取变量的值,setter用于写入变量的值。

我们来看一个标准的ivar+getter+setter样板代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface Person: NSObject
{
// ivar声明
@private
NSString *_myName;
}
// getter方法
- (NSString *)myName {
return _myName;
}
// setter方法
- (void)setMyName:(NSString *)newName {
_myName = newName;
}

可以看到,这样的组合方式造成了代码的臃肿,大大降低了开发效率和可读性,实际开发中使用ivar+getter+setter的情况并不常见,这就要引入@synthesize@property这个关键字。


@synthesize

这个属性已经很少见到了,它是属于MRC和32bit时代的产物。@synthesize属性用来合成一个属性,变量名如果没有显式声明则默认添加一个下划线的前缀(_变量名)。当然也可以手动声明变量名并建立与@property的关系。

为了加深理解,我们看一下以下代码,它的逻辑为:手动声明ivar,使用property声明存取方法,使用@synthesize建立ivar和property的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface SubClass ()
{
// 声明ivar
NSString *_myName;
}
// 声明属性(并合成getter+setter)
@property (nonatomic, copy) NSString* myName;
@end

@implementation SubClass
// 建立myName属性与_myName成员变量的关系
@synthesize myName = _myName;
@end

可以看出@synthesize和@property各自负责的工作,虽然这些工作已经由编译器帮我们做了,但是理解这一概念还是很重要的。

现在我们知道了省略@synthesize声明实际上是因为LLVM的Clang为在ARC模式下会自动生成@synthesize声明,但是这仅限于64位OC运行时中,当使用32位系统时,我们必须要手动声明,否则会报错。我们可以设置NS_BUILD_32_LIKE_64宏来解决这个问题。


@dynamic

相对于@synthesize,@dynamic告诉编译器该属性的getter和setter由程序员自行实现,编译器不再自动生成。在运行时执行过程中如果找不到对应存取方法,则会报错。这便是Runtime中的动态绑定。

同时,使用了@dynamic修饰则必须动态生成方法实现,没有@dynamic myName = _myName;的语法,也就是说我们没有办法静态的建立getter/setter并访问下划线前缀的ivar。对应的解决方法是消息转发和动态方法解析,本文不过多讨论。


@property

本质上来说,@property实际上是告知编译器为你的ivar生成getter和setter,并不生成ivar,要理解这一点。但是由于@synthesize无须再手动声明,所以我们使用@property后实际上是声明了ivar+getter+setter的标准模板。

Runtime下的定义

我们首先反编译为cpp代码,有关反编译的内容请见Objective-C开发中Clang的使用

可以发现property在OC运行时中是objc_property_t类型的,定义如下:

1
2
3
4
5
6
typedef struct objc_property *objc_property_t;

struct property_t {
const char *name;
const char *attributes;
};

property结构体有name和attributes两个成员变量,而attributes则是property的属性定义,我们看一下它的定义:

1
2
3
4
5
/// Defines a property attribute
typedef struct {
const char *name; /**< The name of the attribute */
const char *value; /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;

我们可以通过以下方法获取对应变量:

1
2
3
4
5
6
7
8
// 获取所有属性列表
class_copyPropertyList
// 获取属性名
property_getName
// 获取属性描述字符串
property_getAttributes
// 获取所有属性列表
property_copyAttributeList

可以看到,每一个attribute对应一种属性修饰符,property所定义的属性就包含其中。对应关系如下

属性修饰符类型 name value
属性类型 T 属性类型名
内存管理 C(copy) &(strong/retain) W(weak) R(readonly)
自定义getter/setter G(getter) S(setter) 方法名
原子/非原子类型 N(nonatomic) 空(atomic)
ivar名称 V 变量名称

比如我们分别定义一个对象类型、标量、以及id类型的属性来看一下

属性定义 attributes描述
@property char charDefault; Tc,V_charDefault
@property (nonatomic, copy) NSString *myString; T@”NSString”,C,N,V_myString
@property(nonatomic, readonly, retain) id idVar; T@,R,&,V_idVar

注意:注意描述符中的V_ivar名称,此描述符是基于64bit系统的,因为会自动合成ivar,如果是32bit系统则不会有下划线,前文已做解释。

Runtime下的实现

了解了属性在运行时系统下的定义,我们现在探究一下其的实现。
运行时中有ivar、method、class、object等概念,其中@property就涉及到了ivar和method(get方法和set方法),具体如何实现呢,我们通过反编译来一探究竟。

在OC中,所有对象都可以认为是id类型,id类型定义为下:

1
2
3
typedef struct objc_object {
Class isa;
} *id;

而id类型就是指向Class类型的指针,那么Class又是什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

现在我们大致了解了OC中对象的实现原理。OC中所有对象都可以认为是id类型,而id又是指向Class的指针,Class类型实际是objc_class结构体,其定义了OC对象的基本信息。

更多Runtime的内容在此不再赘述,我们来看一下属性涉及到的类型:objc_ivar_listobjc_method_list

1
2
3
4
5
6
7
8
9
10
11
struct objc_ivar {
char *ivar_name;
char *ivar_type;
int ivar_offset;
int space;
};
struct objc_ivar_list {
int ivar_count;
int space;
struct objc_ivar ivar_list[1];
}

在此我们看到了ivar的真面目,它包含了名称、类型、基地址偏移、内存空间。
同样,objc_method_list定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct objc_method method_list[1];
};
struct objc_method {
SEL method_name;
char *method_types; /* a string representing argument/return types */
IMP method_imp;
};

所以,当在类中创建一个属性时。Runtime做了以下事情:

  1. 创建该属性,设置其objc_ivar,通过偏移量和内存占用就可以方便获取。
  2. 生成其getter和setter。详情请查阅objc中方法的实现(SEL,IMP)。
  3. 将属性的ivar添加到类的ivar_list中,作为类的成员变量存在。
  4. 将getter和setter加入类的method_list中。之后可以通过直接调用或者点语法来使用。
  5. 将属性的描述添加到类的属性描述列表中。

属性的获取

为了记录属性,有以下几个变量:
ivar_list: 记录成员变量的描述
method_list: 记录该变量getter和setter的描述
prop_list: 记录属性的描述
OBJC_IVAR_$类名_$属性名称: 记录属性相对对象地址的偏移地址(重要)

其中,记录变量的偏移地址很重要。我们来看一下实现:

1
2
3
4
5
6
7
8
9
10
// 生成一个SubClass类型,包含一个属性
@interface SubClass ()
@property (nonatomic, strong) NSMutableArray* array;
@end
// 在该类的实现中创建一个方法
@implementation SubClass
- (void)testArrayMethod {
self.array = [NSMutableArray array];
}
@end

反编译代码,我们查看一下它是如何赋值的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 属性的定义
extern "C" unsigned long OBJC_IVAR_$_SubClass$_array;
struct SubClass_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSMutableArray *_array;
};
// 赋值(已经去掉了复杂的类型转换代码)
static void _I_SubClass_testArrayMethod(SubClass * self, SEL _cmd) {
(objc_msgSend)(self, sel_registerName("setArray:"), (objc_getClass("NSMutableArray"), sel_registerName("array")));
}
// 属性的setter方法
static void _I_SubClass_setArray_(SubClass * self, SEL _cmd, NSMutableArray *array) {
*(self + OBJC_IVAR_$_SubClass$_array) = array;
}

我们可以看到,属性的偏移地址命名为OBJC_IVAR_$类名_$属性名称,点语法本质上是调用了setter,而setter中确定属性对应ivar的内存地址则是通过 对象地址+偏移量 来寻址,即*(self + OBJC_IVAR_$_SubClass$_array)


@Property的属性修饰符

谈完@property的底层实现,再看一下属性修饰符。此处仅讨论@property的属性修饰符,对于ARC的所有权修饰符(__strong,__weak,__unsafe_unretained,__autorealesing)会专门写一篇文章讨论。

属性符作用及区别

属性 内容
readwrite 属性可读可写,生成getter+setter,默认属性
readonly 属性只读,只生成getter
nonatomic 非原子属性,提高性能但线程不安全
atomic 原子属性,线程安全但可能降低性能
MRC模式下
assign 直接赋值,不增加引用计数
retain 持有对象,引用计数+1
copy 生成并持有一个新对象,并深拷贝对象的值
ARC模式下
strong 强引用,持有对象,引用计数+1,相当于MRC的retain
weak 弱引用,不持有对象,不增加引用计数,相当于MRC的assign,但在对象销毁后会置为nil
copy 深拷贝,同MRC的copy
unsafe_unretained 无须内存管理的对象,相当于MRC的assign,对象销毁后不会置nil,可能造成野指针。(iOS 4之后基本废弃,使用assign替代)

同时,根据LLVM文档所述,ARC模式下依旧可以使用MRC修饰符,编译器会自动转换。assign对应unsafe_unretainedretain对应strong

原子属性atomic

原子属性(atomic)通过加锁来实现访问/赋值的线程安全,但atomic只是保证了getter和setter的线程安全,并没有保证整个对象是线程安全的。比如线程A在读数据,而线程BCD在写数据,虽然BCD并不能同时写,但A读到的数据却是BCD某个时间写入的,无法保证线程安全。同样的,对于objectAtIndex:等非getter/setter方法,则不是线程安全的。

weak的使用场景及与assign的区别

首先,weak与assign都表示了一种“非持有关系”(nonowning relationship),也成弱引用,在使用时不会增加被引用变量的引用计数。而weak在引用的对象被销毁后会被指向nil,保证了安全,相反assign不会被置nil,成为野指针。
其次,对于标量(基础数据类型:int,double,以及OC中使用宏定义的数据类型:CGFloat,NSInteger),只能使用assign。weak只能用于对象,assign可用于对象和标量

copy的使用场景及注意事项

使用copy修饰的对象在赋值的时候创建对象的副本,也成深拷贝。实际则是调用了copy方法。支持copy方法要遵守NSCopying协议,实现copyWithZone:方法来生成并持有对象的副本。同时,还有mutableCopy用于实现对于可变对象的深拷贝,如NSMutableArray。
当我们想复制字符串的值而非直接引用该字符串时,我们就应该深拷贝一份,否则会出现修改原对象值的情况。NSArray、NSDictionary,以及我们自己的类同理。
但是,对于@property的copy修饰符,只是调用了copy方法,所以只能生成不可变对象。对于如下代码:

1
2
3
4
5
@property (nonatomic, copy) NSMutableArray *mutableArray;
/* ... */
NSMutableArray *anotherMutableArray = [NSMutableArray arrayWithObjects:@1,@2,nil];
self.mutableArray = anotherMutableArray;
[self.mutableArray removeObjectAtIndex:0];

会发生崩溃。原因在于copy生成了不可变对象,导致removeObjectAtIndex:方法报错。
所以,对于可变对象,不要使用copy属性修饰符,而是调用mutableCopy方法

相关资料

  1. Objective-C Runtime Programming Guide - Declared Properties