前言

之前,咱们在探求动画及烘托相关原理的时分,咱们输出了几篇文章,解答了iOS动画是怎么烘托,特效是怎么工作的疑惑。咱们深感体系规划者在创作这些体系结构的时分,是如此脑洞大开,也 深深意识到了解一门技能的底层原理关于从事该方面工作的重要性。

因而咱们决定 进一步探求iOS底层原理的使命。在这篇文章中咱们围绕Runtime打开,会逐个探求:isa详解class的结构办法缓存cache_t

一、Runtime简介

1. OC言语的实质回忆

咱们 之前在 探求 OC言语的实质时,了解到Apple官网对OC的介绍:

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

  • Objective-C是程序员在为OS X和iOS编写软件时运用的首要编程言语(之一,现在现已还有Swift言语)
  • 是C编程言语的超集,提供面向目标的功用和动态运转
  • Objective-C承继了C言语的语法根本类型流操控句子,并增加了用于界说类和办法的语法。(OC彻底兼容规范C言语)
  • 它还增加了面向目标办理和目标字面量的言语级别支持,一起提供动态类型和绑定,将许多责任推迟到运转时

2. Runtime

官网中介绍OC言语时,提及的动态运转动态类型和绑定、将许多责任推迟到运转时等许多运转时特性,便是讲经过Runtime这套底层API来完结的。

尽管,Objective-C是一门闭源的言语,但官方也对该言语有了恰当的开源。咱们通常能够经过该地址去查找苹果官方开源的一些源码:opensource.apple.com/tarballs/

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】
经过大局查找objc,能够找到objc4,然后下载最新的开源版本代码
12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】
咱们能够从官方开源的代码中也能够看到 官方开源的一些完结,其间就包含了runtime的一些完结
12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

综上,咱们不难得出定论:

  • Objective-C是一门动态性比较强的编程言语,跟C、C++等言语有着很大的不同;
  • Objective-C的动态性是由Runtime API来支撑的
  • Runtime API提供的接口根本都是C言语的,源码由C\C++\汇编言语编写

二、isa详解

前面,咱们在探求 OC中的几种目标和目标的isa指针的时分得出一些定论,咱们简略回忆一下: Objective-C中的目标,简称OC目标,首要能够分为3种

  • instance目标(实例目标)
  • class目标(类目标)
  • meta-class目标(元类目标)

1. instance目标

instance目标便是经过类alloc出来的目标,每次调用alloc都会产生新的instance目标

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

  • object1、object2是NSObject的instance目标(实例目标)
  • 它们是不同的两个目标,别离占有着两块不同的内存
    12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】
    12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】
  • instance目标在内存中存储的信息包含
    • isa指针
    • 其他成员变量

2. class目标

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

  • objectClass1 ~ objectClass5都是NSObject的class目标(类目标)
  • 它们是同一个目标。每个类在 内存中有且只要一个 class目标

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

  • class目标在内存中存储的信息首要包含:
    • isa指针
    • superclass指针
    • 类的特点信息(@property)、类的目标办法信息(instance method)
    • 类的协议信息(protocol)、类的成员变量信息(ivar)
    • ……

3. meta-class目标

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

  • objectMetaClass是NSObject的meta-class目标(元类目标)
  • 每个类在内存中有且只要一个meta-class目标

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

  • meta-class目标和class目标的内存结构是相同的,可是用途不相同,在内存中存储的信息首要包含
    • isa指针
    • superclass指针
    • 类的类办法信息(class method)
    • ……

4. isa指针

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

  • instanceisa指向class

    • 当调用目标办法时,经过instanceisa找到class,终究找到目标办法的完结进行调用
  • classisa指向meta-class

    • 当调用类办法时,经过classisa找到meta-class,终究找到类办法的完结进行调用

class目标的superclass指针

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

  • 当Student的instance目标要调用Person的目标办法时,会先经过isa找到Student的class
  • 然后经过superclass找到Person的class,终究找到目标办法的完结进行调用

meta-class目标的superclass指针

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

  • 当Student的class要调用Person的类办法时,会先经过isa找到Student的meta-class
  • 然后经过superclass找到Person的meta-class,终究找到类办法的完结进行调用

5. 对isasuperclass总结

isa

  • instanceisa指向class
  • classisa指向meta-class
  • meta-classisa指向基类的meta-class
  • 基类的classisa 指向基类的meta-class
  • 基类的meta-classisa指向基类的meta-class自身

superclass

  • classsuperclass指向父类的class
    • 假如没有父类,superclass指针为nil
  • meta-classsuperclass指向父类的meta-class
    • 基类的meta-class的superclass指向基类的class

办法调用

  • instance调用目标办法的轨道

    • isa找到class,办法不存在,就经过superclass找父类
  • class调用类办法的轨道

    • isa找meta-class,办法不存在,就经过superclass找父类

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

6. 综上

结合前面的定论,咱们不难得知,OC言语中的三类目标,是经过isa指针建立联系的,而OC的运转时特性所依赖的的RuntimeAPI正是在必定程度上基于isa指针建立的三类目标的联系,完结 动态运转时的。

因而,要想学习Runtime,首要要了解它底层的一些常用数据结构,比方isa指针

  • 在arm64架构之前,isa便是一个一般的指针,存储着ClassMeta-Class目标的内存地址
  • 从arm64架构开端,对isa进行了优化,变成了一个共用体(union)结构,还运用位域来存储更多的信息。需求经过ISA_MASK进行必定的位运算才干进一步获取具体的信息

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

7. isa的实质

在arm64架构之后 OC目标的isa指针并不是直接指向类目标或许元类目标,而是需求&ISA_MASK经过位运算才干获取到类目标或许元类目标的地址。
今天来探寻一下为什么需求&ISA_MASK才干获取到类目标或许元类目标的地址,以及这样的优点。(苹果官方为什么做这个优化呢?咱们来一步一步探求一下!)

首要在源码中找到isa指针,看一下isa指针的实质。

// 截取objc_object内部分代码
struct objc_object {
private:
    isa_t isa;
} 

isa指针其实是一个isa_t类型的共用体,来到isa_t内部检查其结构

// 精简过的isa_t共用体
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
#if SUPPORT_PACKED_ISA
# 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)
    };
# elif __x86_64__     
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
#       define RC_ONE   (1ULL<<56)
#       define RC_HALF  (1ULL<<7)
    };
# else
#   error unknown architecture for packed isa
# endif
#endif 

上述源码中isa_tunion类型,union表明共用体。

从源码中咱们能够看到:

  • 共用体中有一个结构体
  • 结构体内部别离界说了一些变量
  • 变量后边的值代表的是该变量占用多少个字节,也便是位域技能

