前段时间,公司内部开发小组进行了一场Runtime分享交流会,我也从新拾遗了一些与Runtime相关的知识,现分享出来,一块儿学习。git
以前文章:
Runtime在工做中的运用github
源码:算法
关键词:数组
OC对象、Class、isa指针缓存
2.1 Class类型bash
@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结构体的指针。app
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.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.1 instance对象在内存中存储的信息包括
4.2 class对象在内存中存储的信息主要包括
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对象,在内存中存储的信息主要包括
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