前段时间,公司内部开发小组进行了一场Runtime分享交流会,我也重新拾遗了一些与Runtime相关的知识,现分享出来,一起学习。
1.准备:
之前文章:
Runtime在工作中的运用
Runtime经典面试题(附答案)
源码:
Runtime开源代码
编译好的objc-750
关键词:
OC对象、Class、isa指针
2.NSObject对象的Class
2.1 Class类型
@interface 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 转为二进制:
111111111111111111111111111111111000
可以看出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源码objc4-750编译
探寻OC对象的本质
浅析NSObject对象的Class
神经病院Objective-C Runtime入院第一天——isa和Class