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中对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,可以理解成根元类。关于实例对象、类、元类之间的关系,苹果官方给了一张图,非常清晰的表明了三者的关系,如下
imageisa结构体定义
了解了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位,其内存分布如下:
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内部的结构体中,看一下对哪些变量产生了影响:
image可以看到将nonpointer赋值为1;将magci赋值为110111;其他的仍然都是0。所以说只赋值了isa.magci和isa.nonpointer。
nonpointer
在文章开头也提到了,在Objective-C语言中,类也是对象,且每个对象都包含一个isa指针,现在改为了isa结构体。nonpointer作用就是区分这两者。
- 如果nonpointer为1,代表不是isa指针,而是isa结构体。虽然不是isa指针,但是通过isa结构体仍然能获得类指针(下面会分析)。
- 如果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 <Foundation/Foundation.h>
#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,还有一些其他位没有介绍,这里简单了解一下。
- has_assoc: 表示对象是否含有关联引用(associatedObject)
- weakly_referenced: 表示对象是否含有弱引用对象
- deallocating: 表示对象是否正在释放
- has_sidetable_rc: 表示对象的引用计数是否太大,如果太大,则需要用其他的数据结构来存
- 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结构体,应该不是什么难事。