【iOS】Runtime底层详解

677 阅读6分钟

一、Class的本质

下列代码是仿照objc_class结构体,提取其中需要使用到的信息,自定义的一个结构体。

#import <Foundation/Foundation.h>

#ifndef XXClassInfo_h

#define XXClassInfo_h

# if __arm64__

#   define ISA_MASK        0x0000000ffffffff8ULL

# elif __x86_64__

#   define ISA_MASK        0x00007ffffffffff8ULL

# endif

#if __LP64__

typedef uint32_t mask_t;

#else

typedef uint16_t mask_t;

#endif

typedef uintptr_t cache_key_t;

struct bucket_t {

    cache_key_t _key;

    IMP _imp;

};

struct cache_t {

    bucket_t *_buckets;

    mask_t _mask;

    mask_t _occupied;

};

struct entsize_list_tt {

    uint32_t entsizeAndFlags;

    uint32_t count;

};

struct method_t {

    SEL name;

    const char *types;

    IMP imp;

};

struct method_list_t : entsize_list_tt {

    method_t first;

};

struct ivar_t {

    int32_t *offset;

    const char *name;

    const char *type;

    uint32_t alignment_raw;

    uint32_t size;

};

struct ivar_list_t : entsize_list_tt {

    ivar_t first;

};

struct property_t {

    const char *name;

    const char *attributes;

};

struct property_list_t : entsize_list_tt {

    property_t first;

};

struct chained_property_list {

    chained_property_list *next;

    uint32_t count;

    property_t list[0];

};

typedef uintptr_t protocol_ref_t;

struct protocol_list_t {

    uintptr_t count;

    protocol_ref_t list[0];

};

struct class_ro_t {

    uint32_t flags;

    uint32_t instanceStart;

    uint32_t instanceSize;  // instance对象占用的内存空间

#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;

};

struct class_rw_t {

    uint32_t flags;

    uint32_t version;

    const class_ro_t *ro;

    method_list_t * methods;    // 方法列表

    property_list_t *properties;    // 属性列表

    const protocol_list_t * protocols;  // 协议列表

    Class firstSubclass;

    Class nextSiblingClass;

    char *demangledName;

};

#define FAST_DATA_MASK          0x00007ffffffffff8UL

struct class_data_bits_t {

    uintptr_t bits;

public:

    class_rw_t *data() { 

        // 提供data()方法进行 & FAST_DATA_MASK 操作

        return (class_rw_t *)(bits & FAST_DATA_MASK);

    }

};

/* OC对象 */

struct xx_objc_object {

    void *isa;

};

/* 类对象 */

struct xx_objc_class : xx_objc_object {

    Class superclass;

    cache_t cache;

    class_data_bits_t bits;

public:

    class_rw_t* data() {

        return bits.data();

    }

    // 提供metaClass函数,获取元类对象

    xx_objc_class* metaClass() { 

        // isa指针需要经过一次 & ISA_MASK操作之后才得到真正的地址

        return (xx_objc_class *)((long long)isa & ISA_MASK);

    }

};

#endif /* XXClassInfo_h */

根据结构体中的内容及其关系,总结如下图:

image.png 可以看出,每个类都对应有一个class_rw_t结构体,class_rw_t结构体内有一个指向class_ro_t结构体的指针。在编译期间,class_ro_t结构体就已经确定,objc_class中的bits的data部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClass方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。细看两个结构体的成员变量会发现很多相同的地方,他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于:class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容。

二、isa的本质

OC对象在内存中的排布是一个结构体,其大致框架如下图:

image.png

每个对象结构体的首个成员是个Class类型的变量,该变量定义了对象所属的类,通常称为isa指针。在arm64位下的iOS操作系统中,OC对象的isa区域不再只是一个指针,需要经过一次位运算之后才得到真正的地址。用 64 bit 存储一个内存地址显然是种浪费,毕竟很少有那么大内存的设备。于是可以优化存储方案,用一部分额外空间存储其他内容。isa 指针第一位为 1 即表示使用优化的 isa 指针,这里列出不同架构下的 64 位环境中 isa 指针结构:

union isa_t

{

    isa_t() { }

    isa_t(uintptr_t value) : bits(value) { }

    Class cls;

