<简书 — 刘小壮> https://www.jianshu.com/p/5b7e7c8075efgit
在OC1.0
中,Runtime
不少定义都写在NSObject.h
文件中,若是以前研究过Runtime
的同窗能够应该见过下面的定义,定义了一些基础的信息。github
// 声明Class和id typedef struct objc_class *Class; typedef struct objc_object *id; // 声明经常使用变量 typedef struct objc_method *Method; typedef struct objc_ivar *Ivar; typedef struct objc_category *Category; typedef struct objc_property *objc_property_t; // objc_object和objc_class struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; }; struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE;
以前的Runtime
结构也比较简单,都是一些很直接的结构体定义,如今新版的Runtime
在操做的时候,各类地址偏移操做和位运算。swift
后来可能苹果也不太想让开发者知道Runtime
内部的实现,因此就把源码定义从NSObject
中搬到Runtime
中了。并且以前的定义也不用了,经过OBJC_TYPES_DEFINED
预编译指令,将以前的代码废弃调了。缓存
如今NSObject
中的定义很是简单,直接就是一个Class
类型的isa
变量,其余信息都隐藏起来了。架构
@interface NSObject <NSObject> { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-interface-ivars" Class isa OBJC_ISA_AVAILABILITY; #pragma clang diagnostic pop }
这是最新的一些经常使用Runtime
定义,和以前的定义也不太同样了,用了最新的结构体对象,以前的结构体也都废弃了。ide
typedef struct objc_class *Class; typedef struct objc_object *id; typedef struct method_t *Method; typedef struct ivar_t *Ivar; typedef struct category_t *Category; typedef struct property_t *objc_property_t;
在OC中每一个对象都是一个结构体,结构体中都包含一个isa的成员变量,其位于成员变量的第一位。isa
的成员变量以前都是Class
类型的,后来苹果将其改成isa_t
。函数
struct objc_object { private: isa_t isa; };
OC中的类和元类也是同样,都是结构体构成的。因为类的结构体定义继承自objc_object
,因此其也是一个对象,而且具备对象的isa
特征。oop
因此能够经过isa_t
来查找对应的类或元类,查找方法应该是经过uintptr_t
类型的bits
,经过按位操做来查找isa_t
指向的类的地址。布局
实例对象或类对象的方法,并不会定义在各个对象中,而是都定义在isa_t
指向的类中。查找到对应的类后,经过类的class_data_bits_t
类型的bits
结构体查找方法,对象、类、元类都是一样的查找原理。优化
isa_t
是一个union
的结构对象,union
相似于C++
结构体,其内部能够定义成员变量和函数。在isa_t
中定义了cls
、bits
、isa_t
三部分,下面的struct
结构体就是isa_t
的结构体构成。
下面对isa_t
中的结构体进行了位域声明,地址从nonpointer
起到extra_rc
结束,从低到高进行排列。位域也是对结构体内存布局进行了一个声明,经过下面的结构体成员变量能够直接操做某个地址。位域总共占8字节,全部的位域加在一块儿正好是64位。
小提示:union
中bits
能够操做整个内存区,而位域只能操做对应的位。
下面的代码是不完整代码,只保留了arm64
部分,其余部分被忽略掉了。
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; # if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL struct { uintptr_t nonpointer : 1; // 是32位仍是64位 uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用,若是没有关联引用,能够更快的释放对象 uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的析构函数 uintptr_t shiftcls : 33; // 对象指向类的内存地址,也就是isa指向的地址 uintptr_t magic : 6; // 对象是否初始化完成 uintptr_t weakly_referenced : 1; // 对象是否被弱引用或曾经被弱引用 uintptr_t deallocating : 1; // 对象是否被释放中 uintptr_t has_sidetable_rc : 1; // 对象引用计数太大,是否超出存储区域 uintptr_t extra_rc : 19; // 对象引用计数 # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18) }; # elif __x86_64__ // ···· # else // ···· # endif };
在ARM64
架构下,isa_t
以如下结构进行布局。在不一样的CPU
架构下,布局方式会有所不一样,但参数都是同样的。
在Runtime
中类也是一个对象,类的结构体objc_class
是继承自objc_object
的,具有对象全部的特征。在objc_class
中定义了三个成员变量,superclass
是一个objc_class
类型的指针,指向其父类的objc_class
结构体。cache
用来处理已调用方法的缓存。
bits
是objc_class
的主角,其内部只定义了一个uintptr_t
类型的bits
成员变量,存储了class_rw_t
的地址。bits
中还定义了一些基本操做,例如获取class_rw_t
、raw isa
状态、是否swift
等函数。objc_class
结构体中定义的一些函数,其内部都是经过bits
实现的。
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; class_data_bits_t bits; class_rw_t *data() { return bits.data(); } void setData(class_rw_t *newData) { bits.setData(newData); } // ..... }
从objc_class
的源码能够看出,能够经过bits
结构体的data()
函数,获取class_rw_t
指针。咱们进入源代码中看一下,能够看出是经过对uintptr_t
类型的bits
变量,作位运算查找对应的值。
class_rw_t* data() { return (class_rw_t *)(bits & FAST_DATA_MASK); }
uintptr_t
本质上是一个unsigned long
的typedef
,unsigned long
在64位处理器中占8字节,正好是64位二进制。经过FAST_DATA_MASK
转换为二进制后,是取bits
中的47-3的位置,正好是取出class_rw_t
指针。
在OC中一个指针的长度是47,例如打印一个UIViewController
的地址是0x7faf1b580450
,转换为二进制是11111111010111100011011010110000000010001010000
,最后面三位是占位的,因此在取地址的时候会忽略最后三位。
// 查找第0位,表示是否swift #define FAST_IS_SWIFT (1UL<<0) // 当前类或父类是否认义了retain、release等方法 #define FAST_HAS_DEFAULT_RR (1UL<<1) // 类或父类须要初始化isa #define FAST_REQUIRES_RAW_ISA (1UL<<2) // 数据段的指针 #define FAST_DATA_MASK 0x00007ffffffffff8UL // 11111111111111111111111111111111111111111111000 总共47位
由于在bits
中最后三位是没用的,因此能够用来存储一些其余信息。在class_data_bits_t
还定义了三个宏,用来对后三位作位运算。
和class_data_bits_t
相关的有两个很重要结构体,class_rw_t
和class_ro_t
,其中都定义着method list
、protocol list
、property list
等关键信息。
struct class_rw_t { uint32_t flags; uint32_t version; const class_ro_t *ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; Class firstSubclass; Class nextSiblingClass; char *demangledName; };
在编译后class_data_bits_t
指向的是一个class_ro_t
的地址,这个结构体是不可变的(只读)。在运行时,才会经过realizeClass
函数将bits
指向class_rw_t
。
struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; uint32_t reserved; const uint8_t * ivarLayout; const char * name; method_list_t * baseMethodList; protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; };
在程序开始运行后会初始化Class
,在这个过程当中,会把编译器存储在bits
中的class_ro_t
取出,而后建立class_rw_t
,并把ro
赋值给rw
,成为rw
的一个成员变量,最后把rw
设置给bits
,替代以前bits
中存储的ro
。除了这些操做外,还会有一些其余赋值的操做,下面是初始化Class
的精简版代码。
static Class realizeClass(Class cls) { const class_ro_t *ro; class_rw_t *rw; Class supercls; Class metacls; bool isMeta; if (!cls) return nil; if (cls->isRealized()) return cls; ro = (const class_ro_t *)cls->data(); rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); rw->ro = ro; rw->flags = RW_REALIZED|RW_REALIZING; cls->setData(rw); isMeta = ro->flags & RO_META; rw->version = isMeta ? 7 : 0; supercls = realizeClass(remapClass(cls->superclass)); metacls = realizeClass(remapClass(cls->ISA())) cls->superclass = supercls; cls->initClassIsa(metacls); cls->setInstanceSize(ro->instanceSize); if (supercls) { addSubclass(supercls, cls); } else { addRootClass(cls); } methodizeClass(cls); return cls; }
在上面的代码中咱们还发现了两个函数,addRootClass
和addSubclass
函数,这两个函数的职责是将某个类的子类串成一个列表,大体是下面的连接顺序。由此可知,咱们是能够经过class_rw_t
,获取到当前类的全部子类。
superClass.firstSubclass -> subClass1.nextSiblingClass -> subClass2.nextSiblingClass -> ...
初始化rw
和ro
以后,rw
的method list
、protocol list
、property list
都是空的,须要在下面methodizeClass
函数中进行赋值。函数中会把ro
的list
都取出来,而后赋值给rw
,若是在运行时动态修改,也是对rw
作的操做。因此ro
中存储的是编译时就已经决定的原数据,rw
才是运行时动态修改的数据。
static void methodizeClass(Class cls) { bool isMeta = cls->isMetaClass(); auto rw = cls->data(); auto ro = rw->ro; method_list_t *list = ro->baseMethods(); if (list) { prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls)); rw->methods.attachLists(&list, 1); } property_list_t *proplist = ro->baseProperties; if (proplist) { rw->properties.attachLists(&proplist, 1); } protocol_list_t *protolist = ro->baseProtocols; if (protolist) { rw->protocols.attachLists(&protolist, 1); } if (cls->isRootMetaclass()) { // root metaclass addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO); } // Attach categories. category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/); attachCategories(cls, cats, false /*don't flush caches*/); }
假设建立一个类LXZObject
,继承自NSObject
,并为其加入一个testMethod
方法,不作其余操做。由于在编译后objc_class
的bits
对应的是class_ro_t
结构体,因此咱们打印一下结构体的成员变量,看一下编译后的class_ro_t
是什么样的。
struct class_ro_t { flags = 128 instanceStart = 8 instanceSize = 8 reserved = 0 ivarLayout = 0x0000000000000000 <no value available> name = 0x0000000100000f7a "LXZObject" baseMethodList = 0x00000001000010c8 baseProtocols = 0x0000000000000000 ivars = 0x0000000000000000 weakIvarLayout = 0x0000000000000000 <no value available> baseProperties = 0x0000000000000000 }
通过打印能够看出,一个类的class_ro_t
中只会包含当前类的信息,不会包含其父类的信息,在LXZObject
类中只会包含name
和baseMethodList
两个字段,而baseMethodList
中只有一个testMethod
方法。由此可知,class_rw_t
结构体也是同样的。
下面是已经初始化后的isa_t
结构体的布局,以及各个结构体成员在结构体中的位置。
union
常常配合结构体使用,第一次使用union
就是对结构体区域作初始化。在对象初始化时,会对isa_t
的bits
字段赋值为ISA_MAGIC_VALUE
,这就是对union
联合体初始化的过程。
// 在objc-723中已经没有了 inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) { if (!indexed) { isa.cls = cls; } else { isa.bits = ISA_MAGIC_VALUE; isa.has_cxx_dtor = hasCxxDtor; isa.shiftcls = (uintptr_t)cls >> 3; } }
在对象经过initIsa()
函数初始化时,会经过ISA_MAGIC_VALUE
对isa
进行初始化。ISA_MAGIC_VALUE
是一个16进制的值,将其转换为二进制后,会发现ISA_MAGIC_VALUE
是对nonpointer
和magic
作初始化。
nonpointer
是对以前32位处理器的兼容。在访问对象所属的类时,若是是32位则返回以前的isa
指针地址,不然表示是64位处理器,则返回isa_t
结构体。
# define ISA_MAGIC_VALUE 0x000001a000000001ULL 二进制:11010000000000000000000000000000000000001 补全二进制:23个零+11010000000000000000000000000000000000001
随后会经过位域,对has_cxx_dtor
和shiftcls
作初始化,这时候就已经有四个字段被初始化了。has_cxx_dtor
表示是否有C++
或OC的析构方法,在打印方法列表时,常常能看到一个名为.cxx_destruct
的方法,就和这个字段有关系。
在计算机中为了对存储区(Memory or Disk)
读取方便,因此在写入和读取时,会对内存有对其操做。通常是以字节为单位进行对其,这样也是对读写速度的优化。在对shiftcls
进行赋值时,对Class
的指针进行了位移操做,向右位移三位。这是由于类指针为了内存对其,将最后三位用0填充,因此这三位是没有意义的。
isa结构体 0000000001011101100000000000000100000000001110101110000011111001 0x5d8001003ae0f8 类对象地址 100000000001110101110000011111000 0x1003ae0f8 将类对象地址右移三位为100000000001110101110000011111,正好符合isa_t地址中shiftcls的部分,前面不足补零。
外界获取Class
时,应该经过ISA()
函数,而不是像以前同样直接访问isa
指针。在ISA()
函数中,是对isa_t
的结构体作与运算,是经过ISA_MASK
宏进行的,转换为二进制的话,正好是把shiftcls
的地址取出来。
inline Class objc_object::ISA() { return (Class)(isa.bits & ISA_MASK); } #define ISA_MASK 0x0000000ffffffff8ULL 111111111111111111111111111111111000
从iPhone5s
开始,iOS
设备开始引入了64位处理器,以前的处理器一直都是32位的。
可是在64位处理器中,指针长度以及一些变量所占内存都发生了改变,32位一个指针占用4字节,但64位一个指针占用8字节;32位一个long
占用4字节,64位一个long
占用8字节等,因此在64位上内存占用会多出不少。
**苹果为了优化这个问题,推出了Tagged Pointer新特性。以前一个指针指向一个地址,而Tagged Pointer中一个指针就表明一个值,**以NSNumber为例。
NSNumber *number1 = @1; NSNumber *number2 = @3; NSNumber *number3 = @54; // 输出 (lldb) p number1 (__NSCFNumber *) $3 = 0xb000000000000012 (int)1 (lldb) p number2 (__NSCFNumber *) $4 = 0xb000000000000032 (int)3 (lldb) p number3 (__NSCFNumber *) $5 = 0xb000000000000362 (int)54
经过上面代码能够看出,使用了Tagged Pointer
新特性后,指针中就存储着对象的值。例如一个值为1的NSNumber
,指针就是0xb000000000000012
,若是抛去前面的0xb
和后面的2,中间正好就是16进制的值。
苹果经过Tagged Pointer
的特性,明显的提高了执行效率并节省了不少内存。在64位处理器下,内存占用减小了将近一半,执行效率也大大提高。因为经过指针来直接表示数值,因此没有了malloc
和free
的过程,对象的建立和销毁速度提高几十倍。
对于对象指针也是同样,在OC1.0
时代isa
是一个真的指针,指向一个堆区的地址。而OC2.0
时代,一个指针长度是八字节也就是64位,在64位中直接存储着对象的信息。当查找对象所属的类时,直接在isa
指针中进行位运算便可,并且因为是在栈区进行操做,查找速度是很是快的。
struct { uintptr_t nonpointer : 1; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1; uintptr_t shiftcls : 33; uintptr_t magic : 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 19; };
例如isa_t
本质上是一个结构体,若是建立结构体再用指针指向这个结构体,内存占用是很大的。可是Tagged Pointer
特性中,直接把结构体的值都存储到指针中,这就至关节省内存了。
苹果不容许直接访问isa
指针,和Tagged Pointer
也是有关系的。由于在Tagged Pointer
的状况下,isa
并非一个指针指向另外一块内存区,而是直接表示对象的值,因此经过直接访问isa
获取到的信息是错误的。
简书因为排版的问题,阅读体验并很差,布局、图片显示、代码等不少问题。因此建议到我Github
上,下载Runtime PDF
合集。把全部Runtime
文章总计九篇,都写在这个PDF
中,并且左侧有目录,方便阅读。
下载地址:Runtime PDF 麻烦各位大佬点个赞,谢谢!