iOS之Runtime

Runtime是什么

Runtime是一个库,这个库使我们可以在程序运行时创建对象、检查对象,修改类和对象的方法。

  • OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行;
  • Runtime是一套C语言的API,封装了很多动态性相关的函数;
  • OC的动态性就是由Runtime API来支撑和实现的

基础知识

  1. arm64架构之前,isa就是一个普通的指针,存储着ClassMeta-Class对象的内存地址;
  2. 从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息;
  3. 类对象、元类对象的地址值(二进制)最后三位一定是0,所以16进制的地址值末位总是0或者8;

isa的位域信息:

​ isa共用体的64位数中有33位存储的是类/元类的内存地址信息

iOS中对象的底层结构:

类Class的结构

类和元类本质上都是Class,Class是一个objc_class类型的结构体指针:

1
typedef struct objc_class *Class;

class_rw_t的结构

1
2
3
4
5
6
7
struct class_rw_t {
const class_ro_t *ro;
method_list_t *methods;
property_list_t *properties;
const protocol_list_t *protocols;
...
}
  • 可读可写:可以动态地给对象添加协议、方法、属性

  • class_rw_t里面的methodspropertiesprotocols是二维数组,包含了类的初始内容和category的内容。

class_ro_t的结构

  • 包含了类的初始内容:成员变量、属性、方法、协议

  • baseMethodListbaseProtocolsivarsbaseProperties是一维数组

  • 只读的:类注册完成后,无法动态地添加成员变量

method_t的结构

method_t是对方法/函数的封装

1
2
3
4
5
struct method_t {
SEL name; // 函数名
const char *types; // 函数编码,包含返回值类型、参数类型
IMP imp; // 函数地址
}
  • IMP:代表数据的具体实现

    typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);

  • SEL:方法/函数名,一般叫做选择器,底层结构跟char *类似

    typedef struct objc_selector *SEL;

    获取SEL@selector()sel_registerName()

    获取SEL的字符串格式:NSStringFromSelector()或者sel_getName()

    不同类中相同名字的方法,所对应方法选择器是相同的

  • types:包含了函数的返回值、参数编码的字符串

    比如- (int)age:(int)age height:(float)height

    函数的types是"i 24 @ 0 : 8 i 16 f 20"

    i:返回值类型”int”;

    24:所有参数占的字节数;

    @:第一个参数,”id”类型;指针,8个字节;

    ::第二个参数: “SEL”; 指针,8个字节;

    i:第三个参数,”int”;4

    f:第四个参数, “float”;4

    “0”、”8”、”16”、”20” : 参数从多少字节开始

方法缓存cache_t

Class内部结构中有个方法缓存cache_t,是用散列表来缓存曾经调用过的方法,可以提高方法的查找速度

缓存方法过程:

  1. 通过方法名 & 当前的_mask 计算出下标
  2. 将方法存放到下标位置
  3. 如果当前位置已经存储了别的方法,那么将下标-1,向上存储
  4. 如果第一个位置也存储了,就从最后一个位置继续
  5. 如果全部都满了,就会重新分配内存,重新进行缓存

如果缓存列表满了,会清空缓存列表,扩容内存为原来的两倍,等待下次调用方法的时候重新进行缓存,因为 _mask 存放的数量 已经改变了,进行 &运算的时候,肯定是和之前的值不一样,所以需要清空,重新缓存

SideTable

1
2
3
4
5
6
7
// NSObject.mm
struct SideTable {
spinlock_t slock; // 自旋锁
RefcountMap refcnts; // 引用计数表(散列表)
weak_table_t weak_table; // 弱引用表(散列表)
......
}

SideTable存储在SideTables()中,SideTables()本质也是一个散列表,可以通过对象指针来获取它对应的(引用计数表或者弱引用表)在哪一个SideTable中。在非嵌入式系统下,SideTables()中有 64 个SideTable

SideTables()的定义:

1
2
3
4
5
6
// NSObject.mm
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}

查找对象的引用计数表需要经过两次哈希查找:

​ ① 第一次根据当前对象的内存地址,经过哈希查找从SideTables()中取出它所在的SideTable

​ ② 第二次根据当前对象的内存地址,经过哈希查找从SideTable中的refcnts中取出它的引用计数表。

为什么不是一个SideTable,而是使用多个SideTable组成SideTables()结构?

如果只有一个SideTable,那我们在内存中分配的所有对象的引用计数或者弱引用都放在这个SideTable中,那我们对对象的引用计数进行操作时,为了多线程安全就要加锁,就存在效率问题。 系统为了解决这个问题,就引入 “分离锁” 技术方案,提高访问效率。把对象的引用计数表分拆多个部分,对每个部分分别加锁,那么当所属不同部分的对象进行引用操作的时候,在多线程下就可以并发操作。所以,使用多个SideTable组成SideTables()结构。

[super message]的本质

  • 底层汇编实现——objc_msgSendSuper(struct objc_super, SEL)函数:

    该方法第一个参数是个结构体,第二个参数是方法名;
    结构体中第一个成员表示receiver,第二个成员是父类对象,用来表示从父类开始查找message()方法的实现;

    1
    2
    3
    4
    5
    6
    7
    struct objc_super {
    id receiver;
    Class super_class; // receiver的父类对象
    }

    // Student: Person: NSObject
    objc_msgSendSuper({self, [Person class]}, @selelctor(message));
  • 运行时实现——objc_msgSendSuper2(struct objc_super2, SEL)函数:

    receiver是消息接收者, current_classreceiver的类对象。

    objc_msgSendSuper2函数是通过传入的类对象的superclass指针找到当前类的父类,并从父类开始查找方法的实现的。

    1
    2
    3
    4
    5
    6
    struct objc_super2 {
    id receiver;
    Class current_class; // 当前类的类对象
    };

    objc_msgSendSuper({self, [Student class]}, @selelctor(message));

isKindOfClass和isMemberOfClass

  • 作为实例方法:**

    [obj isMemberOfClass: [XXX class]]:判断obj是否是XXX的实例对象

    [obj isKindOfClass: [XXX class]]:判断obj是否是XXX或其子类的实例对象

  • 作为类方法:**

    isKindOfClassisMemberOfClass后面应该传元类对象,结果才是YES:

    1
    2
    3
    4
    5
    6
    7
    // ZZPerson: NSObject
    // NO, ZZPerson是ZZPerson的元类对象的实例
    [ZZPerson isMemeberOfClass: [ZZperson class]];
    // NO, ZZPerson是ZZPerson的元类对象NSObject的元类也就是NSObject的实例
    [ZZPerson isKindOfClass: [ZZperson class]];
    // YES,OC中任意一个类最终都继承自NSObject,NSObject的元类对象仍是NSObject
    [ZZPerson isKindOfClass: [NSObject class]];

消息发送机制

OC中的方法调用,都是转换为objc_msgSend(receiver, SEL) objc_msgSend的执行流程分为三大阶段:消息发送——动态方法解析——消息转发。

  1. 消息发送阶段

  2. 动态方法解析

  3. 消息转发

Runtime的应用

  • 修改私有属性
  • 字典转模型
  • 自动归档解档
  • 交换方法实现
  • 利用关联对象给分类添加属性
  • 利用消息转发机制解决方法找不到的异常问题
  • 热修复(JSPatch实现)