Runtime中的|Runtime中的 isa 结构体

原文链接
有一定经验的iOS开发者,或多或少的都听过Runtime。Runtime,也就是运行时,是Objective-C语言的特性之一。日常开发中,可能直接和Runtime打交道的机会不多。然而,"发消息"、"消息转发"这些名词开发者应该经常听到,这些名词所用到的技术基础就是Runtime。了解Runtime,有助于开发者深入理解Objective-C这门语言。
在具体了解Runtime之前,先提一个问题,什么是动态语言?
Objective-C是一门动态语言
使用Objective-C做iOS开发的同学一定都听说过一句话:Objective-C是一门动态语言。动态语言,肯定是和静态语言相对应的。那么,静态语言有哪些特性,动态语言又有哪些特性?
回顾一下大学时期,学的第一门语言C语言,学习C语言的过程中从来没听说过运行时,也没听说过什么静态语言,动态语言。因此我们有理由相信,C语言是一门静态语言。
事实上也确实如此,C语言是一门静态语言,Objective-C是一门动态语言。然而,还是说不出静态语言和动态语言到底有什么区别……
静态语言和动态语言 静态语言,可以理解成在编译期间就确定一切的语言。以C语言来举例,C语言编译后会成为一个可执行文件。假设我们在C代码中写了一个hello函数,并且在主程序中调用了这个hello函数。倘若在编译期间,hello函数的入口地址相对于主程序入口地址的偏移量是0x0000abcdef(不要在意这个值,只是用来举例),那么在执行该程序时,执行到hello函数时,一定执行的是相对主程序入口地址偏移量为0x0000abcdef的代码块。也就是说,静态语言,在编译期间就已经确定一切,运行期间只是遵守编译期确定的指令在执行。
作为对比,再看一下动态语言,以经常用到的Objective-C为例。假设在Objective-C中写了hello方法,并且在主程序中调用了hello方法,也就是发送hello消息。在编译期间,只能确定要向某个对象发送hello消息,但是具体执行哪个内存块的代码是不确定的,具体执行的代码需要在运行期间才能确定。
到这里,静态语言和动态语言的区别已经很明显了。静态语言在编译期间就已经确定一切,而动态语言编译期间只能确定一部分,还有一部分需要在运行期间才能确定。也就是说,动态语言成为一个可执行程序并能够正确的执行,除了需要一个编译器外,还需要一套运行时系统,用于确定到底执行哪一块代码。Objective-C中的运行时系统内就是Runtime。
Runtime源码
Runtime源码是一套用C语言实现的API,整套代码是开源的,可以从苹果开源网站上下载Runtime源码。默认下载的Runtime源码是不能编译的,通过修改配置和导入必要的头文件,可以编译成功Runtime源码。我在github上放了编译成功的Runtime源码,且有我在看Runtime源码时的一些注释,本篇文章中的代码也是基于此Runtime源码。
由于Runtime源码代码量比较大,一篇文章介绍完Runtime源码是不可能的。因此这篇文章主要介绍Runtime中的isa结构体,作为Runtime的入门。
isa结构体
有经验的iOS开发者可能都听过一句话:在Objective-C语言中,类也是对象,且每个对象都包含一个isa指针,isa指针指向该对象所属的类。不过现在Runtime中的对象定义已经不是这样了,现在使用的是isa_t类型的结构体。每一个对象都有一个isa_t类型的结构体isa。之前的isa指针作用是指向该对象的类,那么isa结构体作为isa指针的替代者,是如何完成这个功能的呢?
在解决这个问题之前,我们先来看一下Runtime源码中对象和类的定义。
objc_object 【Runtime中的|Runtime中的 isa 结构体】看一下Runtime中对id类型的定义

typedef struct objc_object *id;

这里的id也就是Objective-C中的id类型,代表任意对象,类似于C语言中的 void 。可以看到,id实际上是一个指向结构体objc_object的指针。
再来看一下objc_object的定义,该定义位于objc-private.h文件中:
struct objc_object { // isa结构体 private: isa_t isa; }

结构体中还包含一些public的方法。可以看到,对象结构体(objc_object)中的第一个变量就是isa_t 类型的isa。关于isa_t具体是什么,后续再介绍。
Objective-C语言中最主要的就是对象和类,看完了对象在Runtime中的定义,再看一下类在Runtime中的定义。
objc_class Runtime中对于Class的定义
typedef struct objc_class *Class;

Class实际上是一个指向objc_class结构体的指针。
看一下结构体objc_class的定义,objc_class的定义位于objc-runtime-new.h文件中
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 }

