Bootstrap

runtime——一个题引发了长达4天的补习

一、前言

本篇文章是这几天以来学习runtime相关知识的一个总结内容,里面包含了类的实例、类、元类等知识,算是一个大汇总,毕竟学了,学了以后不总结思考,不就是学而不思则罔,话不多说,正文开始。
PS:有不正确的请指出,毕竟这是我的个人理解的文章,请阅读者一定带有辩论的眼光去思考,不要是我写了什么你们就想什么。
文章中会不断出现Q&A。
声明:部分内容来源于网络!!!!

二、问题的抛出(why)

我们先来看一下是什么问题导致了这篇文章的诞生

2.1 请听题(看代码)

以下代码能否正常运行,如果不能会在哪里异常?如果能,运行结果是什么?
请一定要自己思考再往后读!!!
请一定要自己思考再往后读!!!
请一定要自己思考再往后读!!!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.2 输出答案

可以运行,输出结果如下
在这里插入图片描述

2.3 提出问题

我在编译以后,我不明白两个地方

  1. a是怎么取到clsObj的实例对象的?
  2. viewController里的name是怎么被cat实例化的对象调到的?

三、runtime

带着上述的疑问,我展开了搜索,首先搜到的是runtime。

3.1 什么是runtime

Runtime 是一套底层的 C 语言 API,是 iOS 系统的核心之一。在编码过程中,我们可以对任何对象发送消息(object-c消息机制),在编译阶段只是确定了要向接收者发送这条消息,而接受者将要如何响应和处理这条消息,那就要看运行时来决定了。

C语言中,在编译期就会确定要调用的函数。
而Object-C的函数,属于一种动态调用过程,在编译期并不能决定真正调用哪个函数,只有在运行时才会根据函数的名称找到对应的函数来调用。
这意味着OC不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。

3.2 objc与runtime的交互

图片来自一缕殇流化隐半边冰霜
runtime与objc总共有三种交互方法:

  1. 通过Object-C的源代码交互

一般情况开发者只需要编写 OC 代码即可,Runtime 系统自动在幕后把我们写的源代码在编译阶段转换成运行时代码,在运行时确定对应的数据结构和调用具体哪个方法。

  1. 通过 NSObject 类定义的方法

在OC中,除了NSProxy类以外,所有的类都是NSObject的子类。这两个类都遵循了NSObject协议。在NSObject协议中,声明了所有OC对象的公共方法。

在NSObject协议中,有以下5个方法,是可以从Runtime中获取信息,让对象进行自我检查。

在这里插入图片描述
在NSObject的类中还定义了一个方法,来返回指定方法实现的地址IMP。
Q1:什么是IMP?
A1:IMP是”implementation”的缩写,它是objetive-C 方法(method)实现代码块的地址,可像C函数一样直接调用。

在这里插入图片描述
3. 通过对 Runtime 库函数的直接调用:

#import <objc/runtime.h>

从Xcode5开始,苹果就不建议我们手动调用Runtime的API,也同样希望我们不要知道具体底层实现

四、NSObject的过往

在了解了runtime的基本知识后,我们发现与runtime交互时候,有两个都跟NSObject有关,那这里就学一学NSObject。

源码:objc4-680

从NSObject.h中可以看到,其定义为
在这里插入图片描述

4.1 objc_class

在Objc2.0之前,objc_class源码如下(抄的):

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
    
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif
    
} OBJC2_UNAVAILABLE;

在这里可以看到,在一个类中,有超类的指针,类名,版本的信息。

  • ivars是objc_ivar_list成员变量列表的指针;
  • methodLists是指向objc_method_list指针的指针。
  • methodLists是指向方法列表的指针。这里如果动态修改*methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。

在2006年苹果发布Objc 2.0之后,objc_class的定义就变成下面这个样子了。


typedef struct objc_class *Class;
typedef struct objc_object *id;

@interface Object { 
    Class isa; 
}

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

