前言
不论是初入iOS开发还是已经是老江湖的开发者,想必@property
已经成为了我们最熟悉的一个语法。
“属性” (property) 作为 Objc 的一项特性,主要作用就在于封装对象中的数据。Objc 对象通常会把所需要的数据保存为各种实例变量。
实例变量一般通过”存取方法” (access method) 访问。
获取方法 (getter) 用于读取变量值
设置方法 (setter) 用于写入变量值
在正规的 Objc 编码风格中,存取方法有这严格的命名规范。
正是因为这样的命名规范,所以 Objc 这门语言才能根据名称自动创建出存取方法。
关于@property
想必大家应该都知道或者了解,@property
语句相当于系统自动为我们生成了getter
和setter
方法。
想必大家还应该知道,我们常常在调用实例变量的时候,会出现一个前面带下划线的变量,这个变量我们也从未去特意声明过。辣么这个“下划线变量”到底是哪里来的?
没错,就是棒棒的@property
带来的。
所以我们可以说@property = ivar(实例变量) + getter + setter
。
比如下面这个经典的例子
1 | @interface Person : NSObject |
上面的写法等价于:
1 | @interface Person : NSObject |
@property属性关键字
属性可拥有的特质分为四类:
原子性 ——
nonatomic
在默认情况下,由编译器合成的方法会通过锁定机制确保原子性(atomicity)。如果属性具备
nonatomic
特质,则不使用自旋锁。请注意,尽管没有名为”atomic”的特质(如果某属性不是nonatomic
,那他就是原子的atomic
)。读写权限 ——
readwrite(读写)
、readonly(只读)
内存管理 ——
assign
、strong
、weak
、unsafe_unretained
、copy
方法名 ——
getter = <name>
、setter = <name>
getter = <name>
的样式:1
@property (nonatomic, getter = isOn) BOOL on;
setter = <name>
一般用在特殊环境下,比如:在数据反序列化、转模型的过程中,服务器返回的字段以
init
开头,所以你需要定义一个init
开头的属性,但默认生成的getter
和setter
方法也会以init
开头。但是编译器会把init
开头的方法当成初始化方法,而初始化方法只能返回self,所以编译器会报错。这时候我们就要用到
setter = <name>
来防止编译器报错1
2
3
4
5@property(nonatomic, strong, getter=p_initBy, setter=setP_initBy:)NSString *initBy;
//对关键字特殊说明
@property(nonatomic, readwrite, copy, null_resettable) NSString *initBy;
- (NSString *)initBy __attribute__((objc_method_family(none)));不常用的
nonnull
、null_resettable
、nullable
ivar
说到 ivar,就要涉及到内存管理的知识了
还是那个例子
1 | @interface Person : NSObject |
这个 Person 类被编译之后变成一个描述 (arm64),Person 占用24个字节,前8个字节是 isa 指针,中间八个字节是 NSString 指针,后八个字节是 NSInteger 的值
1 | | isa | NSString * _firstName | NSInteger _age | |
调用[[Person alloc]init]
的时候,会分配出24个字节的内存出来:
1 | | 0 | 0 | 0 | |
然后往前8个字节放isa地址
1 | //alloc完成后 |
然后调用alloc出来的 init 方法,把 _age 的值赋为1,因为 _firstName 没有初始化,所以还是0。
1 | //init之后 |
isa
指向了这个类的元类,也就是 meta,meta 里存储了父类/ ivar 结构/方法等内容,关于这个,在另一篇文章中可以看到。
getter与setter
前言说了
1 | Objc对象通常会把所需要的数据保存为各种实例变量。 |
这个观念,几乎都深深的刻在所有程序猿的脑海中。
所以 iOS 开发者在使用@property
带来的便利的同时,不能忘记这个重要的想法。
我们所用到的person.firstName = @"Reus"
其实是一个语法糖,等同于[person setFirstName:@"Reus"]
。
在过去我们需要声明对应的实例变量@synthesize person = _person
。
现在,一句@property
已经可以做到以上所有的步骤了,并且善用@property
对于内存管理来说,也是一件好事。
property 的那些事
property 在 runtime 中是objc_property_t
,定义如下:
1 | typedef struct objc_property *objc_property_t; |
而objc_property
是一个结构体,包含了name
和attributes
。
1 | struct property_t { |
attributes 本质是objc_property_attribute_t
,定义了 property 的一些属性。
1 | typedef struct{ |
attributes 的具体内容大概包括类型
,原子性
,内存语义
,对应的实例变量
。
我们定义一个string的 property @property (nonatomic, copy) NSString *string;
,通过property_getAttributes(property)
获取到attributes并打印,结果为T@"NSString",C,N,V_string
T 代表类型,C 代表 Copy,N 代表 nonatomic,V 代表实例变量。
自动合成
完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫“自动合成” (autosynthesis)。这个过程是在编译的时候由编译器执行。除了生成 getter、setter 之外,编译器还要向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。
可通过@synthesize
语法来指定实例变量的名字。
1 | @implementation Person |
微博@iOS程序犭袁曾反编译过相关代码,他大致生成了五个东西
OBJC_IVAR_$类名$属性名称
:该属性的“偏移量”(offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。- setter 与 getter 方法对应的实现函数
ivar_list
:成员变量列表method_list
:方法列表prop_list
:属性列表
我们每次增加一个属性,系统都会在ivar_list
中添加一个成员变量的描述,在method_list
中增加 setter 与 getter 方法的描述,在prop_list
中增加一个属性的描述,再计算偏移量,给出 setter 与 getter 方法对应的实现。在 setter 方法中从偏移量位置开始赋值,在 getter 方法中从偏移量开始取值,为了读取正确的字节数,系统偏移量的指针类型进行了强转。
写在最后
一般情况下,我们应该多用 @property,因为它可以进行某种程度的自动内存管理。但是我们在用 @property 这样方便的语法时,也千万不能忘记他的本质,这样才更有利于我们对于开发的理解。