了解共用体

  • 在进行某些算法的C言语编程的时分,需求使几种不同类型的变量的值寄存到同一段内存单元中;
  • 这种几个不同的变量共同占用一段内存的结构,在C言语中,被称作“共用体”类型结构,简称共用体

接下来运用共用体的办法来深入的了解apple为什么要运用共用体,以及运用共用体的优点。

7.1 探寻进程

7.1.1 仿照底层对数据的存储

接下来运用代码来仿照底层的做法,创立一个person类并含有三个BOOL类型的成员变量。

@interface Person : NSObject
@property (nonatomic, assign, getter = isTall) BOOL tall;
@property (nonatomic, assign, getter = isRich) BOOL rich;
@property (nonatomic, assign, getter = isHansome) BOOL handsome;
@end 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%zd", class_getInstanceSize([Person class]));
    }
    return 0;
}
// 打印内容
// Runtime - union探寻[52235:3160607] 16

上述代码中Person含有3个BOOL类型的特点,打印Person类目标占有内存空间为16

  • 也便是(isa指针 = 8) + (BOOL tall = 1) + (BOOL rich = 1) + (BOOL handsome = 1) = 13
  • 由于内存对齐准则所以Person类目标占有内存空间为16(关于这内存对齐相关的常识,咱们在这篇文章介绍过)

经过共用体技能,能够使几个不同的变量寄存到同一段内存中去,能够很大程度上节省内存空间

尝试用一个字节存储三个BOOL类型的变量的值

  • 那么咱们知道BOOL值只要两种情况 0 或许 1,可是却占有了一个字节的内存空间
  • 而一个内存空间中有8个二进制位,而且二进制只要 0 或许 1
    • 那么是否能够运用1个二进制位来表明一个BOOL值
    • 也便是说3个BOOL值终究只运用3个二进制位,也便是一个内存空间即可呢?
    • 怎么完结这种办法?

首要假如运用这种办法 需求自己写setter、getter办法的声明与完结:

  • 不能够写特点声明,由于一旦写特点,体系会主动帮咱们增加成员变量(会拓荒内存空间、也会完结setter、getter。我为了探求要躲避体系的主动生成)

别的想要将三个BOOL值寄存在一个字节中,咱们能够增加一个char类型的成员变量

  • char类型占有一个字节内存空间,也便是8个二进制位
  • 能够运用其间终究三个二进制位来存储3个BOOL值。
@interface Person()
{
    char _tallRichHandsome;
} 

例如_tallRichHansome的值为 0b 0000 0010 ,那么只运用8个二进制位中的终究3个,别离为其赋值0或许1来代表tall、rich、handsome的值。如下图所示:

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

那么现在面临的问题便是怎么取出8个二进制位中的某一位的值,或许为某一位赋值呢?

a.) 取值

假设将三个BOOL变量的值 存在 一个字节里边,咱们首要讨论一下怎么从一个字节里边 取出 这三个变量的具体值。

能够运用1个二进制位来表明一个BOOL值,那么从低位开端,一个二进制位代表一个值。

  • 假设char类型的成员变量中存储的二进制为0b 0000 0010
  • 假如想将倒数第2位的值也便是rich的值取出来,则需求进行 进制位的 位运算
  • 咱们能够运用&进行按位与运算从而取出相应方位的值

了解【&:按位与】 同真为真,其他都为假

// 示例
// 取出倒数第三位 tall
  0000 0010
& 0000 0100
------------
  0000 0000  // 取出倒数第三位的值为0,其他位都置为0
// 取出倒数第二位 rich
  0000 0010
& 0000 0010
------------
  0000 0010 // 取出倒数第二位的值为1,其他位都置为0

定论: 按位与能够用来取出特定的二进制位的值

  • 想取出哪一位就将那一方位为1,其他为都置为0
  • 然后同原数据进行按位与核算,即可取出特定的位

按位与 运算来完结get办法

#define TallMask 0b00000100 // 4
#define RichMask 0b00000010 // 2
#define HandsomeMask 0b00000001 // 1
- (BOOL)tall
{
    return !!(_tallRichHandsome & TallMask);
}
- (BOOL)rich
{
    return !!(_tallRichHandsome & RichMask);
}
- (BOOL)handsome
{
    return !!(_tallRichHandsome & HandsomeMask);
} 

上述代码中运用两个!!(非)来将值改为bool类型。相同运用上面的比如

// 取出倒数第二位 rich
  0000 0010  // _tallRichHandsome
& 0000 0010 // RichMask
------------
  0000 0010 // 取出rich的值为1,其他位都置为0 

上述代码中(_tallRichHandsome & TallMask)的值为0000 0010也便是2,可是咱们需求的是一个BOOL类型的值 0 或许 1

  • 那么!!2就将 2 先转化为 0 ,之后又转化为 1
  • 相反假如按位与取得的值为 0 时,!!0将 0 先转化为 1 之后又转化为 0
  • 因而运用!!两个非操作将值转化为 0 或许 1 来表明相应的值。

7.1.2 优化掩码,使其增加可读性

掩码: 一般用来 进行 按位与(&)运算的值称之为掩码

  • 上述代码中界说了三个宏,用来别离进行按位与运算而取出相应的值
  • 三个宏的具体值都是掩码
  • 为了能更明晰的表明掩码是为了取出哪一位的值,上述三个宏的界说能够运用左移运算符:<<来优化

左移运算符 A<<n ,表明在A数值的二进制数据中左移n位得到一个值

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

那么上述宏界说能够运用<<(左移)优化成如下代码

#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1 
b.) 设值

咱们假如想给某一个二进制位赋值 0或许1,仍然能够运用 位运算

假如想设置某个值的某个二进制位的值为1,那么只要在该二进制位 与1进行 |(按位或运算即可

按位或 运算: | : 按位或,只要有一个1即为1,否则为0。 在当时谈论的事例中,也能够说:
假如想设置BOOL值为YES的话,那么将本来的值与掩码(该方位二进制位的值是1)进行按位或的操作即可。
例如咱们想将tall置为1

// 将倒数第三位 tall置为1
  0000 0010  // _tallRichHandsome
| 0000 0100  // TallMask
------------
  0000 0110 // 将tall置为1,其他位值都不变 

按位与 运算: &: 按位与,同真为真,其他都为假 在当时谈论的事例中,也能够说:
假如想设置BOOL值为NO的话,需求将掩码按位取反(~ : 按位取反符)(该方位二进制位的值即变成0),之后在与本来的值进行按位与操作即可。

// 将倒数第二位 rich置为0
  0000 0010  // _tallRichHandsome
& 1111 1101  // RichMask按位取反
------------
  0000 0000 // 将rich置为0,其他位值都不变

此刻set办法内部完结如下

- (void)setTall:(BOOL)tall
{
    if (tall) { // 假如需求将值置为1  // 按位或掩码
        _tallRichHandsome |= TallMask;
    }else{ // 假如需求将值置为0 // 按位与(按位取反的掩码)
        _tallRichHandsome &= ~TallMask; 
    }
}
- (void)setRich:(BOOL)rich
{
    if (rich) {
        _tallRichHandsome |= RichMask;
    }else{
        _tallRichHandsome &= ~RichMask;
    }
}
- (void)setHandsome:(BOOL)handsome
{
    if (handsome) {
        _tallRichHandsome |= HandsomeMask;
    }else{
        _tallRichHandsome &= ~HandsomeMask;
    }
} 

写完set、get办法之后经过代码来检查一下是否能够设值、取值成功。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person  = [[Person alloc] init];
        person.tall = YES;
        person.rich = NO;
        person.handsome = YES;
        NSLog(@"tall : %d, rich : %d, handsome : %d", person.tall,person.rich,person.handsome);
    }
    return 0;
} 

