【iOS】Runtime底层详解

一、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相同。

image.png

五、消息转发机制

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

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352