来自我的个人博客Minecode.link
在使用Objective-C时,频繁用到属性关键字。我们应该理解每种属性的意义,并了解一些偏底层的实现,故在此对OC的属性关键字做个浅析。
基础概念:ivar、getter、setter
在C语言中,我们通常是直接操作成员变量。而在Objective-C中,使用了“属性”这一概念来封装对象中的数据,OC对象会把需要的数据保存为各种实例变量,同时通过“存取方法”(Access Method)来进行访问,也就是常说的getter和setter。
所以,ivar是对象的各种实例变量,getter用于获取变量的值,setter用于写入变量的值。
我们来看一个标准的ivar+getter+setter
样板代码:
@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的关系
。
@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
类型的,定义如下:
typedef struct objc_property *objc_property_t;
struct property_t {
const char *name;
const char *attributes;
};
property结构体有name和attributes两个成员变量,而attributes则是property的属性定义,我们看一下它的定义:
/// 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;
我们可以通过以下方法获取对应变量:
// 获取所有属性列表
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类型定义为下:
typedef struct objc_object {
Class isa;
} *id;
而id类型就是指向Class类型的指针,那么Class又是什么呢?
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_list
和objc_method_list
:
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
定义如下:
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做了以下事情:
- 创建该属性,设置其objc_ivar,通过偏移量和内存占用就可以方便获取。
- 生成其getter和setter。详情请查阅objc中方法的实现(SEL,IMP)。
- 将属性的ivar添加到类的ivar_list中,作为类的成员变量存在。
- 将getter和setter加入类的method_list中。之后可以通过直接调用或者点语法来使用。
- 将属性的描述添加到类的属性描述列表中。
属性的获取
为了记录属性,有以下几个变量:
ivar_list
: 记录成员变量的描述
method_list
: 记录该变量getter和setter的描述
prop_list
: 记录属性的描述
OBJC_IVAR_$类名_$属性名称
: 记录属性相对对象地址的偏移地址(重要)
其中,记录变量的偏移地址很重要。我们来看一下实现:
// 生成一个SubClass类型,包含一个属性
@interface SubClass ()
@property (nonatomic, strong) NSMutableArray* array;
@end
// 在该类的实现中创建一个方法
@implementation SubClass
- (void)testArrayMethod {
self.array = [NSMutableArray array];
}
@end
反编译代码,我们查看一下它是如何赋值的:
// 属性的定义
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_unretained
,retain
对应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方法,所以只能生成不可变对象。对于如下代码:
@property (nonatomic, copy) NSMutableArray *mutableArray;
/* ... */
NSMutableArray *anotherMutableArray = [NSMutableArray arrayWithObjects:@1,@2,nil];
self.mutableArray = anotherMutableArray;
[self.mutableArray removeObjectAtIndex:0];
会发生崩溃。原因在于copy生成了不可变对象,导致removeObjectAtIndex:方法报错。
所以,对于可变对象,不要使用copy属性修饰符,而是调用mutableCopy方法。
网友评论