打印内容

Runtime - union探寻[58212:3857728] tall : 1, rich : 0, handsome : 1

能够看出上述代码能够正常赋值和取值。可是代码还是有必定的局限性:

  • 当需求增加新特点的时分,需求重复上述工作,而且代码可读性比较差
  • 接下来运用结构体的位域特性来优化上述代码

7.1.3 用 位域 技能 完结 变量的 存取

将上述代码进行优化,运用结构体位域,能够使代码可读性更高。 位域声明 位域名 : 位域长度;

运用位域需求注意以下3点:

  • 1.假如一个字节所剩空间不够寄存另一位域时,应从下一单元起寄存该位域。
    • 也能够有意使某位域从下一单元开端
    1. 位域的长度不能大于数据类型自身的长度
    • 比方int类型就不能超越32位二进位。
  • 3.位域能够无位域名,这时它只用来作填充或调整方位
    • 无名的位域是不能运用的

上述代码运用结构体位域优化之后。

@interface Person()
{
    struct {
        char handsome : 1; // 位域,代表占用一位空间
        char rich : 1;  // 按照顺序只占一位空间
        char tall : 1; 
    }_tallRichHandsome;
} 

set、get办法中能够直接经过结构体赋值和取值

- (void)setTall:(BOOL)tall
{
    _tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich
{
    _tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome
{
    _tallRichHandsome.handsome = handsome;
}
- (BOOL)tall
{
    return _tallRichHandsome.tall;
}
- (BOOL)rich
{
    return _tallRichHandsome.rich;
}
- (BOOL)handsome
{
    return _tallRichHandsome.handsome;
} 

经过代码验证一下是否能够赋值或取值正确

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person  = [[Person alloc] init];
        person.tall = YES;
        person.rich = NO;
        person.handsome = YES;
        NSLog(@"tall : %d, rich : %d, handsome : %d", person.tall,person.rich,person.handsome);
    }
    return 0;
} 

首要在log处打个断点,检查_tallRichHandsome内存储的值

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

由于_tallRichHandsome占有一个内存空间,也便是8个二进制位,咱们将05十六进制转化为二进制检查

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

上图中能够发现,倒数第三位也便是tall值为1,倒数第二位也便是rich值为0,倒数一位也便是handsome值为1,如此看来和上述代码中咱们设置的值相同。能够成功赋值。

接着持续打印内容: Runtime - union探寻[59366:4053478] tall : -1, rich : 0, handsome : -1

此刻能够发现问题,tall与handsome咱们设值为YES,讲道理应该输出的值为1为何上面输出为-1呢?

而且上面经过打印_tallRichHandsome中存储的值,也承认tallhandsome的值都为1。咱们再次打印_tallRichHandsome结构体内变量的值。

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

上图中能够发现,handsome的值为0x01,经过核算器将其转化为二进制

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

能够看到值的确为1的,为什么打印出来值为-1呢?此刻应该能够想到应该是get办法内部有问题。咱们来到get办法内部经过打印断点检查获取到的值。

- (BOOL)handsome
{
    BOOL ret = _tallRichHandsome.handsome;
    return ret;
} 

打印ret的值

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

经过打印ret的值发现其值为255,也便是1111 1111,此刻也就能解释为什么打印出来值为 -1了,首要此刻经过结构体获取到的handsome的值为0b1只占一个内存空间中的1位,可是BOOL值占有一个内存空间,也便是8位。当仅有1位的值扩展成8位的话,其他空位就会依据前面一位的值悉数补位成1,因而此刻ret的值就被映射成了0b 11111 1111

11111111 在一个字节时,有符号数则为-1,无符号数则为255。因而咱们在打印时分打印出的值为-1

为了验证当1位的值扩展成8位时,会悉数补位,咱们将tall、rich、handsome值设置为占有两位。

@interface Person()
{
    struct {
        char tall : 2;
        char rich : 2;
        char handsome : 2;
    }_tallRichHandsome;
} 

此刻在打印就发现值能够正常打印出来。 Runtime - union探寻[60827:4259630] tall : 1, rich : 0, handsome : 1

这是由于,在get办法内部获取到的_tallRichHandsome.handsome为两位的也便是0b 01,此刻在赋值给8位的BOOL类型的值时,前面的空值就会主动依据前面一位补全为0,因而回来的值为0b 0000 0001,因而打印出的值也就为1了。

因而上述问题相同能够运用!!双感叹号来处理问题。!!的原理上面现已解说过,这儿不再赘述了。

运用结构体位域优化之后的代码

@interface Person()
{
    struct {
        char tall : 1;
        char rich : 1;
        char handsome : 1;
    }_tallRichHandsome;
}
@end
@implementation Person
- (void)setTall:(BOOL)tall
{
    _tallRichHandsome.tall = tall;
}
- (void)setRich:(BOOL)rich
{
    _tallRichHandsome.rich = rich;
}
- (void)setHandsome:(BOOL)handsome
{
    _tallRichHandsome.handsome = handsome;
}
- (BOOL)tall
{
    return !!_tallRichHandsome.tall;
}
- (BOOL)rich
{
    return !!_tallRichHandsome.rich;
}
- (BOOL)handsome
{
    return !!_tallRichHandsome.handsome;
} 

上述代码中运用结构体的位域则不在需求运用掩码,使代码可读性增强了许多,可是功率相比直接运用位运算的办法来说差许多,假如想要高功率的进行数据的读取与存储一起又有较强的可读性就需求运用到共用体了。

7.1.4 用 共用体 和 来存储 变量的值

为了使代码存储数据高功率的一起,有较强的可读性,能够运用共用体来增强代码可读性,一起运用位运算来进步数据存取的功率。

