Runtime源码浅析(内部分享)

Runtime源码浅析(内部分享)_第1张图片

前段时间,公司内部开发小组进行了一场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;

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位二进制内容完全相同。

Runtime源码浅析(内部分享)_第2张图片
屏幕快照 2019-05-11 下午10.14.56.png

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,最后找到类方法的实现进行调用

Runtime源码浅析(内部分享)_第3张图片
经典isa指向图.png

代码验证:

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源码浅析(内部分享)_第4张图片

参考文章

最新Runtime源码objc4-750编译
探寻OC对象的本质
浅析NSObject对象的Class
神经病院Objective-C Runtime入院第一天——isa和Class

你可能感兴趣的:(Runtime源码浅析(内部分享))