原文连接git
有必定经验的iOS开发者,或多或少的都听过Runtime。Runtime,也就是运行时,是Objective-C语言的特性之一。平常开发中,可能直接和Runtime打交道的机会很少。然而,"发消息"、"消息转发"这些名词开发者应该常常听到,这些名词所用到的技术基础就是Runtime。了解Runtime,有助于开发者深刻理解Objective-C这门语言。github
在具体了解Runtime以前,先提一个问题,什么是动态语言?安全
使用Objective-C作iOS开发的同窗必定都据说过一句话:Objective-C是一门动态语言。动态语言,确定是和静态语言相对应的。那么,静态语言有哪些特性,动态语言又有哪些特性?bash
回顾一下大学时期,学的第一门语言C语言,学习C语言的过程当中历来没据说过运行时,也没据说过什么静态语言,动态语言。所以咱们有理由相信,C语言是一门静态语言。数据结构
事实上也确实如此,C语言是一门静态语言,Objective-C是一门动态语言。然而,仍是说不出静态语言和动态语言到底有什么区别……架构
静态语言,能够理解成在编译期间就肯定一切的语言。以C语言来举例,C语言编译后会成为一个可执行文件。假设咱们在C代码中写了一个hello函数,而且在主程序中调用了这个hello函数。假若在编译期间,hello函数的入口地址相对于主程序入口地址的偏移量是0x0000abcdef(不要在乎这个值,只是用来举例),那么在执行该程序时,执行到hello函数时,必定执行的是相对主程序入口地址偏移量为0x0000abcdef的代码块。也就是说,静态语言,在编译期间就已经肯定一切,运行期间只是遵照编译期肯定的指令在执行。app
做为对比,再看一下动态语言,以常常用到的Objective-C为例。假设在Objective-C中写了hello方法,而且在主程序中调用了hello方法,也就是发送hello消息。在编译期间,只能肯定要向某个对象发送hello消息,可是具体执行哪一个内存块的代码是不肯定的,具体执行的代码须要在运行期间才能肯定。ide
到这里,静态语言和动态语言的区别已经很明显了。静态语言在编译期间就已经肯定一切,而动态语言编译期间只能肯定一部分,还有一部分须要在运行期间才能肯定。也就是说,动态语言成为一个可执行程序并可以正确的执行,除了须要一个编译器外,还须要一套运行时系统,用于肯定到底执行哪一块代码。Objective-C中的运行时系统内就是Runtime。函数
Runtime源码是一套用C语言实现的API,整套代码是开源的,能够从苹果开源网站上下载Runtime源码。默认下载的Runtime源码是不能编译的,经过修改配置和导入必要的头文件,能够编译成功Runtime源码。我在github上放了编译成功的Runtime源码,且有我在看Runtime源码时的一些注释,本篇文章中的代码也是基于此Runtime源码。学习
因为Runtime源码代码量比较大,一篇文章介绍完Runtime源码是不可能的。所以这篇文章主要介绍Runtime中的isa结构体,做为Runtime的入门。
有经验的iOS开发者可能都听过一句话:在Objective-C语言中,类也是对象,且每一个对象都包含一个isa指针,isa指针指向该对象所属的类。不过如今Runtime中的对象定义已经不是这样了,如今使用的是isa_t类型的结构体。每个对象都有一个isa_t类型的结构体isa。以前的isa指针做用是指向该对象的类,那么isa结构体做为isa指针的替代者,是如何完成这个功能的呢?
在解决这个问题以前,咱们先来看一下Runtime源码中对象和类的定义。
看一下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中的定义。
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找到所属的类,而后在类的结构中找到对应方法的实现(关于在类结构中如何找到方法的实现,后续的文章再介绍)。
咱们知道,Objective-C中区分类方法和实例方法。实例方法是如何找到的咱们了解了,那么类方法是如何找到的呢?类结构体中也有isa,类对象的isa指向哪里呢?
为了解决类方法调用,Objective-C引入了元类(metaClass),类对象的isa指向该类的元类,一个类对象对应一个元类对象。
元类对象也是类对象,既然是类对象,那么元类对象中也有isa,那么元类的isa又指向哪里呢?总不能指向元元类吧……这样是无穷无尽的。
Objective-C语言的设计者已经考虑到了这个问题,全部元类的isa都指向一个元类对象,该元类对象就是 meta Root Class,能够理解成根元类。关于实例对象、类、元类之间的关系,苹果官方给了一张图,很是清晰的代表了三者的关系,以下
了解了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代码看一下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内部的结构体中,看一下对哪些变量产生了影响:
能够看到将nonpointer赋值为1;将magci赋值为110111;其余的仍然都是0。因此说只赋值了isa.magci和isa.nonpointer。
在文章开头也提到了,在Objective-C语言中,类也是对象,且每一个对象都包含一个isa指针,如今改成了isa结构体。nonpointer做用就是区分这二者。
magic的值调试器会用到,调试器根据magci的值判断当前对象已经初始过了,仍是还没有初始化的空间。
接下来就是对has_cxx_dtor进行赋值。has_cxx_dtor表示当前对象是否有C++的析构函数(destructor),若是没有,释放时会快速的释放内存。
在函数
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,还有一些其余位没有介绍,这里简单了解一下。
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结构体,应该不是什么难事。