运用共用体优化的代码

#define TallMask (1<<2) // 0b00000100 4
#define RichMask (1<<1) // 0b00000010 2
#define HandsomeMask (1<<0) // 0b00000001 1
@interface Person()
{
    union {
        char bits;
       // 结构体只是是为了增强代码可读性,无实质用途
        struct {
            char tall : 1;
            char rich : 1;
            char handsome : 1;
        };
    }_tallRichHandsome;
}
@end
@implementation Person
- (void)setTall:(BOOL)tall
{
    if (tall) {
        _tallRichHandsome.bits |= TallMask;
    }else{
        _tallRichHandsome.bits &= ~TallMask;
    }
}
- (void)setRich:(BOOL)rich
{
    if (rich) {
        _tallRichHandsome.bits |= RichMask;
    }else{
        _tallRichHandsome.bits &= ~RichMask;
    }
}
- (void)setHandsome:(BOOL)handsome
{
    if (handsome) {
        _tallRichHandsome.bits |= HandsomeMask;
    }else{
        _tallRichHandsome.bits &= ~HandsomeMask;
    }
}
- (BOOL)tall
{
    return !!(_tallRichHandsome.bits & TallMask);
}
- (BOOL)rich
{
    return !!(_tallRichHandsome.bits & RichMask);
}
- (BOOL)handsome
{
    return !!(_tallRichHandsome.bits & HandsomeMask);
} 

上述代码中运用位运算这种比较高效的办法存取值,运用union共用体来对数据进行存储。增加读取功率的一起增强代码可读性。

其间_tallRichHandsome共用体只占用一个字节,由于结构体中tall、rich、handsome都只占一位二进制空间,所以结构体只占一个字节,而char类型的bits也只占一个字节,他们都在共用体中,因而共用一个字节的内存即可。

而且在get、set办法中并没有运用到结构体,结构体只是为了增加代码可读性,指明共用体中存储了哪些值,以及这些值各占多少位空间。一起存值取值还运用位运算来增加功率,存储运用共用体,寄存的方位仍然经过与掩码进行位运算来操控。

此刻代码现已算是优化完结了,高效的一起可读性高,那么此刻在回头看isa_t共用体的源码

7.2 isa_t源码

此刻咱们在回头检查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
}; 

经过前面 对位运算位域以及共用体的介绍,现在再来看源码现已能够很明晰的理解其间的内容:

  • 源码中经过共用体的办法存储了64位的值,这些值在结构体中被展现出来,经过对bits进行位运算而取出相应方位的值
  • shiftcls
    • shiftcls中存储着Class、Meta-Class目标的内存地址信息
    • 咱们之前在OC目标的实质中提到过,目标的isa指针需求同ISA_MASK经过一次&(按位与)运算才干得出真正的Class目标地址

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

那么此刻咱们从头来看ISA_MASK的值0x0000000ffffffff8ULL,咱们将其转化为二进制数

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

  • 上图中能够看出ISA_MASK的值转化为二进制中有33位都为1,前面提到过按位与的效果是能够取出这33位中的值
  • 那么此刻很明显了,同ISA_MASK进行按位与运算即能够取出ClassMeta-Class的值。

一起能够看出ISA_MASK终究三位的值为0,那么任何数同ISA_MASK按位与运算之后,得到的终究三位必定都为0,因而任何类目标或元类目标的内存地址终究三位必定为0,转化为十六进制末位必定为8或许0。

7.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;
    // 里边存储的值是引证计数器减1
    uintptr_t extra_rc          : 19;
    // 引证计数器是否过大无法存储在isa中
    // 假如为1,那么引证计数会存储在一个叫SideTable的类的特点中
    uintptr_t has_sidetable_rc  : 1;
}; 

7.3.1 验证 isa中存储的信息是否可靠

经过下面一段代码验证上述信息存储的方位及效果

// 以下代码需求在真机中运转,由于真机中才是__arm64__ 位架构
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    NSLog(@"%p",[person class]);
    NSLog(@"%@",person);
} 

首要打印person类目标的地址,之后经过断点打印一下person目标的isa指针地址。

首要来看一下打印的内容

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

将类目标地址转化为二进制

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

将person的isa指针地址转化为二进制

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

shiftcls : shiftcls中存储类目标地址,经过上面两张图对比能够发现存储类目标地址的33位二进制内容彻底相同。

extra_rc : extra_rc的19位中存储着的值为引证计数减一,由于此刻person的引证计数为1,因而此刻extra_rc的19位二进制中存储的是0。

magic : magic的6位用于在调试时分辩目标是否未完结初始化,上述代码中person现已完结初始化,那么此刻这6位二进制中存储的值011010即为共用体中界说的宏# define ISA_MAGIC_VALUE 0x000001a000000001ULL的值。

nonpointer : 这儿肯定是运用的优化后的isa,因而nonpointer的值肯定为1

由于此刻person目标没有相关目标而且没有弱指针引证过,能够看出has_assocweakly_referenced值都为0,接着咱们为person目标增加弱引证和相关目标,来调查一下has_assocweakly_referenced的改变。

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    NSLog(@"%p",[person class]);
    // 为person增加弱引证
    __weak Person *weakPerson = person;
    // 为person增加相关目标
    objc_setAssociatedObject(person, @"name", @"xx_cc", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    NSLog(@"%@",person);
} 

从头打印person的isa指针地址将其转化为二进制能够看到has_assocweakly_referenced的值都变成了1

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

注意:只要设置过相关目标或许弱引证引证过目标has_assocweakly_referenced的值就会变成1,不管之后是否将相关目标置为nil或断开弱引证。

假如没有设置过相关目标,目标开释时会更快,这是由于目标在毁掉时会判别是否有相关目标从而对相关目标开释。来看一下目标毁掉的源码

void *objc_destructInstance(id obj)
{
    if (obj) {
        Class isa = obj->getIsa();
        // 是否有c++析构函数
        if (isa->hasCxxDtor()) {
            object_cxxDestruct(obj);
        }
        // 是否有相关目标,假如有则移除
        if (isa->instancesHaveAssociatedObjects()) {
            _object_remove_assocations(obj);
        }
        objc_clear_deallocating(obj);
    }
    return obj;
} 

相信至此咱们现已对isa指针有了新的知道:

  • arm64架构之后,isa指针不单单只存储了ClassMeta-Class的地址,而是运用共用体的办法存储了更多信息
  • 其间shiftcls存储了ClassMeta-Class的地址,需求同ISA_MASK进行按位&运算才干够取出其内存地址值。

三、class的结构

1. 回忆一下Class的内部结构

咱们在之前在探求OC的三类目标的时分,从简略探求过Class的内部结构,且对Class结构的知道终究以一张图作总结:

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

