探究Objective-C属性关键字

来自我的个人博客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_listobjc_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做了以下事情:

  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_$类名_$属性名称: 记录属性相对对象地址的偏移地址(重要)

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

// 生成一个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_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方法,所以只能生成不可变对象。对于如下代码:

@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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容