Runtime源码浅析(内部分享)

前段时间,公司内部开发小组进行了一场Runtime分享交流会,我也从新拾遗了一些与Runtime相关的知识,现分享出来,一块儿学习。git

1.准备:

以前文章:
Runtime在工做中的运用github

Runtime经典面试题(附答案)面试

源码:算法

Runtime开源代码
编译好的objc-750编程

关键词:数组

OC对象、Class、isa指针缓存


2.NSObject对象的Class

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;
复制代码

bitslong类型的数值。

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_objectisa.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_MASKshiftcls进行按位与运算便可以取出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对象在内存中存储的信息包括

  1. isa指针
  2. 其余成员变量

4.2 class对象在内存中存储的信息主要包括

  1. isa指针
  2. superclass指针
  3. 类的属性信息(@property),类的成员变量信息(ivar)
  4. 类的对象方法信息(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对象,在内存中存储的信息主要包括

  1. isa指针
  2. superclass指针
  3. 类的类方法的信息(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,最后找到类方法的实现进行调用

经典isa指向图

代码验证:

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

相关文章
相关标签/搜索