美文网首页
Runtime 一:类与对象基础结构

Runtime 一:类与对象基础结构

作者: iOS_修心 | 来源:发表于2021-09-26 19:26 被阅读0次

Demo: https://github.com/iOSlixiang/RuntimeTest.git
objc4-818.2 源码: https://github.com/iOSlixiang/objc4-818.2.git

Class

Objective-C类是由Class类型来表示的,它实际上只是objc_class的结构体指针。

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

@interface Object { 
    Class isa; 
}

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

在2006年苹果发布Objc 2.0,objc_class定义没有发生变化,但是结构体发生了变化。
在Objc2.0之前,objc_class源码

// 一个类的实例的结构体
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
// 类结构体
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父类
    const char *name                        OBJC2_UNAVAILABLE;  // 类名
    long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
    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;

Objc2.0,objc_class继承于objc_object。在objc_class中也会包含isa_t类型的结构体isa。 Objective-C 中类也是一个对象。

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
}
// isa是一个联合体
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
}

把源码的定义转化成类图,就是上图的样子。

从上述源码中,我们可以看到,Objective-C 对象都是 C 语言结构体实现的,在objc2.0中,所有的对象都会包含一个isa_t类型的结构体。

objc_object被源码typedef成了id类型,这也就是我们平时遇到的id类型。这个结构体中就只包含了一个isa_t类型的结构体。

bjc_class继承于objc_object。所以在objc_class中也会包含isa_t类型的结构体isa。至此,可以得出结论:Objective-C 中类也是一个对象。在objc_class中,除了isa之外,还有3个成员变量,一个是父类的指针,一个是方法缓存,最后一个这个类的实例方法链表。

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

上图的左半边类的关系,右边是isa的结构体

isa指针

  1. 联合体:一种特殊的数据类型,其目的是节省内存。联合体内部可以定义多种数据类型,但是同一时间只能表示某一种数据类型,且所有的数据类型共享同一段内存。联合体的内存大小等于所定义的数据类型中占用内存的最大者。
  2. 互斥赋值/共用内存:允许装入该“联合”所定义的任何一种数据成员,但同一时间只能表示一种数据成员,采用了覆盖的技术;
  3. union所占内存长度:union 变量所占用的内存长度等于最长的成员的内存长度;
union isa_t { //联合体
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    //提供了cls 和 bits ,两者是互斥关系
    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

isa_t类型使用联合体的原因也是基于内存优化的考虑,这里的内存优化是指在isa指针中通过char + 位域(即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针占用的内存大小是8字节,即64位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能

isa_t的定义中可以看出:

  • 提供了两个成员,clsbits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式

    • 通过cls初始化,bits无默认值

    • 通过bits初始化,cls无默认值

  • 还提供了一个结构体定义的·位域·,用于存储类信息及其他信息,结构体的成员·ISA_BITFIELD·,这是一个宏定义,有两个版本 __arm64__(对应ios 移动端) 和 __x86_64__(对应macOS)

位域的宏定义

元类(Meta Class)

在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。如:

NSArray *array = [NSArray array];

这个例子中,+array消息发送给了NSArray类,而这个NSArray也是一个对象。既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么这些就有一个问题了,这个isa指针指向什么呢?为了调用+array方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念

meta-class是一个类对象的类。

当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

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

再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。这样就形成了一个完美的闭环

通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了,如下图所示:


图中实线是 super_class指针,虚线是isa指针。

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

对于NSObject继承体系来说,其实例方法对体系中的所有实例、类和meta-class都是有效的;而类方法对于体系内的所有类和meta-class都是有效的。

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的变量。

mask:分配用来缓存bucket的总数。
occupied:表明目前实际占用的缓存bucket的个数。

bucket_t的结构体中存储了一个unsigned long和一个IMP。IMP是一个函数指针,指向了一个方法的具体实现。

cache_t中的bucket_t *_buckets其实就是一个散列表,用来存储Method的链表。

Cache的作用主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

class_data_bits_t

// class_data_bits_t相当于 class_rw_t指针加上 rr/alloc 的标志。
struct class_data_bits_t {
    // Values are the FAST_ flags above.
    uintptr_t bits;
}
// 返回 class_rw_t *指针的快捷方法
class_rw_t *data() {
    return bits.data();
}


// class_ro_t 在编译期产生,它是类中的可读信息。
// class_rw_t 在运行时产生,它是类中的可读写信息。
struct class_rw_t {
    uint32_t flags;    // 标记
    uint32_t version;  // 版本

    // 类中的只读信息
    const class_ro_t *ro;

    /*
     这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
     methods中,存储 method_list_t ----> method_t
     二维数组,method_list_t --> method_t
     这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
     */
    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

     /* ivar 布局, 在编译期这里就固定了,
     它标示ivars的内存布局,在运行时不能改变,
     这也是为什么我们在运行时不能动态给类添加成员变量的原因*/
    const uint8_t * ivarLayout;
    
    const char * name;  //名字
    method_list_t * baseMethodList;  //方法链表
    protocol_list_t * baseProtocols; //协议链表
    const ivar_list_t * ivars;       //成员变量链表

    const uint8_t * weakIvarLayout; // weak 成员变量的内存布局
    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方法,把类里面的属性,协议,方法都加载进来。

整个流程 objc_readClassPair --> readClass --> realizeClass --> methodizeClass

总结一下objc_class 1.0和2.0的差别。

相关文章

网友评论

      本文标题:Runtime 一:类与对象基础结构

      本文链接:https://www.haomeiwen.com/subject/ejqcnltx.html