- isa指针
通过上一篇文章的分析我们已经知道了实例对象,类对象,元类对象的结构如上图所示,每个对象中都有 isa 指针,isa 指针有什么作用?他们之间的关系是怎样的呢?我们写一个Person
类然后调用它的实例方法eat
,看一下底层源码:
[person eat];
//本质
objc_msgSend(person, sel_registerName("eat"));
[person eat]
本质就是向person
对象发送一条eat
消息,通过上面的结构图我们知道,对象方法是存放在类对象中的,并不在实例对象中,那实例对象是怎么拿到类对象中的对象方法的呢?同样,类方法也是如此怎么拿到存放在元类对象中的类方法的呢?.其实这就用到了isa
指针,大家记住:实例对象的isa
指针指向类对象,类对象的isa
指针指向元类对象,元类对象的isa
指针指向基类的元类对象
- superClass
superClass 只存在类对象和元类对象中,实例对象中没有superClass.类对象的 superClass 指向父类的类对象,父类的 superClass 指向基类的类对象,如果基类没有父类,superClass 为nil;元类的 superClass 指向父类的元类对象,父类的元类对象的 superClass 指向基类的元类对象,基类的元类对象的 superClass 指向基类.
现在我们再添加一个Student
类继承自Person
类,如下:
// Person
@interface Person : NSObject
{
int _age;
int _no;
int _height;
}
-(void)personInstanceMethod;
+(void)personClassMethod;
@end
@implementation Person
- (void)personInstanceMethod{
}
+ (void)personClassMethod{
}
@end
// Student
@interface Student : Person
{
int _sex;
}
-(void)studentInstanceMethod;
+(void)studentClassMethod;
@end
@implementation Student
- (void)studentInstanceMethod{
}
+ (void)studentClassMethod{
}
@end
然后再创建一个student
对象,让student
对象去调用Person
的实例方法:
Student *student = [[Student alloc]init];
[student personInstanceMethod];
我们知道Person
的实例方法是存放在Person
的类对象中的,student
对象如何找到这个方法呢?我们画一张图:
首先
student
根据
isa
找到
Student class
对象,然后再通过
Student class
对象的
superClass
找到父类
Person class
对象,再从
Person class
对象中找到
Person
的实例方法.如果父类
Person class
中依然没有,就依次网上找,直到
NSObject
,如果
NSObject
中还没有,就会报一个很经典的错误
unrecognized selector sent to instance
.
我们再改变一下,让改一下代码,让
Student
去执行
Person
的类方法:
[Student personClassMethod];
这次方法执行的查找顺序是怎样的呢?我就不画图了,其实跟[student personInstanceMethod]
差不多.首先Student
类对象先通过isa
找到Student 元类对象
,然后通过Student 元类对象
的superClass
找到父类的元类对象,也就是Person 元类对象
,然后在Person 元类对象
找到类方法执行.
关于isa 和 superClass
的关系,网上有人总结了一张图,可以很明显的展示,虚线表示 isa , 实现 表示 superClass:
我们参照这张图,对isa 和 superClass
做一个总结:
- instance 的 isa 指向 class , class 的 isa 指向 meta-class , meta-class 的 isa 指向基类的 meta-class.
- class 的 superClass 指向父类
如果没有父类, superClass 为 nil. - meta-class 的 superClass 指向父类的 meta-class
基类的 meta-class 的 superClass 指向基类的 class. - instance 调用方法的轨迹
通过 isa 找到 class ,如果方法不存在就通过 superClass 找父类 - class 调用类的方法轨迹
通过 isa 找到 meta-class,如果没有,通过 superClass 找到父类的 meta-class.
注意上图中有一根 superClass 线我用红色的⭕️标识出来了,这根线特别特殊:基类的元类对象的 superClass 指向基类.下面我们通过代码验证一下.
我们给NSObject
创建一个分类NSObject+test
,然后在分类的.m
方法中写一个实例方法,打印输出调用者:
- (void)test{
NSLog(@"NSObject 的对象方法test,调用者: %p",self);
}
然后在Person
类的头文件中声明一个+ (void)test;
方法,在让Person
调用这个方法,如下:
NSLog(@"Student 地址: %p",[Student class]);
NSLog(@"Person 地址: %p",[Person class]);
[Student test];
我们来分析一下,Person
的头文件中声明了一个test
方法,并没有在实现文件中实现,而NSObject+test
的.m
文件中实现了一个对象方法test
,如果我们执行[Student test];
会执行成功么?
运行一下看看:
2019-02-27 15:23:19.834089+0800 OC对象的分类_01[2112:300025] Student 地址: 0x100001478
2019-02-27 15:23:19.834469+0800 OC对象的分类_01[2112:300025] Person 地址: 0x100001428
2019-02-27 15:23:19.834505+0800 OC对象的分类_01[2112:300025] NSObject 的对象方法test,调用者: 0x100001478
会发现并没有报unrecognized selector sent to instance
这个错误,居然调用成功了!很奇怪,为什么我们使用Student
调用➕方法,最后却执行了NSObject
的➖方法呢?这就是我在上图中用红⭕️标识的哪条线的作用.
我们分析一下它的方法调用轨迹:
1: 首先Student
通过isa
指针找到Student
的meta-class
,在Student
的meta-class
中查找test
方法,结果没找到.
2: 又通过Student
的meta-class
中的superClass
找到他的父类的元类对象,也就是Person
的meta-class
,结果又没找到
3: 继续通过Person
的meta-class
中的superClass
找到NSObject
的meta-class
,结果还没找到
4: 最后NSObject
的meta-class
的superClass
指向了基类NSObject
,它又去NSObject
中查找,结果找到了- (void)test
,就执行了.
大家可能会疑惑,问什么调用的类方法,最后却执行了对象方法?
因为OC调用方法的本质就是发送消息,[Student test]
本质就是objc_msgSend(objc_getClass("Student"), sel_registerName("test"));
它只知道去执行test
,并不关心是加号方法还是减号方法.
我们一直在说实例对象的isa指针指向类对象,类对象的isa指针指向元类对象,也就是说实例对象的isa地址和类对象的地址相同,类对象的isa地址和元类的地址相同,我们来验证一下.
1: 分别创建Person
的实例对象,类对象,元类对象:
//实例对象
Person *person = [[Person alloc]init];
//类对象
Class personClass = [person class];
//元类对象
Class personMetaClass = object_getClass(personClass);
2: 通过命令行打印person->isa
和 personClass
地址
person->isa
地址是: 0x001d800100001429
personClass
地址是: 0x0000000100001428
咦~怎么不一样呢?
这里需要注意一下,在 arm64 位之前,实例对象的 isa 地址和类对象的地址就是相同的,但是 arm64 位之后,实例对象的 isa 地址需要按位与
ISA_MASK
后才能得到类对象的地址,我们在 object之isa指针详解篇幅中详细讲解过,有兴趣的同学可以看一下.
那
ISA_MASK
是什么呢?打开 runtime 源码搜索一下就能看到:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
如果是 arm64 位系统,ISA_MASK
就是0x0000000ffffffff8ULL
,如果是x86_64
系统ISA_MASK
就是0x00007ffffffffff8ULL
,因为我们是在mac 上运行,所以我们就使用x86_64
的:
可以发现,通过位运算后得到的地址
0x0000000100001428
和
personClass
地址:
0x0000000100001428
就是同一个地址!
下面验证 类对象的 isa 指针指向元类对象
我们打印
personClass 的 isa
,会发现报错,因为系统没有暴露
isa
这个成员:
为了解决这个问题,我们定义一个和
class
相同结构相同的
hh_objc_class
:
struct objc_class {
Class _Nonnull isa;
Class _Nullable super_class
}
然后把Class personClass
类型转换为struct hh_objc_class
类型:
struct hh_objc_class *hh_personClass = (__bridge struct hh_objc_class *)(personClass);
然后再打印:
p/x hh_personClass->isa
发现结果为:0x001d800100001401
p/x personMetaClass
的结果为:0x0000000100001400
同样在按位或运算:
按位或运算后的结果和
p/x personMetaClass
的结果:
0x0000000100001400
相同,也印证了我们之前的结论.
现在来验证一下类对象的 superClass 指向父类对象也就是说Student class
的superClass
地址和Person class
的地址相同.
还是之前代码,我们直接打印p/x hh_studentClass->super_class
和p/x personClass
,结果如图:
通过打印的结果可以看出,superClass
和isa
不同,superClass
并没有& ISA_MASK
,而是直接相等.
我们一直在说属性信息、协议信息、成员变量信息、对象方法存放在类对象中,类方法存放在元类对象中,但是一直没有亲眼看到,接下来我们就来验证一下,亲眼看看.
首先我们知道,类对象和元类对象的结构是一模一样的,因为他们都是class
类型.所以我们点击进入class
类型,发现它是这样:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
可以看到这个结构体中的确是有method_list
,ivar_list
,protocol_list
等等,完全符合我们之前的说法,但是请注意:#if !__OBJC2__
,也就是说这段代码的执行有一个前提不是objc2.0
才会执行,而现在我们用的都是OC2.0了,所以这段代码是不会执行的.也就是说这段代码已经过时了OBJC2_UNAVAILABLE
.所以我们需要从 runtime 的源码中查看.
打开 runtime 源码搜索struct objc_class
找到struct objc_class : objc_object {
这个结构体,我把主要成员挑出来,如下:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags 获取具体的类信息
class_rw_t *data() {
return bits.data();
}
}
我们主要研究class_rw_t
,rw_t
是read_write_table
的缩写,意思是可读可写的表.bits.data()
返回class_rw_t
.class_rw_t
里面存放的什么信息呢?我们点击进入class_rw_t
看看:
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;//方法信息
property_array_t properties;//属性信息
protocol_array_t protocols;//协议信息
}
可以看到我们想看的方法,属性,协议信息.只是还没看到成员变量信息. class_rw_t
中有一个class_ro_t
,ro_t
是readOnly_table
的缩写,意思是只读的表.我们点击进入const class_ro_t
看看里面存放哪些信息:
struct class_ro_t {
const char * name;//类名
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;//成员变量
}
我画了一张图,能一目了然展示Class
的内存结构:
这就是class
类型在 runtime 中的源码,到这里我们已经看到了所有我们想到看的信息,但是这只是在源码上证明了结论,最好我们能把代码跑起来,在内存中看看是否是这样,要想做到这种效果,就要借助李哥用 c++ 仿写的一个class
类型:MJClassInfo.h
文件.
步骤:
1: 导入MJClassInfo.h
文件到我们工程中,这是编译会出错,我们把main.m
后缀改为main.mm
,让编译器编译OC 和 C++ 文件.
2: 创建Student class
和Person class
然后转换为mj_objc_class
类型
mj_objc_class *studentClass = (__bridge mj_objc_class *)[Student class];
mj_objc_class *personClass = (__bridge mj_objc_class *)[Person class];
3: 调用mj_objc_class
的data()
方法,返回class_rw_t
,这是李哥封装的方法,在data()
内部调用bits.data()
和runtime源码实现一致.
class_rw_t *studentClass_rw_t_Data = studentClass->data();
class_rw_t *personClass_rw_t_Data = personClass->data();
4:打断点,查看class_rw_t
内存数据:
这样我们也从内存上查验了
class
的内存结构,meta-class的查验方法和
class
一样,我就不写步骤了,直接上图大家撸一眼:
大总结:
1: 对象的 isa 指针指向哪里?
- instance 对象的 isa 指针指向 class 对象
- class 对象的 isa 指针指向 meta-class对象
- meta-class 对象的 isa 指向基类的 meta-class 对象
2: OC的类信息存放在哪里?
- 属性信息,协议信息,成员变量信息,对象方法存放在类对象中.
- 类方法存放在 meta-class 对象中.
- 成员变量的具体指存放在 instance 对象.
3: 类对象和元类对象什么时候加载?什么时候释放?
- 当程序执行
main
函数后就会加载类信息,此时加载到内存; - 程序运行过程中一直存在,直到程序退出时销毁.
4: 实例对象中的成员变量和类对象中的成员变量有什么不同?
- 实例对象中存储的成员变量的具体值
- 类对象中的存储的成员变量的类型,名字等信息,只存储一份