结构体中还包含一些方法。
注意,objc_class是继承于objc_object的,因此objc_class中也包含isa_t类型的isa。objc_class的定义可以理解成下面这样:
struct objc_class { isa_t isa; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags }

isa的作用 上面也提到了,isa能够使该对象找到自己所属的类。为什么对象需要知道自己所属的类呢?这主要是因为对象的方法是存储在该对象所属的类中的。
这一点是很容易理解的,一个类可以有多个对象,倘若每个对象都含有自己能够执行的方法,那对于内存来说是灾难级的。
在向对象发送消息,也就是实例方法被调用时,对象通过自己的isa找到所属的类,然后在类的结构中找到对应方法的实现(关于在类结构中如何找到方法的实现,后续的文章再介绍)。
我们知道,Objective-C中区分类方法和实例方法。实例方法是如何找到的我们了解了,那么类方法是如何找到的呢?类结构体中也有isa,类对象的isa指向哪里呢?
元类(metaClass) 为了解决类方法调用,Objective-C引入了元类(metaClass),类对象的isa指向该类的元类,一个类对象对应一个元类对象。
元类对象也是类对象,既然是类对象,那么元类对象中也有isa,那么元类的isa又指向哪里呢?总不能指向元元类吧……这样是无穷无尽的。
Objective-C语言的设计者已经考虑到了这个问题,所有元类的isa都指向一个元类对象,该元类对象就是 meta Root Class,可以理解成根元类。关于实例对象、类、元类之间的关系,苹果官方给了一张图,非常清晰的表明了三者的关系,如下
Runtime中的|Runtime中的 isa 结构体
文章图片
image isa结构体定义 了解了isa的作用,现在来看一下isa的定义。isa是isa_t类型,isa_t也是一个结构体,其定义在objc-private.h中:
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { }Class cls; // 相当于是unsigned long bits; uintptr_t bits; #if defined(ISA_BITFIELD) struct { ISA_BITFIELD; // defined in isa.h }; #endif };

ISA_BITFIELD的定义在 isa.h文件中:
uintptr_t nonpointer: 1; \ uintptr_t has_assoc: 1; \ uintptr_t has_cxx_dtor: 1; \ uintptr_t shiftcls: 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \ uintptr_t magic: 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t deallocating: 1; \ uintptr_t has_sidetable_rc: 1; \ uintptr_t extra_rc: 8

注意:这里的代码都是x86_64架构下的,arm64架构下和x86_64架构下有区别,但是不影响我们理解isa_t结构体。
将isa_t结构体中的ISA_BITFIELD使用isa.h文件中的ISA_BITFIELD替换,isa_t的定义可以表示如下:
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { }Class cls; // 相当于是unsigned long bits; uintptr_t bits; #if defined(ISA_BITFIELD) struct { uintptr_t nonpointer: 1; uintptr_t has_assoc: 1; uintptr_t has_cxx_dtor: 1; uintptr_t shiftcls: 44; uintptr_t magic: 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating: 1; uintptr_t has_sidetable_rc: 1; uintptr_t extra_rc: 8; }; #endif };

注意isa_t是联合体,也就是说isa_t中的变量,cls、bits和内部的结构体全都位于同一块地址空间。
本篇文章主要分析下isa_t中内部结构体中各个变量的作用
struct { uintptr_t nonpointer: 1; uintptr_t has_assoc: 1; uintptr_t has_cxx_dtor: 1; uintptr_t shiftcls: 44; uintptr_t magic: 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating: 1; uintptr_t has_sidetable_rc: 1; uintptr_t extra_rc: 8; };

