type
status
date
slug
summary
tags
category
icon
password
本文是Objective-C系列的第2篇,主要讲述OC对象的底层结构,以及分类:实例对象、类对象、元类对象。
在上一篇Objective-C(一)对象内存分析分析后,我们得知了一个类在内存中的存储。
但是,我们只分析了类的成员变量和属性,我们知道,一个OC对象,还包括方法、协议等极其重要的信息,那么它们在哪里,又是如何存储,如何用的呢?
本篇在此进一步分析Objective-C类体系的分类及其在内存中的完整分布。
为此,我们先要进行一些准备工作。
一、准备
1.1 对象的分类
Objective-C中的对象,简称OC对象,主要可以分为3种:
- instance对象(实例对象)
- class对象(类对象)
- meta-class对象(元类对象)
我们针对OC中对象分为三种进行了测试,测试结果如下:
从上图我们可以得到以下结论:
- 实例对象可以有多个:每alloc一个对象,就会创建一个实例对象;
- 类对象只能有一个:不同实例对象的类对象为同一个;
- 元类对象只能有一个:不同实例对象(类对象)的元类对象只有一个;
那么测试代码中的几个方法,下面也简要说明下,部分截取源码项目-BFOCClass分类-01。
1.1.1 [NSObject class]
在
NSObject.mm
中:返回类对象(元类对象)本身。
所以,
[[[NSObject class] class] class]
无论调用多少次class,其返回都是NSObject class对象。1.1.2 [person1 class]
返回实例对象的类对象
由(2)中可知,
- [person1 class]:返回person1中isa指向的对象,就是BFPerson class对象;
1.1.3 objc_getClass
该方法与object_getClass从方法名看极其相似。
- 参数:字符串类名;
- 返回:对应的类对象。
1.1.4 object_getClass
返回传入
obj
中isa
所指向的对象,传入的obj可能是instance
对象、class
对象、meta-class
对象其返回值对应为:
a) 如果是
instance
对象,返回class
对象b) 如果是
class
对象,返回meta-class
对象c) 如果是
meta-class
对象,返回NSObject(基类)的meta-class
对象所以上面测试中:
- object_getClass(person1):返回即
class
对象;
- object_getClass(personClass5):返回即
meta-class
对象;
1.1.5 class_isMetaClass
返回是否为meta-class对象。
1.2 一些从源码来的结构体
我们会从源码摘抄一部分结构体,用来进行后面的论证。
1.2.1 NSObject
NSObject从源码中得到,其第一个成员变量为
isa
,该Class
结构体第一个成员变量名称也是isa
。isa
里则包含了(元)类信息的存放地址。1.2.2 class_rw_t
class_rw_t
、class_ro_t
均是存放类中信息的结构体,包括成员变量、属性列表、协议列表及方法列表,其具体如下:class_ro_t
当前类在编译期就已经确定的属性、方法以及遵循的协议。class_rw_t
则是在运行时,runtime重新加载布局的当前类的属性、方法和协议等。1.2.3 method_t
method_t
是方法最底层的结构。
method_list_t
- 方法列表,一维数组
- 内部存放
method_t
class_ro_t
中的成员变量。
method_array_t
- 同样是方法列表,但是是二维数组
- 内部存放
method_t
class_rw_t
中的成员变量;
三者的关系如下图:
与method类似,property、protocol的类似。
至于为什么会有一维和二维数组,后续文章会详述该问题。
1.2.4 property_t
property_array_t
与property_list_t
的结构与上面的method中的数组类似,分别是二维、一维数组。假如属性名为name,类型为NSString,那么对应的attributes:T@"NSString",C,N,V_name
1.2.5 protocol_t
protocol_array_t
、protocol_list_t
分别是存放protocol_t
的二维及一维数组。类似的,我们关注
protocol_t
结构体:1.2.6 ivar_t
我们发现,针对成员变量,只有
ivar_list_t
,而没有ivar_array_t
这样的结构。因此就算是runtime,也无法在运行时往类中添加成员变量。
1.3 NSObject实例对象的内存
我们已经对NSObject实例对象的内存结构相当熟悉了。我们再阐述以下观点:
- NSObject实例对象内存中有且只有一个变量为isa,isa指向NSObject的class对象;
下面,我们针对编译期的
isa
,结合源码,我们得到如下:运行期的isa:
二、对象的内存存储
上面,我们做了一些准备工作,了解了NSObject如何存储各种各样信息的结构体及存储在什么地方。
下面我们将会针对,三种类型做一个简单的描述。
2.1 instance对象
其内存布局如下:
instance对象在内存中存储的信息有:
isa
指针
- 其他成员变量
2.2 class对象
class对象在内存中存储的信息主要包括
isa
指针
superclass
指针
- 类的对象方法信息(instance method)
- 类的属性信息(property)
- 类的协议信息(protocol)
- 类的成员变量信息(ivar)
2.3 meta-class对象
meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括
isa
指针
superclass
指针
- 类的类方法信息(class method)
- 其他包括属性、协议、成员变量均为NULL
三、isa和superclass
isa
在平时开发中很少使用,但是相信很多iOS开发者并不陌生。在前面了解了NSObject、各种结构体及对象三大分类之后,苹果为我们提供了的Cocoa/Cocoa Touch中,类蔟体系的建立依赖两个指针:
isa
和superclass
。那么又是如何串联的?横向联系:
isa
的作用就是,将某一个类的instance对象、class对象、meta-class对象建立了横向联系,为查找对象存储各类信息提供指引。纵向联系:
superclass
则是面向对象语言中最为重要的特性——继承的体现,它体现的是类与类之间的联系,即为类图中建立了纵向联系。请注意区分:一个类各种对象之间的联系,以及不同类之间的联系!
3.1 isa
3.1.1 isa指针
3.1.2 ISA_MASK
我们为了验证该结论,在真机设备上,运行项目-BFOCClass分类-01,尝试获取了BFPerson对象,并打印类相关地址:
从上面打印我们得知:
- person1实例对象的地址为0x12fd1f570
- person1对象地址起始的8个字节,就是
isa
,此处为:0x1a100091e2d
- BFPerson class 地址为0x100091e28不等于0x1a100091e2d
- 0x1a100091e2d(
isa
) & 0x0000000ffffffff8(ISA_MASK
) = 0x100091e28
3.2 superclass
3.2.1 class对象
3.2.2 meta-class对象
3.3 类图体系
据此,
isa
和superclass
联手织起的下面这张类图:3.3.1 联系
isa
指向- instance的
isa
指向class - class的
isa
指向meta-class - meta-class的
isa
指向基类的meta-class
- superclass指向
- class的
superclass
指向父类的class,如果没有父类,superclass
指针为nil - meta-class的
superclass
指向父类的meta-class,基类的meta-class的superclass
指向基类的class
3.3.2 方法调用
- instance调用对象方法的轨迹
isa
找到class,方法不存在,就通过superclass
找父类
- class调用类方法的轨迹
isa
找meta-class,方法不存在,就通过superclass
找父类
代码参考BFOCClass分类-01
继承关系:
BFStudent
->BFPerson
->NSObject
3.3.2.1 情况1
- BFStudent实现
-(void)test;
,调用BFStudent的实现方法;
- BFStudent未实现
-(void)test;
,若BFPerson实现,调用BFPerson实现;
- BFStudent和BFPerson均未实现,NSObject若实现,调用NSObject实现;
- 以上都未实现,代码是不能直接调用
[stu test];
,会编译报错,需要改调用方式: - NSObject若实现
-(void)test;
,调用-(void)test;
; - NSObject若实现
+(void)test;
,崩溃;
- 继承体现中的类,均未实现,崩溃—— [BFStudent test]: unrecognized selector sent to instance
3.3.2.2 情况2
- BFStudent实现
+(void)test;
,调用BFStudent的实现;
- BFStudent未实现
+(void)test;
,若BFPerson实现,调用BFPerson实现;
- BFStudent和BFPerson均未实现,调用NSObject实现;
- 无法
[BFStudent test];
调用,调用方式需改为: - NSObject若实现
+(void)test;
,调用+(void)test;
; - NSObject若实现
-(void)test;
,调用-(void)test;
; - NSObject若实现
+(void)test;
和-(void)test;
,调用+(void)test;
;
- 继承体现中的类,均未实现,崩溃—— +[BFStudent test]: unrecognized selector sent to instance
3.3.3 分析
针对情况1和2,大部分我们都能理解,但有一种情况。是有差别的。
在BFStudent和BFPerson中均未实现对应的对象方法(类方法)时,都会去NSObject中寻找。
- 对象方法:NSObject寻找对应的对象方法,未找到对象方法,崩溃,参考下面的蓝色箭头。
- 类方法:NSObject先寻找对应的类方法,未找到类方法,然后继续寻找对应的实例方法,若两者未找到,才会崩溃,见下面的黄色箭头。
四、窥探对象完整的内部结构
到此,我们针对对象已经了解的差不多了,但是我们还是没有完整的看到运行时,对象的内部结构。
4.1 如何窥探
有两种方式:
- 方式一:编译objc源码,直接通过objc4源码调试;
- 编译细节,请参考:Runtime(一)Runtime简介
- 方式二:抽象出[一些从源码来的结构体](#2. 一些从源码来的结构体)中提到的结构体,将正常对象转换为对应的结构体,进行调试;
我们采用方式二。
4.2 一探究竟
- 抽象出的结构体从这里下载。
- 导入后,将编译项目调整为命令行项目,并将main.m改为main.mm,以支持C++编译。
对应的项目源码。
4.2.1 类对象
4.2.2 元类对象
现在,你知道OC对象的本质了吗?它们的分类呢?
参考
链接
示例代码