-
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 <NSCopying>
{
int _age;
int _no;
int _height;
}
-(void)personInstanceMethod;
+(void)personClassMethod;
@end
@implementation Person
- (void)personInstanceMethod{
}
+ (void)personClassMethod{
}
@end
// Student
@interface Student : Person <NSCopying>
{
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
地址是: 0x001d800100001429personClass
地址是: 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: 实例对象中的成员变量和类对象中的成员变量有什么不同?
- 实例对象中存储的成员变量的具体值
- 类对象中的存储的成员变量的类型,名字等信息,只存储一份