struct objc_object {
private:
    isa_t isa;
}

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
}

在这里插入图片描述
把源码的定义转化成类图,就是上图的样子。

图中左半部分,可以得出一下三个结论:

  1. objc_object被源码typedef成了id类型,这也就是我们平时遇到的id类型。这个结构体中就只包含了一个isa_t类型的结构体。这个结构体在下面会详细分析。

  2. objc_class继承于objc_object。所以在objc_class中也会包含isa_t类型的结构体isa。于是Objective-C 中类也是一个对象。在objc_class中,除了isa之外,还有3个成员变量,1.父类的指针,2.方法缓存,3.类的实例方法链表。

  3. object类和NSObject类里面分别都包含一个objc_class类型的isa。

4.3 接下来再说说isa

当一个对象的实例方法被调用的时候,会通过isa找到相应的类,然后在该类的class_data_bits_t中去查找方法。class_data_bits_t是指向了类对象的数据区域,在该数据区域内查找相应方法的对应实现。

这里为了和对象查找方法的机制一致,遂引入了元类(meta-class)的概念(可以跳到第五节先看元类简单叙述)。

4.4 isa_t结构体的具体实现

objc_object里面的isa是isa_t类型。通过查看源码,我们可以知道isa_t是一个union联合体。

struct objc_object {
private:
    isa_t isa;
public:
    // initIsa() should be used to init the isa of new objects only.
    // If this object already has an isa, use changeIsa() for correctness.
    // initInstanceIsa(): objects with no custom RR/AWZ
    void initIsa(Class cls /*indexed=false*/);
    void initInstanceIsa(Class cls, bool hasCxxDtor);
private:
    void initIsa(Class newCls, bool indexed, bool hasCxxDtor);

4.4.1 initIsa方法

以arm64为例

inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    initIsa(cls, true, hasCxxDtor);
}


inline void
objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor)
{
    if (!indexed) {
        isa.cls = cls;
    } else {
        isa.bits = ISA_MAGIC_VALUE;
        isa.has_cxx_dtor = hasCxxDtor;
        isa.shiftcls = (uintptr_t)cls >> 3;
    }
}

// initIsa第二个参数传入了一个true,所以initIsa就会执行else里面的语句。


# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t indexed           : 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)
    };

struct参数(抄):

  1. 第一位indexed,代表是否开启isa指针优化。index = 1,代表开启isa指针优化。这里涉及Tagged Pointer
  2. has_assoc:对象含有或者曾经含有关联引用,没有关联引用的可以更快地释放内存
  3. has_cxx_dtor:表示该对象是否有 C++ 或者 Objc 的析构器
  4. shiftcls:类的指针。arm64架构中有33位可以存储类指针。isa.shiftcls = (uintptr_t)cls >> 3;将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。具体可以看从 NSObject 的初始化了解 isa这篇文章里面的shiftcls分析。
  5. magic;判断对象是否初始化完成,在arm64中0x16是调试器判断当前对象是真的对象还是没有初始化的空间。
  6. weakly_referenced:对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放
  7. deallocating:对象是否正在释放内存
  8. has_sidetable_rc:判断该对象的引用计数是否过大,如果过大则需要其他散列表来进行存储。
  9. extra_rc:存放该对象的引用计数值减一后的结果。对象的引用计数超过 1,会存在这个这个里面,如果引用计数为 10,extra_rc的值就为 9。
  10. ISA_MAGIC_MASK 和 ISA_MASK 分别是通过掩码的方式获取MAGIC值 和 isa类指针。

4.5 cache_t的具体实现

源码:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}

typedef unsigned int uint32_t;
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

typedef unsigned long  uintptr_t;
typedef uintptr_t cache_key_t;

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
}

在这里插入图片描述
cache_t存储了一个bucket_t的结构体,和两个unsigned int的变量。
bucket_t结构体中存储了一个unsigned long和一个IMP。IMP是一个函数指针,指向了一个方法的具体实现。
cache_t中的bucket_t *_buckets其实就是一个散列表,用来存储Method的链表。
Cache的作用主要是为了优化方法调用的性能。当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