该结构体共占64位,其内存分布如下:
Runtime中的|Runtime中的 isa 结构体
文章图片
image 在了解内个结构体各个变量的作用前,先通过Runtime代码看一下isa结构体是如何初始化的。
isa结构体初始化 isa结构体初始化定义在objc_object结构体中,看一下官方提供的函数和注释:
// initIsa() should be used to init the isa of new objects only. // If this object already has an isa, use changeIsa() for correctness. // initInstanceIsa(): objects with no custom RR/AWZ // initClassIsa(): class objects // initProtocolIsa(): protocol objects // initIsa(): other objects void initIsa(Class cls /*nonpointer=false*/); void initClassIsa(Class cls /*nonpointer=maybe*/); void initProtocolIsa(Class cls /*nonpointer=maybe*/); void initInstanceIsa(Class cls, bool hasCxxDtor);

官方提供的有类对象初始化isa,协议对象初始化isa,实例对象初始化isa,其他对象初始化isa,分别对应不同的函数。
看下每个函数的实现:
inline void objc_object::initIsa(Class cls) { initIsa(cls, false, false); }inline void objc_object::initClassIsa(Class cls) { if (DisableNonpointerIsa||cls->instancesRequireRawIsa()) { initIsa(cls, false/*not nonpointer*/, false); } else { initIsa(cls, true/*nonpointer*/, false); } }inline void objc_object::initProtocolIsa(Class cls) { return initClassIsa(cls); }inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) { assert(!cls->instancesRequireRawIsa()); assert(hasCxxDtor == cls->hasCxxDtor()); initIsa(cls, true, hasCxxDtor); }

可以看到,无论是类对象,实例对象,协议对象,还是其他对象,初始化isa结构体最终都调用了
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)

函数,只是所传的参数不同而已。
最终调用的initIsa函数的代码,经过简化后如下:
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) { if (!nonpointer) { isa.cls = cls; } else { // 实例对象的isa初始化直接走else分之 // 初始化一个心得isa_t结构体 isa_t newisa(0); // 对新结构体newisa赋值 // ISA_MAGIC_VALUE的值是0x001d800000000001ULL,转化成二进制是64位 // 根据注释,使用ISA_MAGIC_VALUE赋值,实际上只是赋值了isa.magic和isa.nonpointer newisa.bits = ISA_MAGIC_VALUE; newisa.has_cxx_dtor = hasCxxDtor; // 将当前对象的类指针赋值到shiftcls // 类的指针是按照字节(8bits)对齐的,其指针后三位都是没有意义的0,因此可以右移3位 newisa.shiftcls = (uintptr_t)cls >> 3; // 赋值。看注释这个地方不是线程安全的?? isa = newisa; } }

初始化实例对象的isa时,传入的nonpointer参数是true,所以直接走了else分之。在else分之中,对isa的bits分之赋值ISA_MAGIC_VALUE。根据注释,这样代码实际上只是对isa中的magic和nonpointer进行了赋值,来看一下为什么。
ISA_MAGIC_VALUE的值是0x001d800000000001ULL,转化成二进制就是0000 0000 0001 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001,将每一位对应到isa内部的结构体中,看一下对哪些变量产生了影响:
Runtime中的|Runtime中的 isa 结构体
文章图片
image 可以看到将nonpointer赋值为1;将magci赋值为110111;其他的仍然都是0。所以说只赋值了isa.magci和isa.nonpointer。
nonpointer 在文章开头也提到了,在Objective-C语言中,类也是对象,且每个对象都包含一个isa指针,现在改为了isa结构体。nonpointer作用就是区分这两者。
  1. 如果nonpointer为1,代表不是isa指针,而是isa结构体。虽然不是isa指针,但是通过isa结构体仍然能获得类指针(下面会分析)。
  2. 如果nonpointer为0,代表当前是isa指针,访问对象的isa会直接返回类指针。
magic magic的值调试器会用到,调试器根据magci的值判断当前对象已经初始过了,还是尚未初始化的空间。
has_cxx_dtor 接下来就是对has_cxx_dtor进行赋值。has_cxx_dtor表示当前对象是否有C++的析构函数(destructor),如果没有,释放时会快速的释放内存。
shiftcls 在函数
inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)

中,参数cls就是类的指针。而
newisa.shiftcls = (uintptr_t)cls >> 3;

shiftcls存储的到底是什么呢?
实际上,shiftcls存储的就是当前对象类的指针。之所以右移三位是出于节省空间上的考虑。
在Objective-C中,类的指针是按照字节(8 bits)对齐的,也就是说类指针地址转化成十进制后,都是8的倍数,也就是说,类指针地址转化成二进制后,后三位都是0。既然是没有意义的0,那么在存储时就可以省略,用节省下来的空间存储一些其他信息。
在objc-runtime-new.mm文件的
static __attribute__((always_inline)) id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct = true, size_t *outAllocatedSize = nil)