    uintptr_t bits;

#if SUPPORT_NONPOINTER_ISA

# if __arm64__

#   define ISA_MASK        0x00000001fffffff8ULL

#   define ISA_MAGIC_MASK  0x000003fe00000001ULL

#   define ISA_MAGIC_VALUE 0x000001a400000001ULL

    struct {

        uintptr_t indexed           : 1;

        uintptr_t has_assoc         : 1;

        uintptr_t has_cxx_dtor      : 1;

        uintptr_t shiftcls          : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000

        uintptr_t magic             : 9;

        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)

    };

# elif __x86_64__

#   define ISA_MASK        0x00007ffffffffff8ULL

#   define ISA_MAGIC_MASK  0x0000000000000001ULL

#   define ISA_MAGIC_VALUE 0x0000000000000001ULL

    struct {

        uintptr_t indexed           : 1;

        uintptr_t has_assoc         : 1;

        uintptr_t has_cxx_dtor      : 1;

        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000

        uintptr_t weakly_referenced : 1;

        uintptr_t deallocating      : 1;

        uintptr_t has_sidetable_rc  : 1;

        uintptr_t extra_rc          : 14;

#       define RC_ONE   (1ULL<<50)

#       define RC_HALF  (1ULL<<13)

    };

# else

    // Available bits in isa field are architecture-specific.

#   error unknown architecture

# endif

// SUPPORT_NONPOINTER_ISA

#endif

};

下面是一些位所代表的的含义

截屏2020-06-02 下午4.13.50.png

三、对象,类对象,元类对象的关系

image.png

上图所示即为对象、类对象、元类对象之间的关系,总结如下:

1.每一个对象中都包含一个isa对象。

2.实例的isa指针指向类,类是一个objc_class结构体,包含实例的方法列表、参数列表、category等,除此之外,objc_class中还有一个super_class,指向其类的父类。

3.类的isa指针指向元类,即metaClass,元类存储类方法等信息。元类里也包含isa指针,元类里的isa指针指向根元类,根元类的isa指针指向自己。

4.obj_msgSend发送实例消息的时候,先找到实例,然后通过实例的isa指针找到类的方法列表及参数列表等,如果找到则返回,如果没有找到,则通过super_class在其父类中重复此过程。

5.obj_msgSend发送类消息的时候,通过类的isa找到元类,然后流程与步骤4相同。

四、消息传递机制

OC是一门非常动态的语言,以至于确定调用哪个方法被推迟到了运行时,而非编译时。与之相反,C语言使用静态绑定,也就是说,在编译期就能决定程序运行时所应该调用的函数,所以在C语言中,如果某个函数没有实现,编译时是不能通过的。而OC是相对动态的语言,运行时还可以向类中动态添加方法,所以编译时并不能确定方法到底有没有对应的实现,编译器在编译期间也就不能报错。

对象的方法调用用OC的术语来讲叫做“给某个对象发送某条消息”。在运行时,编译器会把方法调用转化为一条标准的C语言函数调用,即objc_msgSend(),该函数是运行时消息传递机制中的核心函数。

对象的方法调用步骤如下:

1.实例对象的方法调用要先通过实例的isa指针找到类,随后去该类的方法 cache 中查找,如果找到了就返回它。

2.如果没有找到,就去该类的方法列表中查找。如果在该类的方法列表中找到了,则将 IMP 返回,并将它加入cache中缓存起来。根据最近使用原则,这个方法再次调用的可能性很大,缓存起来可以节省下次调用再次查找的开销。

3.如果在该类的方法列表中没找到对应的 IMP,再通过该类结构中的 super_class指针在其父类的方法 cache和方法列表中查找。当在某个父类的方法 cache或方法列表中找到对应的 IMP,就返回它,否则就继续循环,直到基类。

4.如果在自身以及所有父类的方法 cache和方法列表中都没有找到对应的 IMP,则进入消息转发流程。

5.类对象的方法调用要通过类的isa找到元类,随后到元类及其所有父类的方法 cache 和方法列表中进行查找,流程与步骤1~4相同。

五、消息转发机制

消息传递过程中会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索直到继承树根部(通常为NSObject),如果还是找不到就进行消息转发,如果消息转发失败了就会执行doesNotRecognizeSelector:方法报unrecognized selector错。消息转发主要分三步:动态方法决议、备用接收者、完整消息转发,流程如下:

image.png