咱们在前面的篇幅中对isa指针有了新的知道之后,也需求基于此 对Class有 进一步的探求,从头知道Class内部结构:

首要回忆一下Class的内部结构相关的源码:

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
    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
} 
class_rw_t* data() {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}  

1.1 class_rw_t

从源码中咱们不难得知:

  • bits & FAST_DATA_MASK位运算之后,能够得到class_rw_t
  • class_rw_t中存储着办法列表、特点列表以及协议列表等
  • 来看一下class_rw_t部分代码:
    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 firstSubclass;
        Class nextSiblingClass;
        char *demangledName;
    }; 
    
    • class_rw_t结构体内部的成员:method_array_tproperty_array_tprotocol_array_t其实都是二维数组
    • 咱们能够去看下method_array_tproperty_array_tprotocol_array_t的内部结构:
      class method_array_t :
          public list_array_tt<method_t, method_list_t> 
      {
          typedef list_array_tt<method_t, method_list_t> Super;
       public:
          method_list_t **beginCategoryMethodLists() {
              return beginLists();
          }
          method_list_t **endCategoryMethodLists(Class cls);
          method_array_t duplicate() {
              return Super::duplicate<method_array_t>();
          }
      };
      class property_array_t : 
          public list_array_tt<property_t, property_list_t> 
      {
          typedef list_array_tt<property_t, property_list_t> Super;
       public:
          property_array_t duplicate() {
              return Super::duplicate<property_array_t>();
          }
      };
      class protocol_array_t : 
          public list_array_tt<protocol_ref_t, protocol_list_t> 
      {
          typedef list_array_tt<protocol_ref_t, protocol_list_t> Super;
       public:
          protocol_array_t duplicate() {
              return Super::duplicate<protocol_array_t>();
          }
      };
      
    • 咱们这儿以method_array_t为例,剖析一下其二维数组的构成:
      • method_array_t自身便是一个数组,数组里边寄存的是数组method_list_t
      • method_list_t里边终究寄存的是method_t
      • method_t是一个办法目标

class_rw_t里边的methods、properties、protocols是二维数组,是可读可写的,其间包含了类的初始内容以及分类的内容。 (这儿以methods为例,实际上propertiesprotocols都是相似的构成)

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

1.2 class_ro_t

咱们之前提到过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;
    }
}; 

class_rw_t的源码中咱们能够看到class_ro_t *ro成员,可是其是被const润饰的,也便是是只读,不可修正的。咱们进一步去看一下class_ro_t的内部结构咱们能够得知:

  • 内部直接存储的直接便是method_list_t、protocol_list_t 、property_list_t类型的一维数组
  • 数组里边别离寄存的是类的初始信息
  • method_list_t为例,method_list_t中直接寄存的便是method_t,可是是只读的,不允许增修改查。

1.3 总结

以办法列表为例,class_rw_t中的methods是二维数组的结构,而且可读可写

  • 因而能够动态的增加办法,而且更加便于分类办法的增加
  • 由于咱们在Category的实质里边提到过,attachList函数内经过memmove 和 memcpy两个操作将分类的办法列表兼并在本类的办法列表中(也即是class_rw_tmethods中)
  • 那么此刻就将分类的办法和本类的办法统一整合到一起了

其实一开端类的办法,特点,成员变量特点协议等等都是寄存在class_ro_t中的

  • 当程序运转的时分,需求将分类中的列表跟类初始的列表兼并在一起的时,就会将class_ro_t中的列表和分类中的列表兼并起来寄存在class_rw_t
  • 也便是说class_rw_t中有部分列表是从class_ro_t里边拿出来的。而且终究和分类的办法兼并
  • 能够经过源码看到这一部分的完结:
    static Class realizeClass(Class cls)
    {
        runtimeLock.assertWriting();
        const class_ro_t *ro;
        class_rw_t *rw;
        Class supercls;
        Class metacls;
        bool isMeta;
        if (!cls) return nil;
        if (cls->isRealized()) return cls;
        assert(cls == remapClass(cls));
        // 最开端cls->data是指向ro的
        ro = (const class_ro_t *)cls->data();
        if (ro->flags & RO_FUTURE) { 
            // rw现已初始化而且分配内存空间
            rw = cls->data();  // cls->data指向rw
            ro = cls->data()->ro;  // cls->data()->ro指向ro
            cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
        } else { 
            // 假如rw并不存在,则为rw分配空间
            rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); // 分配空间
            rw->ro = ro;  // rw->ro从头指向ro
            rw->flags = RW_REALIZED|RW_REALIZING;
            // 将rw传入setData函数,等于cls->data()从头指向rw
            cls->setData(rw); 
        }
    } 
    

源码解读: 那么从上述源码中能够发现:

  • 类的初始信息本来其实是存储在class_ro_t中的
    • 而且ro本来是指向cls->data()
    • 也便是说bits.data()得到的是ro
  • 可是在运转进程中创立了class_rw_t,并将cls->data指向rw
    • 一起将初始信息ro赋值给rw中的ro
    • 终究在经过setData(rw)设置data
  • 那么此刻bits.data()得到的便是rw
  • 之后再去检查是否有分类,一起将分类的办法特点协议列表整合存储在class_rw_t办法特点协议列表中

经过上述对源码的剖析,咱们对class_rw_t内存储办法特点协议列表的进程有了更明晰的知道,那么接下来探寻class_rw_t中是怎么存储办法的。

2. class_rw_t中是怎么存储办法的?

2.1 method_t

咱们知道 method_array_t中终究存储的是method_t

  • method_t是对办法、函数的封装,每一个办法目标便是一个method_t
  • 经过源码看一下method_t的结构体:
struct method_t {
    SEL name;  // 函数名
    const char *types;  // 编码(回来值类型,参数类型)
    IMP imp; // 指向函数的指针(函数地址)
}; 

method_t结构体中能够看到三个成员,咱们依次来看看这三个成员变量别离代表什么:

2.1.1 SEL

SEL代表办法\函数名,一般叫做选择器,底层结构跟char *相似

  • SEL能够经过@selector()sel_registerName()取得
    SEL sel1 = @selector(test);
    SEL sel2 = sel_registerName("test");
    
  • 也能够经过sel_getName()NSStringFromSelector()SEL转成字符串
    char *string = sel_getName(sel1);
    NSString *string2 = NSStringFromSelector(sel2);
    
  • 不同类中相同姓名的办法,所对应的办法选择器是相同的
    • SEL只是代表办法的姓名,而且不同类中相同的办法名的SEL是大局唯一的。
    NSLog(@"%p,%p", sel1,sel2);
    Runtime-test[23738:8888825] 0x1017718a3,0x1017718a3
    

