准备工作
定义两个类
- 继承自
NSObject
的类CJLPerson
,
- 继承自
CJLPerson
的类CJLTeacher
- 在main中分别用两个定义两个对象:
person & teacher
int main(int argc, const char * argv[]) {
@autoreleasepool {
//ISA_MASK 0x00007ffffffffff8ULL
CJLPerson *person = [CJLPerson alloc];
CJLTeacher *teacher = [CJLTeacher alloc];
NSLog(@"Hello, World! %@ - %@",person,teacher);
}
return 0;
}
元类
首先,我们先通过一个案例的lldb调试先引入元类
- 在main中CJLTeacher部分加一个断点,运行程序
-
开启lldb调试,调试的过程如下图所
总结
从图中可以看出
对象
的isa
指向类
(也可称为类对象
)类
的isa
指向元类
元类
的isa
指向根元类
,即NSObject
根元类
的isa
指向 它自己
NSObject到底有几个?
从图中可以看出,最后的根元类
是NSObject
,这个NSObject
与我们日开开发中所知道的NSObject
是同一个吗?
我们也通过lldb调试,来验证这两个NSObject是否是同一个,如下图所示
从图中可以看出,最后NSObject
类的元类
也是NSObject
,与上面的CJLPerson
中的根元类
(NSObject)的元类
,是同一个,所以可以得出一个结论:内存中只存在存在一份根元类NSObject,根元类的元类是指向它自己
[面试题]:类存在几份?
由于类的信息在内存中永远只存在一份,所以 类对象只有一份
著名的 isa走位 & 继承关系 图
根据上面的探索以及各种验证,对象、类、元类、根元类
的关系如下图所示
- 【注意】
实例对象
之间没有继承关系
,类
之间有继承关系
举例说明
以前文提及的的CJLTeacher及对象teacher
、CJLPerson及对象person
举例说明,如下图所示
-
isa 走位链(两条)
-
teacher的isa走位链:
teacher(子类对象) --> CJLTeacher (子类)--> CJLTeacher(子元类) --> NSObject(根元类) --> NSObject(跟根元类,即自己)
-
person的isa走位图:
person(父类对象) --> CJLPerson (父类)--> CJLPerson(父元类) --> NSObject(根元类) --> NSObject(跟根元类,即自己)
-
-
superclass走位链(两条)
-
类的继承关系链:
CJLTeacher(子类) --> CJLPerson(父类) --> NSObject(根类)--> nil
-
元类的继承关系链:
CJLTeacher(子元类) --> CJLPerson(父元类) --> NSObject(根元类)--> NSObject(根类)--> nil
-
objc_class & objc_object
isa走位我们理清楚了,又来了一个新的问题:为什么 对象
和 类
都有isa属性
呢?这里就不得不提到两个结构体类型:objc_class
& objc_object
下面在这两个结构体的基础上,对上述问题进行探索。
在上一篇文章使用clang
编译过main.m文件,从编译后的c++文件中可以看到如下c++源码
NSObject
的底层编译是NSObject_IMPL
结构体,- 其中
Class
是isa
指针的类型,是由objc_class
定义的类型, - 而
objc_class
是一个结构体。在iOS中,所有的Class
都是以objc_class
为模板创建的`
- 其中
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
-
在objc4源码中搜索
可以看到objc_class
的定义,源码中对其的定义有两个版本新版 位于objc-runtime-new.h
,这个是objc4-781
最新优化的,我们后面的类的结构分析也是基于新版来分析的。objc_class
结构体类型是继承自objc_object
的 -
在objc4源码中搜索
objc_object (或者 objc_object {
,这个类型也有两个版本- 一个位于
objc.h
,没有被废除,从编译的main.cpp
中可以看到,使用的这个版本的objc_object
- 位于
objc-privat.h
- 一个位于
以下是编译后的main.cpp
中的objc_object
的定义
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
【问题】objc_class 与 objc_object 有什么关系?
通过上述的源码查找以及main.cpp中底层编译源码,有以下几点说明:
-
结构体类型
objc_class
继承自objc_object
类型,其中objc_object
也是一个结构体,且有一个isa
属性,所以objc_class
也拥有了isa
属性 -
mian.cpp底层编译文件中,
NSObject
中的isa
在底层是由Class
定义的,其中class
的底层编码来自objc_class
类型,所以NSObject
也拥有了isa
属性 -
NSObject
是一个类,用它初始化一个实例对象objc
,objc 满足objc_object
的特性(即有isa属性),主要是因为isa
是由NSObject
从objc_class
继承过来的,而objc_class
继承自objc_object
,objc_object
有isa
属性。所以对象
都有一个isa
,isa表示指向
,来自于当前的objc_object
-
objc_object(结构体)
是 当前的根对象
,所有的对象
都有这样一个特性objc_object
,即拥有isa属性
【百度面试题】objc_object 与 对象的关系
-
所有的
对象
都是以objc_object
为模板继承
过来的 -
所有的对象 是 来自
NSObject
(OC) ,但是真正到底层的 是一个objc_object(C/C++)
的结构体类型
【总结】 objc_object
与 对象
的关系
是 继承
关系
总结
-
所有的
对象
+类
+元类
都有isa
属性 -
所有的
对象
都是由objc_object
继承来的 -
简单概括就是
万物皆对象
,万物皆来源于objc_object
,有以下两点结论:-
所有以
objc_object
为模板 创建的对象
,都有isa
属性 -
所有以
objc_class
为模板,创建的类
,都有isa
属性
-
-
在结构层面可以通俗的理解为
上层OC
与底层
的对接
:下层
是通过结构体
定义的模板
,例如objc_class、objc_object
上层
是通过底层的模板创建
的 一些类型,例如CJLPerson
objc_class、objc_object、isa、object、NSObject
等的整体的关系,如下图所示
类结构分析
探索类信息中都有哪些内容
探索类信息
中有什么时,事先我们并不清楚类
的结构
是什么样的,但是我们可以通过类
得到一个首地址
,然后通过地址平移
去获取里面所有的值
根据前文提及的objc_class
的新版定义(objc4-781
版本)如下,有以下几个属性
struct objc_class : objc_object {
// Class ISA; //8字节
Class superclass; //Class 类型 8字节
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
//....方法部分省略,未贴出
}
-
isa
属性:继承自objc_object
的isa
,占8
字节 -
superclass
属性:Class
类型,Class
是由objc_object
定义的,是一个指针
,占8
字节 -
cache
属性:简单从类型class_data_bits_t
目前无法得知,而class_data_bits_t
是一个结构体
类型,结构体
的内存大小
需要根据内部的属性
来确定,而结构体指针才是8字节
-
bits
属性:只有首地址
经过上面3个属性的内存大小总和的平移,才能获取到bits
计算 cache 类的内存大小
进入cache类cache_t
的定义(只贴出了结构体中非static修饰的属性,主要是因为static类型的属性 不存在结构体的内存中),有如下几个属性
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节
explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节
mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节
#if __LP64__
uint16_t _flags; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
#endif
uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
-
计算
前两个属性
的内存大小
,有以下两种情况,最后的内存大小总和都是12
字节-
【情况一】
if
流程-
buckets
类型是struct bucket_t *
,是结构体指针
类型,占8
字节 -
mask
是mask_t
类型,而mask_t
是unsigned int
的别名,占4
字节
-
-
【情况二】
elseif
流程-
_maskAndBuckets
是uintptr_t
类型,它是一个指针
,占8
字节 -
_mask_unused
是mask_t
类型,而mask_t
是uint32_t
类型定义的别名,占4
字节
-
-
-
_flags
是uint16_t
类型,uint16_t是unsigned short
的别名,占2
个字节 -
_occupied
是uint16_t
类型,uint16_t是unsigned short
的别名,占2
个字节
总结:所以最后计算出cache类的内存大小 = 12 + 2 + 2 = 16
字节
【问题】探索成员变量的存储
由此可得出property_list 中只有属性,没有成员变量
,属性与成员变量的区别就是有没有set、get方法,如果有,则是属性,如果没有,则是成员变量。
那么问题来了,成员变量存储在哪里?为什么会有这种情况?请移至文末的分析与探索
探索 方法列表,即methods_list
准备工作:在前文提及的CJLPerson中增加两个方法(实例方法 & 类方法)
//CJLPerson.h
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end
//CJLPerson.m
@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end
也是通过lldb调试来获取方法列表,步骤如图所示
获取方法列表的lldb调试流程
-
通过
p $4.methods()
获得具体的方法列表
的list
结构,其中methods
也是class_rw_t
提供的方法 -
通过打印的
count = 4
可知,存储了4
个方法,可以通过p $7.get(i)
内存偏移
的方式获取单个方法,i 的范围是0-3
-
如果在打印
p $7.get(4)
,获取第五个方法,也会报错,提示数组越界
新问题的探索
【问题】探索成员变量的存储
由上面的属性列表分析可得出property_list 中只有属性,没有成员变量
,那么问题来了,成员变量存储在哪里?为什么会有这种情况?
通过查看objc_class
中bits
属性中存储数据的类class_rw_t
的定义发现,除了methods、properties、protocols
方法,还有一个ro
方法,其返回类型是class_ro_t
,通过查看其定义,发现其中有一个ivars
属性,我们可以做如下猜测:是否成员变量就存储在这个ivar_list_t
类型的ivars
属性中呢?
下面是lldb的调试过程
成员变量存储探索的调试
class_ro_t
结构体中的属性如下所示,想要获取ivars
,需要ro的首地址平移48
字节
struct class_ro_t {
uint32_t flags; //4
uint32_t instanceStart;//4
uint32_t instanceSize;//4
#ifdef __LP64__
uint32_t reserved; //4
#endif
const uint8_t * ivarLayout; //8
const char * name; //1 ? 8
method_list_t * baseMethodList; // 8
protocol_list_t * baseProtocols; // 8
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
//方法省略
}
通过图中可以看出,获取的ivars
属性,其中的count
为2
,通过打印发现 成员列表中除了有hobby,还有name,所以可以得出以下一些结论:
-
通过
{}
定义的成员变量
,会存储在类的bits
属性中,通过bits --> data() -->ro() --> ivars
获取成员变量列表
,除了包括成员变量,还包括属性定义的成员变量 -
通过
@property
定义的属性,也会存储在bits属性中,通过bits --> data() --> properties() --> list
获取属性列表
,其中只包含属性
【问题】探索类方法的存储
由此可得出methods list 中只有 实例方法,没有类方法
,那么问题来了,类方法存储在哪里?为什么会有这种情况?下面我们来仔细分析下
在文章前半部分,我们曾提及了元类
,类对象
的isa
指向就是元类
,元类
是用来存储类
的相关信息的,所以我们猜测:是否类方法存储在元类的bits中呢
?可以通过lldb
命令来验证我们的猜测。下图是lldb命令的调试流程类方法存储的探索流程
通过图中元类方法列表
的打印结果,我们可以知道,我们的猜测是正确的,所以可以得出以下结论:
-
类
的实例方法
存储在类的bits属性
中,通过bits --> methods() --> list
获取实例方法列表
,例如CJLPersong
类的实例方法sayHello
就存储在CJLPerson类的bits
属性中,类中的方法列表
除了包括实例方法
,还包括属性的set方法
和get方法
-
类
的类方法
存储在元类的bits属性
中,通过元类bits --> methods() --> list
获取类方法列表
,例如CJLPerson
中的类方法sayBye
就存储在CJLPerson
类的元类
(名称也是CJLPerson)的bits
属性中