主要内容:围绕类
展开探索
一、isa 走位
1.类的分析
2.元类
3.isa走位
4.superClass 走位
二、objc_class & objc_object
三、类结构分析
一、isa 走向
主要分析:isa
的走向 及类
的关系
1.准备工作
- 自定义两个类:
LGPerson
继承NSObject
,LGTeacher
继承LGPerson
@interface LGPerson : NSObject
@end
@interface LGTeacher : LGPerson
@end
- main函数中 实例化对象
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
LGTeacher *teacher = [LGTeacher alloc];
}
return 0;
}
2.元类
main函数的 LGTeacher
部分加断点,lldb
调试,调试过程:如图
问题
: 为什么在前面过程中,地址0x00000001000021c8
和0x00000001000021a0
都得到 LGPerson, 到底哪个才是 LGPerson?
0x00000001000021c8
是对象
的isa
指针地址& isaMask
后得到对象的 isa
归属的类,也就是person
的isa
的归属来自于LGPerson
.0x00000001000021a0
是类信息
中isa
指针地址& isaMask
后得到类对象
的归属,类对象
的归属来自于元类
- 所以,上面两个LGPerson,一个是
类对象
,一个是元类
元类的定义
- 什么是
元类
元类
是系统给我们的非常重要的东西,对象
的isa
归属 来自于类
,类
也是一个对象 是类对象
,那么类对象的归属是谁?苹果把类对象
的归属给了元类
。 - 为什么要有
元类
?
既然类
是一个对象
,那类
就会有方法、协议、属性等等
一系列的存储和归属,那么方法等
归属于谁?怎么去查看?不好查看,这时就需要在系统级别
里面,需要元类
这样的定义,所以就有元类
。元类
的定义
和创建
都是由编译器自动完成
。
结论
:类
的归属来自于元类
,所有类方法
的存储等等都存在元类
里面
3.isa走向图
- 继续
lldb
调试 ?
得到结论
:对象
的isa ->类
,类
的isa ->元类
,元类
的isa ->NSObject
引出问题
:元类
isa 指向的NSObject
不是内存
中的NSObject
,那是什么?内存中会存在多个NSObject 类信息吗?
验证内存中有几个类对象:
void lgTestClassNum(){
Class class1 = [LGPerson class];
Class class2 = [LGPerson alloc].class;
Class class3 = object_getClass([LGPerson alloc]);
Class class4 = [LGPerson alloc].class;
NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}
输出结果:
结论:
- NSObject在内存中只存在一份,如果存在多份,每创建一个类的对象,就要多一份,但输出结果地址相同,说明在内存中永远只存在一份。
元类
isa 指向的NSObject
是元类,NSObject 来自于根类,所以也叫根元类。
验证:元类
isa 指向的NSObject
是元类
得出以下结论:
NSObjet 的实例对象
的isa
指向NSObject 的类
,NSObject 的类
的isa
指向NSObject 的元类
,NSObject 的元类
就是根元类
。根元类
再指,指向的是自己
- 任何
类
的实例对象
的isa
指向类
,类
的isa
指向元类
,元类
的isa
指向根元类
。特殊情况
:比如说子类
和父类
,Teacher
继承自Person
,他们也满足这样情况Person的对象
指向Person类
,类
指向元类
,元类
指向根元类
。只有NSObject
免了一步 元类
的指向,直接指向根元类
。
如下图:
代码验证:
`
void lgTestNSObject2(){
// NSObject实例对象
LGPerson *object1 = [LGPerson alloc];
// NSObject类
Class class = object_getClass(object1);
// NSObject元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
}
`
输出结果:
4.类的继承图
思考问题:LGTeacher
继承于LGPerson
,实例化的对象 teache
r 与 person
有关系吗? 答案是:没关系
,因为实例对象
没有继承关系
,只有类
和类
之间才存在继承关系
,同样道理 元类
也存在继承关系
。注意
:根元类 继承于 NSObject
也就是 根元类
的父类
是 NSObject
,万物皆是由NSObject
来创建的
如图:
类
之间 的继承关系
:
-
实例对象
之间没有
继承关系
-
子类
继承于父类
,父类
继承于NSObject
,NSObject
继承于nil
,NSObject 是万物之源
。 -
子类的元类
继承于父类的元类
,父类的元类
继承于根元类
,根元类
继承于NSObject类
(根元类
的父类
是NSObject
)。
二、objc_class & objc_object
新问题:为什么 对象
和类
都有isa属性? 这里就要说两个结构体类型:objc_class
和objc_object
。
从 clang
编译生成的 main.m文件中,可以看到如下c++源码
-
NSObject
在底层编译为NSObject_IMPL
结构体-
NSObject_IMPL
结构体 中有 一个Class
类型的isa
指针,其中Class
是 objc_class 类型。 -
objc_class
是一个结构体,在iOS中,所有的Class
都是以objc_class
为模版创建的。
-
typedef struct objc_class *Class;
struct NSObject_IMPL {
Class isa;
};
-
在 objc4 源码中搜索
objc_class
的定义,共找到两个版本-
旧版
已废除 在runtime.h
中
新版
在objc_runtime-new.h 中-
从新版的定义中,可以看到
objc_class
结构体类型是继承自 objc_object -
-
在 源码中搜索
objc_object {
,可以找到objc_object
的定义
问题:objc_class 与 objc_object 有什么关系?
结构体类型
objc_class
继承自objc_object
类型,其中objc_object
也是一个结构体,且有一个isa
属性,所以objc_class
也拥有了isa属性mian.cpp底层编译文件中,
NSObject
中的isa
在底层是由Class
定义的,其中class
的底层编码来自objc_class
类型,所以NSObject
也拥有了isa
属性NSObjec
t 是一个类,用它初始化一个实例对象objc
,objc 满足objc_object
的特性(即有isa属性),主要是因为isa
是由NSObject
从objc_class
继承过来的,而objc_clas
s继承自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
-
上层
是通过底层
的模板
创建的 一些类型,例如LGTeacher
-
objc_class、objc_object、object、NSObject
等的整体的关系,如图
三、类结构分析
主要是分析类的属性、方法、成员变量、协议...
上面我们知道了类的结构是什么样的,那么类里面具体都包含了一些什么内容呢,下面我们就来分析一下objc_class
struct objc_class : objc_object {
// Class ISA; // 8字节
Class superclass; // 8字节
cache_t cache; // formerly cache pointer and vtable 16字节
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() const {
return bits.data();
}
.......
}
- 第一个属性
Class ISA
被注释掉的,意思就是从父类继承
过来的,我们进入objc_object
里面可以看到只有一个isa
,占用8个字节。 - 第二个属性
Class superclass父类
,一个指针占用8个字节。 - 第三个属性
cache_t cache
一个结构体
,顾名思义是一些缓存的信息,总共占用16个字节
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic _buckets; // bucket_t 结构体指针8个字节
explicit_atomic _mask; //typedef uint32_t mask_t; int占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
.........
#if __LP64__
uint16_t _flags;// typedef unsigned short 2字节
#endif
uint16_t _occupied;// typedef unsigned short 2字节
}
其他的static变量和方法均不存在结构体内存中,因此不占内存
要获取bits ,就是首地址 平移32,也就是 + 0x20.
- 第四个属性bits是什么?这里我们来看一下。
typedef unsigned long uintptr_t;
struct class_data_bits_t {
// 相当于 unsigned long bits; 占64位
// bits实际上是一个地址(是一个对象的指针,可以指向class_ro_t,也可以指向class_rw_t)
uintptr_t bits;
......
}
从这里可以看到bits
应该就是一个64位
的数据段,那么里面存了什么数据呢,还要继续往下分析。
在class_data_bits_t bits
的注释:class_rw_t * plus custom rr/alloc flags
,意思是class_data_bits_t
就相当于class_rw_t *
加上rr/alloc
标志。它提供了data()
方法返回class_rw_t *
指针。
而在bits后面就紧接着声明了一个 class_rw_t *
指针,通过bits.data()
返回,接下来就来看看这个bits.data()
class_rw_t *data() {
// 这里的bits就是上面定义的class_data_bits_t bits;
return bits.data();
}
struct class_data_bits_t {
uintptr_t bits;
class_rw_t* data() const {
// FAST_DATA_MASK的值是0x00007ffffffffff8UL
//(lldb) p/t 0x00007ffffffffff8 打印二进制 看一下
//(long) $0 = 0b0000000000000000011111111111111111111111111111111111111111111000
// bits和FAST_DATA_MASK按位与,实际上就是取了bits中的[3,46]共44位
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
}
那么这个class_rw_t *是什么呢?
struct class_rw_t {
......
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->methods;
} else {
return method_array_t{v.get()->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->properties;
} else {
return property_array_t{v.get()->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->protocols;
} else {
return protocol_array_t{v.get()->baseProtocols};
}
}
}
在这个 class_rw_t
结构体中我们发现这里有methods(方法)、properties(属性)、protocols(协议
这些信息,那么我们所需要的类中的方法、属性、成员变量等信息是不是在这里存储的呢?下面我们就用代码来验证下。
@interface LGPerson : NSObject
{
NSString *hobby;
}
@property (nonatomic,copy) NSString *nickName;
- (void) ins_sayHello;
+ (void) cls_sayHappy;
@end
先定义一个LGPerson类,里面有 属性:nickName、 成员变量:hobby、 对象方法: ins_sayHello、 类方法: cls_sayHappy
然后我们通过lldb
指令打印查看,b结合上面的分析,来看看这几个成员都存储在了什么地方
在找bits
的时候是通过内存偏移
方法来找到,这也就是开头先补充的内存偏移的概念。 因为在objc_class的结构中,isa占8字节,superclass占用8字节,cache占用16个字节,将cls的地址偏移32
个字节即0x20
便是bits
的地址。
获取类的首地址有两种方式
通过p/x LGPerson.class直接获取首地址
通过x/4gx LGPerson.class,打印内存信息获取
-
注意:这里获取bits
是通过类的内存地址 + 偏移量
而不是通过isa的地址加上偏移量,这也是类和数组不同的地方。类的地址也是第一个元素地址,只是通过x/4gx 读出来的是类地址后面存的值,不是第一个元素地址。isa的地址比类的地址低(0x28)40字节(当我们实现了类之后就会直接去处理元类,所以类和元类是连续的,而类的大小 到bit 之后正好40在,这里涉及到内存平移到元类)。
通过查看class_rw_t
定义的源码发现,结构体
中有提供相应的方法去获取属性列表
、方法列表
等
方法的符号绑定:对于v16@0:8
v
表示方法的返回值为void
16
表示方法参数的大小(通过clang
编译的cpp文件
可知该方法存在两默认的参数:(NAPerson * self, SEL _cmd
))
@
表示方法的第一个参数类型
0
表示方法的第一个参数占用内存的起始位置
:
表示方法的第二个参数类型
8
表示方法的第二个参数占用内存的起始位置
对于set方法会多一个参数所以方法的符号会有区别
关于参数类型编码参考这个链接
通过以上打印可以看到,在class_rw_t
中找到了我们所定义的nickName属性、对象方法ins_sayHello、nickName的setter/getter方法,但是成员变量hobby
和类方法cls_sayHappy
都没有找到。
此时再从class_rw_t
找一找其他线索,发现有一个const class_ro_t *ro()
的方法,该方法返回一个常量结构体指针
,那么我们要找的成员变量和类方法会不会在这里呢,点进去看一下
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList; //方法列表
protocol_list_t * baseProtocols; //协议列表
const ivar_list_t * ivars; //成员变量列表
property_list_t *baseProperties; //属性列表
method_list_t *baseMethods() const {
return baseMethodList;
}
......
}
进来一看发现,这里竟然跟class_rw_t看起来差不多,同样有方法、属性、协议列表
,而且还有一个ivars
列表,那么这个ivar
s会不会就是成员变量列表
呢。接下来继续用lldb指令来查看
果然,我们定义的成员变量hobby
是在class_ro_t
里面的。同样在baseMethodList、baseProperties
里面也找到了我们所定义的属性和对象方法
,这里就不截图了。
此时我们来总结一下:
1.在class_rw_t
里面存放的有methods、properties、protocols
2.在class_ro_t
里有baseMethodList、baseProperties、baseProtocols、ivars
3.class_ro_t
这个结构体是通过const定义,说明在编译时候就确定好了,后面取出来使用是不可以更改的。
4.成员变量
不生成setter/getter方法,并且存在class_ro_t的ivars
里面。
5.此时还有一个类方法cls_sayHappy没有找到。
通过以上分析我们大概可以知道,类的属性、成员变量、方法、协议等信息存在什么位置了。但是class_rw_t和class_ro_t为什么会存了一些相同的信息呢?这就需要我们进一步的分析了。
类的信息是如何存储的
通过前面的分析,在class_rw_t结构中可以拿到类的属性等相关信息了,class_ro_t结构中可以拿到类的成员变量等信息,由此就形成以这样的一个结构。
通过上图可以得出以下一些结论:
- 通过
@property
定义的属性
,会存储在bits
属性中,通过bits --> data() --> properties() --> list
获取属性列表,其中只包含属性。 - 通过
{}
定义的成员变量
,也会存储在类的bits
属性中,通过bits --> data() -->ro() --> ivars
获取成员变量列表,除了包括成员变量
,还包括属性定义的成员变量
探索类方法存储位置
此时此刻我们就把class_rw_t
和 class_ro_t
存储类信息的过程探索的差不多了。
但是类方法cls_sayHappy还没找到,既然我们在类里面没有找到cls_sayHappy
,那么我们想一下它会存到哪里呢?通过上面分析的isa走位,猜想它会不存到元类
里面去了,那就去元类找找看
从上图打印来看,我们在元类里面找到了类方法cls_sayHappy,证明了类方法
是存在元类
里面的。此刻我们所声明的方法、属性、成员变量已经全部找到了,也大概了解了类的结构
以及类的成员
信息都存在哪里。