typedef struct objc_selector *SEL;,能够把SEL看做是办法姓名符串。

2.1.2 types

types包含了函数回来值,参数编码的字符串

  • 经过字符串拼接的办法将回来值和参数拼接成一个字符串
  • 这个字符串能够用于 代表函数回来值参数

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

咱们经过代码检查一下types是怎么代表函数回来值及参数的:

  • 首要经过在本地写几个与runtime底层完结class相同的结构体,用于模仿Class的内部完结
  • 咱们曾在探寻Class的实质时,做过该操作:经过类型强制转化来探寻内部数据
Person *person = [[Person alloc] init];
xx_objc_class *cls = (__bridge xx_objc_class *)[Person class];
class_rw_t *data = cls->data();

经过断点能够在data中找到types的值

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

  • 上图中能够看出types的值为v16@0:8
  • 那么这个值代表什么呢?
  • apple为了能够明晰的运用字符串表明办法及其回来值,制定了一系列对应规矩,经过下表能够看到逐个对应联系
    12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

将types的值同表中的逐个对照检查types的值v16@0:8 代表什么

- (void) test;
 v    16      @     0     :     8
void         id          SEL
// 16表明参数的占用空间巨细,id后边跟的0表明从0位开端存储,id占8位空间。
// SEL后边的8表明从第8位开端存储,SEL相同占8位空间

咱们知道任何办法都默认有两个参数的,id类型的self,和SEL类型的_cmd,而上述经过对types的剖析一起也验证了这个说法。

为了能够看的更加明晰,咱们为test增加回来值及参数之后从头检查types的值。

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

相同经过上表找出逐个对应的值,检查types的值代表的办法

- (int)testWithAge:(int)age Height:(float)height
{
    return 0;
}
  i    24    @    0    :    8    i    16    f    20
int         id        SEL       int        float
// 参数的总占用空间为 8 + 8 + 4 + 4 = 24
// id 从第0位开端占有8位空间
// SEL 从第8位开端占有8位空间
// int 从第16位开端占有4位空间
// float 从第20位开端占有4位空间

iOS提供了@encode的指令,能够将具体的类型转化成字符串编码。

NSLog(@"%s",@encode(int));
NSLog(@"%s",@encode(float));
NSLog(@"%s",@encode(id));
NSLog(@"%s",@encode(SEL));
// 打印内容
Runtime-test[25275:9144176] i
Runtime-test[25275:9144176] f
Runtime-test[25275:9144176] @
Runtime-test[25275:9144176] :

上述代码中能够看到,对应联系的确如上表所示。

2.1.3 IMP

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】
IMP代表函数的具体完结

  • 存储的内容是函数地址
  • 也便是说当找到IMP的时分就能够找到函数完结,从而对函数进行调用

在上述代码中打印IMP的值