函数,类初始化时会调用该函数。可以在该函数中打印类对象的地址
if (!cls) return nil; // 这里可以打印类指针的地址,类指针地址最后一位是十六进制的8或者0,说明 // 类指针地址后三位都是0 printf("cls address = %p\n",cls);

打印出的部分信息如下:
cls address = 0x7fff83bca218 cls address = 0x7fff83bcab28 cls address = 0x7fff83bc5290 cls address = 0x7fff83717f58 cls address = 0x7fff83717f58 cls address = 0x100b15140 cls address = 0x7fff83717fa8 cls address = 0x7fff837164c8 cls address = 0x7fff837164c8 cls address = 0x7fff83716e78 cls address = 0x100b15140 cls address = 0x7fff837175a8 cls address = 0x7fff837175a8 cls address = 0x7fff83717fa8

可以看到类对象的地址最后一位都是8或者0,说明类对象确实是按照字节对齐,后三位都是0。因此在赋值shiftcls时,右移三位是安全的,不会丢失类指针信息。
我们可以写代码验证一下对象的isa和类对象指针的关系。代码如下:
#import #import "objc-runtime.h"// 把一个十进制的数转为二进制 NSString * binaryWithInteger(NSUInteger decInt){ NSString *string = @""; NSUInteger x = decInt; while(x > 0){ string = [[NSString stringWithFormat:@"%lu",x&1] stringByAppendingString:string]; x = x >> 1; } return string; }int main(int argc, const char * argv[]) { @autoreleasepool { // 把对象转为objc_object结构体 struct objc_object *object = (__bridge struct objc_object *)([NSObject new]); NSLog(@"binary = %@",binaryWithInteger(object->isa)); // uintptr_t实际上就是unsigned long NSLog(@"binary = %@",binaryWithInteger((uintptr_t)[NSObject class])); } return 0; }

打印出isa的内容是:1011101100000000000000100000000101100010101000101000001,NSObject类对象的指针是:100000000101100010101000101000000。首先将isa的内容补充至64位
0000 0101 1101 1000 0000 0000 0001 0000 0000 1011 0001 0101 0001 0100 0001

取第4位到第47位之间的内容,也就是shiftcls的值:
000 0000 0000 0001 0000 0000 1011 0001 0101 0001 0100 0

将类对象的指针右移三位,即去除后三位的0,得到
100000000101100010101000101000

和上面的shiftcls对比:
10 0000 0001 0110 0010 1010 0010 1000 0000 0000 0000 0010 0000 0001 0110 0010 1010 0010 1000

可以确认:shiftcls中的确包含了类对象的指针。
其他位 上面已经介绍了nonpointer、magic、shiftcls、has_cxx_dtor,还有一些其他位没有介绍,这里简单了解一下。
  1. has_assoc: 表示对象是否含有关联引用(associatedObject)
  2. weakly_referenced: 表示对象是否含有弱引用对象
  3. deallocating: 表示对象是否正在释放
  4. has_sidetable_rc: 表示对象的引用计数是否太大,如果太大,则需要用其他的数据结构来存
  5. extra_rc:对象的引用计数大于1,则会将引用计数的个数存到extra_rc里面。比如对象的引用计数为5,则extra_rc的值为4。
extra_rc和has_sidetable_c可以一起理解。extra_rc用于存放引用计数的个数,extra_rc占8位,也就是最大表示255,当对象的引用计数个数超过257时,has_sidetable_rc的值应该为1。
总结
至此,isa结构体的介绍就完了。需要提醒的是,上面的代码是运行在macOS上,也就是x86_64架构上的,isa结构体也是基于x86_64架构的。在arm64架构上,isa结构体中变量所占用的位数和x86_64架构是不一样的,但是表示的含义是一样的。理解了x86_64架构下的isa结构体,相信对于理解arm架构下的isa结构体,应该不是什么难事。
参考文章
从 NSObject 的初始化了解 isa

    推荐阅读