对于一个iOS开发者来说对象是最熟悉不过的,因为我们开发的时候时刻都是在操作各种对象,而且都知道对象是通过了类的初始化创建出来的,那么问题来了,我们都知道类是通过继承实现方法以及属性和协议的传递,那么底层原理是怎么实现的呢?那么带着对未知的好奇心和求知欲我们一起探索类的原理,今天主要探索isa
。
知识补充:isa
我们都知道isa
是个指针,那么他是一个什么样的指针呢?我们先创建一个NSObject
对象,然后按住command
键+鼠标右键点击,我们就可以进入类定义的地方,我们能够看到如下代码:
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
在NSObject
中有一个成员变量isa
,而且是Class
类型,然后我们继续看这个Class
类型的时候会发现进不去了,我们看不到了。
引用某人的口头禅好烦
,那么接下来就上大招了,objc源码工程,我们从源码工程中进行搜索Class
。
貌似有点多啊,但是经过我的不断查找(加上猜测),所以最终锁定这么一行代码:
typedef struct objc_class *Class;
瞬间明白为啥经常要说isa
指针了,这不就是结构体指针嘛。然后再次引出来一个问题,我们都知道类继承就是isa
幕后操手,那么继续看下这个objc_class
结构体的构成,欲知后事如何,请听下回分析。
知识补充:objc_class
对于结构体我们都不陌生,毕竟开发中也会经常遇到,那么这节我们一起看下objc_class
结构体,继续在objc源码工程中进行全局搜索struct objc_class
,(⊙o⊙)…貌似和我想的不一样,咋这么多:
机智如我,经过这么一观察发现,一个是objc-runtime-new.h
、一个是objc-runtime-old.h
,那么直接放弃objc-runtime-old.h
因为我们现在用的是新版OBJC2
,然后再去看下runtime.h
中objc_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;
结构体最后有一个标记OBJC2_UNAVAILABLE
意思就是告诉我们在OBJC2
中不让用,那么我们就只需要去看objc-runtime-new.h
中的声明了,愉快的打开objc-runtime-new.h
,然后......
这个结构体出乎意料了,好长,然后大概瞅了那么一圈,嘿,貌似不长,都是函数我们需要的是了解它的成员有那些,所以经过简化后,结果如下:
struct objc_class : objc_object {
// Class ISA; 这块系统注释的原因是告诉我们,这个结构体还有一个影藏的成员```ISA```
Class superclass; //父类
cache_t cache; // 用来缓存指针和虚函数表
class_data_bits_t bits; // 存储类的信息的,包括成员列表、属性列表、方法列表、协议列表等等
}
然后到这块基本上已经了解了isa
指针的本质objc_class
结构体,那么顺带我们瞄一眼objc_object
,发现里面就一个成员isa_t isa;
,至于isa_t
的结构体的介绍,在我的对象的本质这篇博客中有详细介绍,在这里就不多做解释了,感兴趣可以去看下。
到这里前面的知识补充就完成了,也就是开胃小菜吃完了,那么开始今天的重点,类的继承链也即是我们经常能够看到的isa
走位以及通过源码搭配lldb
去分析查看类的结构。
类的superClass
走位
1、首先我们看下类的继承链,我们打开Xcode
->command+shift+N
->macOS
->Command Line Tool
然后起个霸气的名字。
2、然后创建一个基于NSObject
的类KGPerson
,然后添加如下几个属性、成员变量、对象方法以及类方法:
3、然后基于KGPerson
创建一个KGTeacher
类。
4、然后在main.m
中引入KGPerson
和KGTeacher
的头文件。
5、声明并实现一个函数void kgTestSuperClass(void)
,然后在main
函数中调用,具体代码如下:
然后我们运行程序,最后打印结果如下:
image.png6、到此那么我们可以得出如下一个集成链的isa
走位图:
类的isa
走位
1、在基于上一过程中创建的项目以及代码基础上我们在main
函数中添加一个函数,如下:
2、运行项目后得到的结果如下图所示:
image.png3、分析输出的结果,我们可以得到如下的isa
走位图:
4、然后我们再看下superClass
的isa
走位图,继续修改main.m
代码如下:
5、运行结果如下:
image.png6、通过打印结果,我们再次得到这样一个元类的superClass
走位图:
7、我们继续探索,元类的父类的isa
是如何走的呢?下面继续修改main.m
中的代码,如下:
8、代码运行结果如下:
image.png9、最后结合之前的superClass
走位图以及isa
走位图,我们最后能够证明得到这个全球通用的图:
10、分析完成类的继承链以及元类的继承链后,回到开始的时候,我们的好奇心提出的疑问,下面我们一起通过源码搭配lldb
去剖析类的原理。
类的属性
1、我们先前在KGPerson
中创建了两个属性、一个成员变量、一个类方法、一个对象方法,下面我们先去看下类的属性是存储在哪里的。
2、我们前面以及探索了isa
的结构,那么我们通过它的结构去查看存储的类信息,既然要看属性,那么我们必定是需要访问bits
了它在类结构体中定义是这样的:class_data_bits_t bits;
。
3、我们需要访问的数据存储在bits
里面,但是我们只能得到类的地址,也就是isa
的地址,那么我们怎么去访问后面的元素呢?请看下面一个简单的示例:
然后输出如下:
image.png4、从上面小测试我们就可以看到,我们可以通过指针地址的偏移去访问地址指向的存储空间中的值,那么我们接下来看下,我们想要访问bits
中的内容,我们需要将指针移动多少位?我们前面看了objc_class
中bits
前面有三个成员ISA
、superclass
、cache
,然后isa
是一个结构体指针占8
位我们都知道,然后superclass
是个Class
类型,我们查看isa
的时候就发现了Class
也是个结构体指针,那就是说superclasss
也是占8
位,然后去看cache
时发现这是个结构体,而且结构体中有两个成员,一个是_bucketsAndMaybeMask
它的大小看uintptr_t
的大小,uintptr_t
又是一个无符号长整型
数据,它在32
位系统下是4
字节,在64
位系统下是8
字节,我们现在都是在64
位系统下进行的,所以它是8
字节;另一个是联合体
,然后联合体里面又是一个结构体和_originalPreoptCache
,我们都知道联合体成员之间是互斥的,所以我们不看结构体,直接看_originalPreoptCache
的大小,explicit_atomic<preopt_cache_t *> _originalPreoptCache;
我们可以看到它的类型收到preopt_cache_t *
的影响,然后preopt_cache_t *
是个结构体指针,那么它的大小是8
字节,所以得到如下结论:
5、所以我们如果想要访问bits
需要进行地址偏移32
位,地址是采用十六进制
所以我们需要在原地址基础上加上0x20
才可以访问到bits
,那么我们先通过lldb
测试下是否正确。
[图片上传失败...(image-c00025-1624257325729)]
6、从图上可以看出我们的猜测是完全正确的,确实偏移32
位后能够访问到bits
。然后我们继续分析class_data_bits_t
结构体,发现里面有个data()
方法,然后返回值是个class_rw_t*
的指针,具体代码如下:
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
7、那么我们去看下这个class_rw_t
结构体,发现好多方法和成员,但是滑动到最后的时候看到了几个熟悉的单词如下:
8、那么我们能否猜测,这几个方法就是读取属性、方法、协议方法的呢?带着好奇,我们先去看下properties
,具体操作是在上次的基础上进行,如下:
9、通过以上步骤我们验证了一个类的属性存储在bits
中,我们可以通过以下流程来获取类的属性:
10、到这里属性在类中存储的位置已经找到了,那么接下来我们再去分析下成员变量存储的位置。
类的对象方法
1、上面两节我们探索了类存储属性以及成员变量的位置,那么这节开始探索类的对象方法的存储位置,前面介绍属性存储位置的时候,我们是通过properties()
来获取的,同时我们找的时候也看到了有个methods()
函数,那么我们可以做个大胆的猜测,是否这个函数就是获取方法列表的呢?下面我们通过lldb
在源码工程中进行调试验证一下。
[图片上传失败...(image-4b7e-1624257459334)]
2、通过上述验证我们可以确定properties()
中确实存储的方法,具体存储了哪些呢?下面我们继续读取:
为啥不是我想象的那种样,出现我们熟悉的方法名呢?瞬间感觉好像在哪忽略了什么,于是我们再次回到class_rw_t
结构体,然后顺着methods
找到了method_array_t
然后继续查找到了method_t
结构体,看到如下内容:
发现我们需要的东西在big
这个结构体内,然后我们继续往下查找,发现如下方法:
3、我们通过以上的查找得到了查找流程,那么我们继续去验证一下,通过lldb
调试后,结果如下:
[图片上传失败...(image-729895-1624257474342)]
如上图我们能够看到name
和homeTown
的setter
和getter
方法,但是没有看到我们的类方法和对象方法,我猜测是否是因为没有实现导致呢?然后我们去实现方法,再次运行后发现只有对象方法没有类方法,结果如下:
4、通过打印发现对象方法是有了,但是没有类方法,那么类方法存储在哪里呢?请听下回分析。
总结
就俩字欧耶
网友评论