Printing description of data->methods->first.imp:
(IMP) imp = 0x000000010c66a4a0 (Runtime-test`-[Person testWithAge:Height:] at Person.m:13)

之后在test办法内部打印断点,并来到其办法内部能够看出IMP中的存储的地址也便是办法完结的地址。

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

四、cache_t办法缓存

经过前面的探求咱们知道了办法列表是怎么存储在Class类目标中的

  • 可是当屡次承继的子类想要调用基类办法时,就需求经过superclass指针一层一层找到基类,在从基类办法列表中找到对应的办法进行调用
  • 假如屡次调用基类办法,那么就需求屡次遍历每一层父类的办法列表,这对功能来说无疑是伤害巨大的

Apple经过办法缓存技能的办法处理了这一问题,接下来咱们来探寻Class类目标是怎么进行办法缓存的

回到类目标结构体objc_class。里边有一个成员变量cache

  • 这个cache成员变量便是用于完结 办法缓存技能 的支撑
    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
        class_rw_t *data() { 
            return bits.data();
        }
        void setData(class_rw_t *newData) {
            bits.setData(newData);
        }
    } 
    
  • Class内部结构中有个办法缓存(cache_t),用散列表哈希)来缓存曾经调用过的办法,能够进步办法的查找速度

回忆办法调用进程:

  • 调用办法的时分,需求去办法列表里边进行遍历查找
    • 假如办法不在列表里边,就会经过superclass找到父类的类目标,在去父类类目标办法列表里边遍历查找。
  • 假如办法需求调用许屡次的话,那就相当于每次调用都需求去遍历屡次办法列表

cache_t技能

  • 为了能够快速查找办法,apple规划了cache_t来进行办法缓存:
  • 每逢调用办法的时分,会先去cache中查找是否有缓存的办法:
    • 假如没有缓存,在去类目标办法列表中查找,以此类推直到找到办法之后,就会将办法直接存储在cache
    • 下一次在调用这个办法的时分,就会在类目标的cache里边找到这个办法,直接调用了

1. cache_t 怎么进行缓存

那么cache_t是怎么对办法进行缓存的呢?首要来看一下cache_t的内部结构。

struct cache_t {
    struct bucket_t *_buckets; // 散列表 数组
    mask_t _mask; // 散列表的长度 -1
    mask_t _occupied; // 现已缓存的办法数量
}; 

bucket_t是以数组的办法存储办法列表的,看一下bucket_t内部结构

struct bucket_t {
private:
    cache_key_t _key; // SEL作为Key
    IMP _imp; // 函数的内存地址
}; 

从源码中能够看出:

  • bucket_t中存储着SEL_imp
  • 经过key->value的办法:
    • SELkey
    • 函数完结的内存地址 _impvalue来存储办法

经过一张图来展现一下cache_t的结构

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

办法散列表bucket_t

  • 上述bucket_t列表咱们称之为散列表(哈希表)
  • 散列表(Hash table,也叫哈希表),是依据要害码值(Key value)而直接进行访问的数据结构
  • 也便是说,它经过把要害码值映射到表中一个方位来访问记载,以加快查找的速度。
  • 这个映射函数叫做散列函数,寄存记载的数组叫做散列表

那么apple怎么在散列表中快速而且准确的找到对应的key以及函数完结呢?
这就需求咱们经过源码来看一下apple的散列函数是怎么规划的:

2.散列函数及散列表原理

首要来看一下办法缓存的源码(首要检查几个函数,要害代码都有注释,便不再打开介绍)

2.1 cache_fill 及 cache_fill_nolock 函数

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();
    // 假如没有initialize直接return
    if (!cls->isInitialized()) return;
    // 确保线程安全,没有其他线程增加缓存
    if (cache_getImp(cls, sel)) return;
    // 经过类目标获取到cache 
    cache_t *cache = getCache(cls);
    // 将SEL包装成Key
    cache_key_t key = getKey(sel);
   // 占用空间+1
    mask_t newOccupied = cache->occupied() + 1;
   // 获取缓存列表的缓存才能,能存储多少个键值对
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // 假如为空的,则创立空间,这儿创立的空间为4个。
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // 假如所占用的空间占总数的3/4一下,则持续运用现在的空间
    }
    else {
       // 假如占用空间超越3/4则扩展空间
        cache->expand();
    }
    // 经过key查找适宜的存储空间。
    bucket_t *bucket = cache->find(key, receiver);
    // 假如key==0则阐明之前未存储过这个key,占用空间+1
    if (bucket->key() == 0) cache->incrementOccupied();
    // 存储key,imp 
    bucket->set(key, imp);
} 

2.2 expand ()函数

当散列表的空间被占用超越3/4的时分,散列表会调用expand ()函数进行扩展,咱们来看一下expand ()函数内散列表怎么进行扩展的。

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    // 获取旧的散列表的存储空间
    uint32_t oldCapacity = capacity();
    // 将旧的散列表存储空间扩容至两倍
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    // 为新的存储空间赋值
    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        newCapacity = oldCapacity;
    }
    // 调用reallocate函数,从头创立存储空间
    reallocate(oldCapacity, newCapacity);
} 

2.3 reallocate 函数

经过上述源码看到reallocate函数担任分配散列表空间,来到reallocate函数内部。

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    // 旧的散列表能否被开释
    bool freeOld = canBeFreed();
    // 获取旧的散列表
    bucket_t *oldBuckets = buckets();
    // 经过新的空间需求量创立新的散列表
    bucket_t *newBuckets = allocateBuckets(newCapacity);
    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    // 设置Buckets和Mash,Mask的值为散列表长度-1
    setBucketsAndMask(newBuckets, newCapacity - 1);
    // 开释旧的散列表
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
} 

上述源码中初次传入reallocate函数的newCapacityINIT_CACHE_SIZEINIT_CACHE_SIZE是个枚举值,也便是4。因而散列表开始创立的空间便是4个。

enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
}; 

上述源码中能够发现散列表进行扩容时会将容量增至之前的2倍。

2.4 find 函数

终究来看一下散列表中怎么快速的经过key找到相应的bucket呢?咱们来到find函数内部

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);
    // 获取散列表
    bucket_t *b = buckets();
    // 获取mask
    mask_t m = mask();
    // 经过key找到key在散列表中存储的下标
    mask_t begin = cache_hash(k, m);
    // 将下标赋值给i
    mask_t i = begin;
    // 假如下标i中存储的bucket的key==0阐明当时没有存储相应的key,将b[i]回来出去进行存储
    // 假如下标i中存储的bucket的key==k,阐明当时空间内现已存储了相应key,将b[i]回来出去进行存储
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            // 假如满足条件则直接reutrn出去
            return &b[i];
        }
    // 假如走到这儿阐明上面不满足,那么会往前移动一个空间从头进行判定,知道能够成功return停止
    } while ((i = cache_next(i, m)) != begin);
    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
} 

函数cache_hash (k, m)用来经过key找到办法在散列表中存储的下标,来到cache_hash (k, m)函数内部

static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
    return (mask_t)(key & mask);
} 

能够发现cache_hash (k, m)函数内部只是是进行了key & mask的按位与运算,得到下标即存储在相应的方位上。按位与运算在上文中已具体解说过,这儿不在赘述。

2.5 _mask

经过上面的剖析咱们知道_mask的值是散列表的长度减一,那么任何数经过与_mask进行按位与运算之后取得的值都会小于等于_mask,因而不会出现数组溢出的情况。

举个比如,假设散列表的长度为8,那么mask的值为7

  0101 1011  // 任意值
& 0000 0111  // mask = 7
------------
  0000 0011 //获取的值一直等于或小于mask的值 

3.办法调用总结

  • 初次办法查找与缓存:
    • 初次办法查找: 当第一次运用办法时,音讯机制经过isa指针找到class/meta-class
    • 办法缓存: 遍历办法列表找到办法之后(假如找不到就调用superclass去父类中找),会对办法以SEL为keyIMP为value的办法缓存在cache_buckets
    • 散列表下标: 当第一次存储的时分,会创立具有4个空间的散列表,并将_mask的值置为散列表的长度减一,之后经过SEL & mask核算出办法存储的下标值,并将办法存储在散列表中
      • 举个比如,假如核算出下标值为3,那么就将办法直接存储在下标为3的空间中,前面的空间会留空
  • 散列表扩容:
    • 当散列表中存储的办法占有散列表长度超越3/4的时分,散列表会进行扩容操作:
      • 将创立一个新的散列表而且空间扩容至原来空间的两倍
      • 并重置_mask的值
      • 终究开释旧的散列表
    • 此刻再有办法要进行缓存的话,就需求从头经过SEL & mask核算出下标值之后在按照下标进行存储了
  • 散列表下标核算:
    • 假如一个类中办法许多,其间很可能会出现多个办法的SEL & mask得到的值为同一个下标值
    • 假如核算出来的下标有值,那么会调用cache_next函数往下标值-1位去进行存储
    • 假如下标值-1位空间中有存储办法,而且key不与要存储的key相同,那么再到前面一位进行比较,直到找到一位空间没有存储办法或许key与要存储的key相同停止
    • 假如到下标0的话就会到下标为_mask的空间也便是最大空间处进行比较。
  • 非初次办法查找:
    • 当要查找办法时,并不需求遍历散列表,相同经过SEL & mask核算出下标值,直接去下标值的空间取值即可
    • 同上,假如下标值中存储的key与要查找的key不相同,就去前面一位查找。
    • 这样尽管占用了少量空间,可是大大节省了时刻,也便是说其实apple是运用空间交换时刻的一种办法查找算法优化测战略。

经过一张图更明晰的看一下其间的流程:

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

4. 验证上述流程

经过一段代码演示一下 。相同运用仿照objc_class结构体自界说一个结构体,并进行强制转化来检查其内部数据,自界说结构体在之前的文章中运用过屡次这儿不在赘述。

咱们创立Person类承继NSObjectStudent类承继PersonCollegeStudent承继Student。三个类别离有personTest,studentTest,colleaeStudentTest办法

经过打印断点来看一下办法缓存的进程

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CollegeStudent *collegeStudent = [[CollegeStudent alloc] init];
        xx_objc_class *collegeStudentClass = (__bridge xx_objc_class *)[CollegeStudent class];
        cache_t cache = collegeStudentClass->cache;
        bucket_t *buckets = cache._buckets;
        [collegeStudent personTest];
        [collegeStudent studentTest];
        NSLog(@"----------------------------");
        for (int i = 0; i <= cache._mask; i++) {
            bucket_t bucket = buckets[i];
            NSLog(@"%s %p", bucket._key, bucket._imp);
        }
        NSLog(@"----------------------------");
        [collegeStudent colleaeStudentTest];
        cache = collegeStudentClass->cache;
        buckets = cache._buckets;
        NSLog(@"----------------------------");
        for (int i = 0; i <= cache._mask; i++) {
            bucket_t bucket = buckets[i];
            NSLog(@"%s %p", bucket._key, bucket._imp);
        }
        NSLog(@"----------------------------");
        NSLog(@"%p",@selector(colleaeStudentTest));
        NSLog(@"----------------------------");
    }
    return 0;
} 

咱们别离在collegeStudent实例目标调用personTest,studentTest,colleaeStudentTest办法处打断点检查cache的改变。

personTest办法调用之前:

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

从上图中能够发现:

  • personTest办法调用之前,cache中只是存储了init办法
  • 上图中能够看出init办法刚好存储在下标为0的方位因而咱们能够看到
  • _mask的值为3验证咱们上述源码中提到的散列表第一次存储时会分配4个内存空间
  • _occupied的值为1证明此刻_buckets中只是存储了一个办法。

collegeStudent在调用personTest的时分:

  • 首要发现collegeStudent类目标cache中没有personTest办法,就会去collegeStudent类目标的办法列表中查找
  • 办法列表中也没有,那么就经过superclass指针找到Student类目标
  • Studeng类目标cache和办法列表相同没有,再经过superclass指针找到Person类目标
  • 终究在Person类目标办法列表中找到之后进行调用,并缓存在collegeStudent类目标cache中。

履行personTest办法之后检查cache办法的改变:

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

上图中能够发现:

  • _occupied值为2,阐明此刻personTest办法现已被缓存在collegeStudent类目标cache

同理履行过studentTest办法之后,咱们经过打印检查一下此刻cache内存储的信息

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

上图中能够看到cache中的确存储了 init 、personTest 、studentTest三个办法。

那么履行过colleaeStudentTest办法之后此刻cache中应该对colleaeStudentTest办法进行缓存。

前面源码提到过,当存储的办法数超越散列表长度的3/4时,体系会从头创立一个容量为原来两倍的新的散列表替代原来的散列表。
过掉colleaeStudentTest办法,从头打印cache内存储的办法检查:

12-探究iOS底层原理|Runtime【isa详解、class的结构、方法缓存cache_t】

从图中可看出:

  • _bucket散列表扩容之后只是存储了colleaeStudentTest办法
  • 而且上图中打印SEL & _mask 位运算得出下标的值的确是_bucket列表中colleaeStudentTest办法存储的方位

至此现已对Class的结构及办法缓存的进程有了新的认知:

  • apple经过散列表的办法对办法进行缓存,以少量的空间节省了大量查找办法的时刻

专题系列文章

1.前常识

  • 01-探求iOS底层原理|总述
  • 02-探求iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM】
  • 03-探求iOS底层原理|LLDB
  • 04-探求iOS底层原理|ARM64汇编

2. 基于OC言语探求iOS底层原理

  • 05-探求iOS底层原理|OC的实质
  • 06-探求iOS底层原理|OC目标的实质
  • 07-探求iOS底层原理|几种OC目标【实例目标、类目标、元类】、目标的isa指针、superclass、目标的办法调用、Class的底层实质
  • 08-探求iOS底层原理|Category底层结构、App启动时Class与Category装载进程、load 和 initialize 履行、相关目标
  • 09-探求iOS底层原理|KVO
  • 10-探求iOS底层原理|KVC
  • 11-探求iOS底层原理|探求Block的实质|【Block的数据类型(实质)与内存布局、变量捕获、Block的品种、内存办理、Block的润饰符、循环引证】
  • 12-探求iOS底层原理|Runtime1【isa详解、class的结构、办法缓存cache_t】
  • 13-探求iOS底层原理|Runtime2【音讯处理(发送、转发)&&动态办法解析、super的实质】
  • 14-探求iOS底层原理|Runtime3【Runtime的相关使用】
  • 15-探求iOS底层原理|RunLoop【两种RunloopMode、RunLoopMode中的Source0、Source1、Timer、Observer】
  • 16-探求iOS底层原理|RunLoop的使用
  • 17-探求iOS底层原理|多线程技能的底层原理【GCD源码剖析1:主队列、串行队列&&并行队列、大局并发队列】
  • 18-探求iOS底层原理|多线程技能【GCD源码剖析1:dispatch_get_global_queue与dispatch_(a)sync、单例、线程死锁】
  • 19-探求iOS底层原理|多线程技能【GCD源码剖析2:栅门函数dispatch_barrier_(a)sync、信号量dispatch_semaphore】
  • 20-探求iOS底层原理|多线程技能【GCD源码剖析3:线程调度组dispatch_group、事情源dispatch Source】
  • 21-探求iOS底层原理|多线程技能【线程锁:自旋锁、互斥锁、递归锁】
  • 22-探求iOS底层原理|多线程技能【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】
  • 23-探求iOS底层原理|内存办理【Mach-O文件、Tagged Pointer、目标的内存办理、copy、引证计数、weak指针、autorelease

3. 基于Swift言语探求iOS底层原理

关于函数枚举可选项结构体闭包特点办法swift多态原理StringArrayDictionary引证计数MetaData等Swift根本语法和相关的底层原理文章有如下几篇:

  • Swift5中心语法1-根底语法
  • Swift5中心语法2-面向目标语法1
  • Swift5中心语法2-面向目标语法2
  • Swift5常用中心语法3-其它常用语法
  • Swift5使用实践常用技能点

其它底层原理专题

1.底层原理相关专题

  • 01-核算机原理|核算机图形烘托原理这篇文章
  • 02-核算机原理|移动终端屏幕成像与卡顿

2.iOS相关专题

  • 01-iOS底层原理|iOS的各个烘托结构以及iOS图层烘托原理
  • 02-iOS底层原理|iOS动画烘托原理
  • 03-iOS底层原理|iOS OffScreen Rendering 离屏烘托原理
  • 04-iOS底层原理|因CPU、GPU资源消耗导致卡顿的原因和处理计划

3.webApp相关专题

  • 01-Web和类RN大前端的烘托原理

4.跨渠道开发计划相关专题

  • 01-Flutter页面烘托原理

5.阶段性总结:Native、WebApp、跨渠道开发三种计划功能比较

  • 01-Native、WebApp、跨渠道开发三种计划功能比较

6.Android、HarmonyOS页面烘托专题

  • 01-Android页面烘托原理
  • 02-HarmonyOS页面烘托原理 (待输出)

7.小程序页面烘托专题

  • 01-小程序结构烘托原理