内容简介:在Runtime源码中,我们能发现NSObject对象只有一个Class对象其实是一个指向objc_class结构体的指针。
2.1 Class类型
@interface NSObject <NSObject> { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-interface-ivars" Class isa OBJC_ISA_AVAILABILITY; #pragma clang diagnostic pop } 复制代码
在Runtime源码中,我们能发现NSObject对象只有一个 Class
类型的成员变量: isa
typedef struct objc_class *Class; 复制代码
Class对象其实是一个指向objc_class结构体的指针。
struct objc_object { private: isa_t isa; // 这里省略成员变量以及方法... } 复制代码
Class
类型本质是个结构体,该结构体中存储了该 NSObject
中的所有信息。
那么一个NSObject对象占用多少内存?
NSObjcet实际上是只有一个名为isa的指针的结构体,因此占用一个指针变量所占用的内存空间大小,如果64bit(64位架构中)占用8个字节,如果32bit占用4个字节。
2.2 Class方法
- (Class)class { return object_getClass(self); } 复制代码
在Runtime源码中,我们调用Class方法,其实是在调用 object_getClass(self)
,最终通过下面代码获取结果值。
inline Class objc_object::ISA() { // 忽略其它方法 return (Class)(isa.bits & ISA_MASK); } 复制代码
2.3 isa.bits & ISA_MASK
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; }; 复制代码
上述源码可以知道, isa_t
是个联合体。
typedef unsigned long uintptr_t; 复制代码
bits
是 long
类型的数值。
在 isa.h
中,可以找到 ISA_MASK
源码
# if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # elif __x86_64__ # define ISA_MASK 0x00007ffffffffff8ULL # else # error unknown architecture for packed isa # endif 复制代码
可知,其实 ISA_MASK
还是个数值类型
我们可以看到class方法最终获取的即是:
结构体 objc_object
的 isa.bits & ISA_MASK
的数值计算结果。
3.NSObject对象的isa_t
3.1 isa_t
// 精简过的isa_t共用体 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; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1; uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 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) }; #endif }; 复制代码
上述源码中 isa_t
是union(共用体)类型。可以看到共用体中有一个结构体,结构体内部分别定义了一些变量,变量后面的值代表的是该变量占用多少个二进制位,也就是位域技术。
源码中通过共用体的形式存储了64位的值,这些值在结构体中被展示出来,通过对 bits
进行位运算而取出相应位置的值。
3.2 共用体
在进行某些算法的 C语言 编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构,简称共用体,也叫联合体。
优点:可以很大程度上节省内存空间。
union U1 { int n; char s[11]; double d; }; 复制代码
对于U1共用体,s占11字节,n占4字节,d占8字节,因此其至少需11字节的空间。然而其实际大小并不是11,用运算符sizeof测试其大小为16。这是因为 内存对齐原则 ,11既不能被4整除,也不能被8整除。因此补充字节到16,这样就符合所有成员的自身对齐了。所以 联合体的内存除了取最大成员内存外,还要保证是所有成员类型size的最小公倍数
对比类的内存对齐:
原则 1. 前面的地址必须是后面的地址正数倍,不是就补齐。 原则 2. 整个Struct的地址必须是最大字节的整数倍。
@interface MXRPerson : NSObject{ int _age; } 复制代码
person对象的第一个地址要存放isa指针需要8个字节,第二个地址要存放_age成员变量需要4个字节,因此person对象就占用16个字节空间。
代码验证:
int main(int argc, const char * argv[]) { @autoreleasepool { // 验证内存地址 NSObject *obj = [[NSObject alloc] init]; NSLog(@"%zd",class_getInstanceSize([NSObject class])); NSLog(@"%zd",class_getInstanceSize([MXRPerson class])); } return 0; } // 8 16 复制代码
3.3 isa中存储的信息及作用
struct { // 0代表普通的指针,存储着Class,Meta-Class对象的内存地址。 // 1代表优化后的使用位域存储更多的信息。 uintptr_t nonpointer : 1; // 是否有设置过关联对象,如果没有,释放时会更快 uintptr_t has_assoc : 1; // 是否有C++析构函数,如果没有,释放时会更快 uintptr_t has_cxx_dtor : 1; // 存储着Class、Meta-Class对象的内存地址信息 uintptr_t shiftcls : 33; // 用于在调试时分辨对象是否未完成初始化 uintptr_t magic : 6; // 是否有被弱引用指向过。 uintptr_t weakly_referenced : 1; // 对象是否正在释放 uintptr_t deallocating : 1; // 引用计数器是否过大无法存储在isa中 // 如果为1,那么引用计数会存储在一个叫SideTable的类的属性中 uintptr_t has_sidetable_rc : 1; // 里面存储的值是引用计数器减1 uintptr_t extra_rc : 19; }; 复制代码
此时我们重新来看 ISA_MASK
的值 0000000ffffffff8 转为二进制:
可以看出 ISA_MASK
的值转化为二进制中有33位都为1,所以按位与的作用是可以取出这33位中的值。我们再回头看看 isa_t
的源码,不难发现,这33位对应的是结构体的 shiftcls
的位域。那么 ISA_MASK
同 shiftcls
进行按位与运算即可以取出Class或Meta-Class的值(内存地址的值)。
同时可以看出 ISA_MASK
最后三位的值为0,那么任何数同 ISA_MASK
按位与运算之后,得到的最后三位必定都为0,因此任何类对象或元类对象的内存地址最后三位必定为0,转化为十六进制末位必定为8或者0。
对象的isa指针需要同 ISA_MASK
经过一次&(按位与)运算才能得出真正的Class对象地址。
代码验证
- (void)viewDidLoad { [super viewDidLoad]; MXRPerson *person = [[MXRPerson alloc]init]; NSLog(@"%p",[person class]); NSLog(@"%@",person); } 复制代码
2019-04-24 18:21:30.424630+0800 IsaTestDemo[58799:8221193] 0x1005c8db0 (lldb) p/x person->isa (Class) $0 = 0x000001a1005c8db1 MXRPerson 复制代码
shiftcls
中存储类对象地址。把转为2进制的实例对象isa地址与转为2进制的类对象地址作对比,可以看出存储类对象地址的33位二进制内容完全相同。
4.Class对象在内存中存储的信息
4.1 instance对象在内存中存储的信息包括
- isa指针
- 其他成员变量
4.2 class对象在内存中存储的信息主要包括
- isa指针
- superclass指针
- 类的属性信息(@property),类的成员变量信息(ivar)
- 类的对象方法信息(instance method),类的协议信息(protocol)
4.3 class_rw_t & class_ro_t
我们发现class_rw_t中存储着方法列表,属性列表,协议列表等内容。
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // 方法进行缓存 class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *data() { return bits.data(); } 复制代码
class_rw_t
中的methods是二维数组的结构,并且可读可写,因此可以动态的添加方法,并且更加便于分类方法的添加。
struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint32_t version; const class_ro_t *ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; 复制代码
而class_rw_t是通过bits调用data方法得来的,我们来到data方法内部实现。我们可以看到,data函数内部仅仅对bits进行&FAST_DATA_MASK操作
class_rw_t* data() { return (class_rw_t *)(bits & FAST_DATA_MASK); } 复制代码
成员变量信息则是存储在class_ro_t内部中的,我们来到class_ro_t内查看。
struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; //实例对象大小 #ifdef __LP64__ uint32_t reserved; #endif 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; method_list_t *baseMethods() const { return baseMethodList; } }; 复制代码
4.4 每个类在内存中有且只有一个meta-class对象,在内存中存储的信息主要包括
- isa指针
- superclass指针
- 类的类方法的信息(class method)
5.验证对象的isa指针指向
1.当对象调用实例方法的时候,我们上面讲到,实例方法信息是存储在class类对象中的,那么要想找到实例方法,就必须找到class类对象,那么此时isa的作用就来了
instance的isa指向class,当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用。
2.当类对象调用类方法的时候,同上,类方法是存储在meta-class元类对象中的。那么要找到类方法,就需要找到meta-class元类对象,而class类对象的isa指针就指向元类对象
class的isa指向meta-class当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用
3.当对象调用其父类对象方法的时候,又是怎么找到父类对象方法的呢?,此时就需要使用到class类对象superclass指针。
当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用,同样如果Person发现自己没有响应的对象方法,又会通过Person的superclass指针找到NSObject的class对象,去寻找响应的方法
4.当类对象调用父类的类方法时,就需要先通过isa指针找到meta-class,然后通过superclass去寻找响应的方法
当Student的class要调用Person的类方法时,会先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用
代码验证:
struct mxr_objc_class{ Class isa; }; int main(int argc, const char * argv[]) { @autoreleasepool { // 如何证明isa指针的指向真的如上面所说? NSObject *object = [[NSObject alloc] init]; Class objectClass = [NSObject class]; // 我们自己创建一个同样的结构体并通过强制转化拿到isa指针。 struct mxr_objc_class *objectClass2 = (__bridge struct mxr_objc_class *)(objectClass); Class objectMetaClass = object_getClass([NSObject class]); NSLog(@"%p %p %p", object, objectClass, objectMetaClass); } return 0; } 复制代码
验证结果1
(lldb) p/x object->isa (Class) $0 = 0x001d800100b16141 NSObject (lldb) p/x objectClass (Class) $1 = 0x0000000100b16140 NSObject (lldb) p/x 0x00007ffffffffff8 & 0x001d800100b16141 (long) $2 = 0x0000000100b16140 复制代码
object-isa指针地址0x001dffff96537141经过同0x00007ffffffffff8位运算,得出objectClass的地址0x00007fff96537140
验证结果2
我们来验证class对象的isa指针是否同样需要位运算计算出 meta-class
对象的地址。 以同样的方式打印 objectClass->isa
指针时,发现无法打印。
(lldb) p/x objectClass->isa error: member reference base type 'Class' is not a structure or union 复制代码
为了拿到isa指针的地址,我们自己创建一个同样的结构体并通过强制转化拿到isa指针。
(lldb) p/x objectClass2->isa (Class) $0 = 0x001d800100b160f1 (lldb) p/x objectMetaClass (Class) $1 = 0x0000000100b160f0 (lldb) p/x 0x00007ffffffffff8 & 0x001d800100b160f1 (long) $2 = 0x0000000100b160f0 复制代码
objectClass2的isa指针经过位运算之后的地址是meta-class的地址。
以上所述就是小编给大家介绍的《Runtime源码浅析(内部分享)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。