内存模型
对象本质是 c 中的结构体,Objective-C 中对对象的定义为struct objc_object
,其中isa
是它唯一的私有成员变量。在内存模型中,每个对象除了isa
指针,还包括从顶级父类到直接父类,以及自己的所有成员变量。
类对象
isa
指针指向的是对象的类,对象的类本质上也是对象,称为类对象。类对象描述了对象的数据:对象占用的内存大小、成员变量的类型和布局等,而且也描述了对象的行为:对象能够响应的消息、实现的实例方法等:
- 对象的方法列表(对象能够接收的消息列表,保存在它所对应的类对象中)
- 成员变量列表
- 属性列表
因此,当我们调用[receiver message]
时,首先通过isa
在类对象中查找message
,找到则执行,否则沿着继承链继续在 superclass 中找。类对象中保存着类的继承关系,类对象中有一个superclass
指针,指向它的父类对象。大部分对象都继承自NSObject
,另一个根类是NSProxy
。根类的superclass
指针指向nil
。
既然类对象是一个对象,那么它也是其他类的实例,这个类叫做元类(metaclass)。其实,每个类对象都是元类的一个单例。
元类
元类其实也是对象,它保存着类的数据与类方法列表。与类对象不同的是,所有的元类都是根元类的实例,即isa
指针都指向根元类,而根元类的isa
指针指向自己。根元类也继承自根类。
比如在调用类方法时,[NSObject new]
,类对象是否能相应这个消息是通过isa
到元类中查找才能知道。
理论上我们也可以给元类发送消息,但是 Objective-C 倾向于隐藏元类,不想让大家知道元类的存在。元类是为了保持 Objective-C 对象模型在设计上的完整性而引入的,比如用来保存类方法等,它主要是用来给编译器使用的。
属性
属性用@property
定义,那么属性的本质是什么?
@property = ivar + getter + setter;
下面解释下:
属性(property)有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)
也就是说,定义一个属性后,编译器会自动帮你定义一个与之对应的实例变量,并生成对该实例变量的存取方法。
property 在 runtime 中为objc_property_t
:
|
|
而objc_property
是一个结构体,定义如下:
|
|
而attributes本质是objc_property_attribute_t
,定义了property的一些属性,定义如下:
|
|
而attributes的具体内容是什么呢?其实,包括:类型,原子性,内存语义和对应的实例变量。
例如:我们定义一个string的property@property (nonatomic, copy) NSString *string;
,通过 property_getAttributes(property)获取到attributes并打印出来之后的结果为T@"NSString",C,N,V_string
其中T就代表类型,可参阅Type Encodings,C就代表Copy,N代表nonatomic,V就代表对于的实例变量。
自动合成(autosynthesis)
编译器为属性自动添加存取方法的过程叫做自动合成。这个过程是编译器在编译器进行的,以编辑器里看不到这些“合成方法”(synthesized method)的源代码。
编译器自动添加的实例变量的名称默认是属性名前再加个下划线。如属性firstName
对应的默认实例变量为_firstName
。也可以通过synthesize
来手动指定实例变量名:
|
|
定义属性大致会生成五个东西:
OBJC_IVAR_$类名$属性名称
:该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。setter
与getter
方法对应的实现函数ivar_list
:成员变量列表method_list
:方法列表prop_list
:属性列表
也就是说我们每次在增加一个属性,系统都会在ivar_list
中添加一个成员变量的描述,在 method_list
中增加 setter 与 getter 方法的描述,在属性列表中增加一个属性的描述,然后计算该属性在对象中的偏移量,然后给出 setter 与 getter 方法对应的实现,在 setter 方法中从偏移量的位置开始赋值,在 getter 方法中从偏移量开始取值,为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转.
修饰符
属性的修饰符由四部分组成,举个例子:
|
|
- 原子性:有
atomic
、nonatomic
两种,atomic
使属性具有原子性,一定程度上是线程安全的,而nonatomic
则不支持。在 iOS 开发当中,绝大部分情况下使用nonatomic
- 读写规则:有
readwrite
、readonly
两种,分别支持读写和只读 - 存取方法名:通过
getter=
、setter=
可以指定属性的存取方法名 - 内存管理:有
assign
、strong
、copy
等修饰符,具体作用在内存管理中解释
属性默认的修饰符为:
- atomic
- readwrite
- getter=$prop$, setter=set$Prop$
- 基本数据类型为 assign,对象为 strong
@synthesize
@synthesize
一般有两个作用:
- 指定属性对应的实例变量名,不写的话默认为
@synthesize var = _var
,一般不推荐改写实例变量名 - 当自动合成失效时,进行手动合成。
自动合成失效,即编译器不帮你生成实例变量。有以下情况会时自动合成失效:
- 手动实现了 getter 和 setter 方法(或实现了 readonly 属性的 getter),这时编译器会认为你想自己管理实例变量,所以不会帮你生成
- 通过
@dynamic
指定了属性的动态性,详见下节
@dynamic
@dynamic
告诉编译器:属性的 setter 和 getter 方法由用户自己实现,不用自动生成。假如一个属性被声明为@dynamic var
,然后你没有提供 setter 方法和 getter 方法,编译的时候没问题,但是当程序运行到instance.var = someVar`,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
拷贝
系统对象
一般在定义NSString
、NSArray
、NSDictionary
这些对象时,通常会使用copy
关键字,因为它们有对应的可变类型:NSMutableString
、NSMutableArray
、NSMutableDictionary
。用copy
修饰的对象在被赋值时会自动调用它的 copy 方法,即被赋值的指针会指向拷贝对象。
对于上述具有可变和不可变两种形式的对象而言,调用 copy 方法产生的都是不可变的拷贝对象,对应的可变拷贝方法为mutableCopy
。拷贝还存在深拷贝和浅拷贝两种形式,对应如下:
- [immutableObject copy] // 浅复制
- [immutableObject mutableCopy] //深复制
- [mutableObject copy] //深复制
- [mutableObject mutableCopy] //深复制
这个也比较好理解,对于不可变对象进行 copy,因为被拷贝对象与拷贝对象都不可变,所以只需进行指针复制即可;如果有一个对象是可变的,那就需要进行内容复制。对于容器对象而言,内容复制只是对于容器本身的复制,容器中的对象仍是指针复制。
因此,对于不可变对象的申明,最好使用 copy 修饰,防止对象在外部被修改。对于可变对象,使用 strong,保持对象的可变性。如:
|
|
如果 array 持有的是一个外部可变对象,那么 array 会随着外部对象的改变而改变,因为它们指向的是同一块内存,而 array 的申明却是不可变对象NSArray
。又如:
|
|
如果对 mutableArray 进行元素的增删,编译时不会报错,但是运行时会崩溃。因为用了 copy 修饰符,所以它本质上是一个不可变数组,因此运行时找不到对应的增删元素的方法。申明的可变只能骗过编译器。
block 也经常使用 copy 关键字,这是历史遗留问题。在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。
自定义对象
如果要让自定义的对象能够实现 copy 方法,首先需要实现<NSCopying>
协议(mutableCopy 则需要实现<NSMutableCopying>
协议)。
已 NSCopying 协议为例,该协议只有一个方法:
|
|
要实现 copy,就要实现这个方法,而不是 copy 方法。比如可以这样写:
|
|
注意,对象的实例变量如果需要拷贝,还需要手动实现。如果缩写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。
如果需要重写带 copy 修饰符的 setter,不要忘了手动 copy:
|
|
等同性
首先看一段代码:
|
|
在比较两个对象时,==操作符比较的是两个指针所指向的地址,equalA 比较两个不同的对象,因此返回 NO。isEqualToString:
方法是 NSString 的特有方法,只比较字符串是否相等,上面两个字符串是一样的,因此返回 YES。isEqual:
方法是定义在 NSObject 协议中的方法,它的默认实现是:当且仅当其内存地址完全相等时,两个对象才相等,这与==操作符的比较方式相同。但是在 NSString 中,isEqual:
方法得到了覆写,它首先比较两个对象类型是否相同,若相等,则调用isEqualToString:
,否则再执行原有的isEqual:
。因此,equalB 的结果与 equalC 相同。但是第三个调用比第二个调用快。
isEqualToString:
这类方法是特定类所具有的等同性判定方法,除 NSString 之外,NSArray 与 NSDictionary 类也具有特殊的等同性判定方法,分别为isEqualToArray
与isEqualToDictionary
。一般对于自定义类也可编写此类比较方法。
NSObject 协议中有两个用于判断等同性的关键方法:
|
|
如果要比较自定义的类需要覆写isEqual:
方法,同时也需要覆写hash
。在覆写isEqual:
方法时,可以首先==操作符比较,若相等则直接返回 YES,接着比较对象类型,若不同则直接返回 NO,然后再比较能够判定对象等同性的关键属性即可。
接着实现hash
方法,根据等同性约定:若两对象相等,则其哈希码也相等,但是两个哈希码相同的对象却未必相等。对于哈希码计算的要求是:
- 计算速度快
- 对于相等的对象,其哈希值必须相等
- 尽量减少不等对象的哈希码碰撞
有一种做法是:
|
|
这种做法既能保持较高效率,又能使生成的哈希码至少位于一定范围之内,而不会过于频繁。如果哈希码碰撞率过高,在 collection 中使用这类对象就会产生性能问题。
对于容器中可变类的等同性,有一种情况要注意,就是在容器中放入可变类对象后,就不应再改变其哈希码了。如果某对象在放入“箱子”之后哈希码又变了,那么其现在所处的这个箱子对它来说就是“错误”的。要想解决这个问题,需要确保哈希码不是根据对象的“可变部分”计算出来的,或是保证放入 collection 之后就不再改变对象内容了。
Core Fundation 对象
Core Fundation 框架是一组 C 语言接口,它与 Fundation 框架紧密相关,CF 对象与 OC 对象也可以相互转换,因为 ARC 的内存管理只针对 OC 对象有效,所以对象的转换主要涉及对象所有权的处理:
__bridge
:只作类型转换,不涉及所有权的转移。如:
|
|
这里 string 对象会由 ARC 管理内存,所以不用手动释放 cfString。
__bridge_retained
:会让 CF 对象得到对象所有权,即 CF 和 OC 都持有对象所有权。
|
|
string 释放后,cfString 仍然可以使用,但是它需要手动被释放:CFRelease(cfString);
。__bridge_retained
可以用CFBridgingRetain()
代替。
__bridge_transfer
:对象所有权转移:CF -> OC
|
|
cfString 的所有权已经被转移,所以不需要手动释放。可以用CFBridgingRelease()
代替。
+load & +initialize
+load
+load 方法是当类或分类被添加到 Objective-C runtime 时被调用的,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。子类的 +load 方法会在它的所有父类的 +load 方法之后执行,而分类的 +load 方法会在它的主类的 +load 方法之后执行。但是不同的类之间的 +load 方法的调用顺序是不确定的。
load 方法并不遵从那套继承规则。如果某个类本身没实现 load 方法,那么不管其各级超类是否实现此方法,系统都不会调用。此外,在分类和其所属的类里,都可能出现 load 方法。此时两种实现代码都会调用。
+load 方法的执行先于 main 函数。
load 方法务必实现的精简一些,也就是要尽量减少其所执行的操作,其真正用途仅在于调试程序。
+initialize
+initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 +initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize 方法是永远不会被调用的。
initialize 方法与其他消息一样,如果某个类未实现它,而其超类实现了,那么就会运行超类的实现代码。因此父类的实现可能会被执行多次的。有时候,这可能是你想要的;但如果我们想确保自己的 +initialize 方法只执行一次,避免多次执行可能带来的副作用时,我们可以使用下面的代码来实现:
|
|
+load | +initialize | |
---|---|---|
调用时机 | 被添加到 runtime 时 | 收到第一条消息前,可能永远不调用 |
调用顺序 | 父类->子类->分类 | 父类->子类 |
调用次数 | 1次 | 多次 |
是否需要显式调用父类实现 | 否 | 否 |
是否沿用父类的实现 | 否 | 是 |
分类中的实现 | 类和分类都执行 | 覆盖类中的方法,只执行分类的实现 |
参考资料
- Objective-C 对象模型
- 招聘一个靠谱的iOS–参考答案
- 《Effective Objective-C 2.0》
- Objective-C +load vs +initialize