4.6 class_data_bits_t的具体实现

源码:

struct class_data_bits_t {

    // Values are the FAST_ flags above.
    uintptr_t bits;
}

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

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

在这里插入图片描述
Objc的类的属性、方法、以及遵循的协议在obj 2.0的版本之后都放在class_rw_t中。class_ro_t是一个指向常量的指针,存储来编译器决定了的属性、方法和遵守协议。rw-readwrite,ro-readonly

在编译期类的结构中的 class_data_bits_t *data指向的是一个 class_ro_t *指针:
在这里插入图片描述
在运行时调用 realizeClass方法,会做以下3件事情:

  1. 从 class_data_bits_t调用 data方法,将结果从 class_rw_t强制转换为 class_ro_t指针
  2. 初始化一个 class_rw_t结构体
  3. 设置结构体 ro的值以及 flag
    最后调用methodizeClass方法,把类里面的属性,协议,方法都加载进来。
    在runtime中消息传递和转发的目的就是为了找到IMP,并执行函数。

4.7 区别图

在这里插入图片描述

五、元类

元类也是类,是描述类对象的类。

一切皆对象。每一个对象都对应一个类。

先来看个图,阐述了对象、类、元类的关系:

在这里插入图片描述
简单的说:元类就是类所属的类
从消息机制的层面来说:

当你给对象发送消息时,消息是在寻找这个对象的类的方法列表。
当你给类发消息时,消息是在寻找这个类的元类的方法列表。

换句话说

对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。
类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。

meta-class之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。

注意四个点:

  1. Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
  2. 每个Class都有一个isa指针指向唯一的Meta class
  3. Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
  4. 每个Meta class的isa指针都指向Root class (meta)。

类对象和元类对象是唯一的,对象是可以在运行时创建无数个的。

六、回到最初的问题

6.1 clsObj实例过程

在这里插入图片描述
这里的转化过程:
a被转换成了一个指向Cat Class的指针,然后使用id转换成了objc_object类型。a现在实际上已经是一个Cat类型的实例对象了。
换个说法:
a已经满足了构成一个objc对象的全部要求(objc_object结构体的构成就是一个指向Class的isa指针),遂能够正常走消息机制!!!回忆前面知识想下上一篇博文中objc_msgSend的工作流程,在代码中的a指向的Cat Class中能够找到call3方法,所以代码能够正常运行。

6.2 a如何调用实例方法

咱们修改一下代码:

在这里插入图片描述

输出结果:

ViewController = <ViewController: 0x7ff78d9053c0> , 地址 = 0x7ffee48f0248
Cat class = Cat 地址 = 0x7ffee48f0228
Void *a = <Cat: 0x7ffee48f0228> 地址 = 0x7ffee48f0220
Cat <ViewController: 0x7ff78d9053c0> call3
Cat instance = <Cat: 0x6000004c3ec0> 地址 = 0x7ffee48f0218

按viewDidLoad执行时各个变量入栈顺序从高到底为self(隐藏), _cmd(隐藏), super_class(等同于self.class), receiver(等同于self), obj。
在这里插入图片描述
第一个self和第二个_cmd是隐藏参数。第三个self.class和第四个self是[super viewDidLoad]方法执行时候的参数。

在调用self.name的时候,本质上是self指针在内存向高位地址偏移一个指针。

从打印结果我们可以看到,obj就是clsObj的地址。在obj向上偏移一个指针就到了0x7ffee48f0248,这正好是ViewController的地址。

Q:Objc中的对象到底是什么呢?
A:实质:Objc中的对象是一个指向ClassObject地址的变量,即 id obj = &ClassObject , 而对象的实例变量 void *ivar = &obj + offset(N)

回到原题,在原来的格式中,偏移一个指针
在这里插入图片描述

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;