Runtime

Runtime相关介绍

Runtime

组成

  • 数据结构
    • objc_object
    • objc_class
    • isa指针
    • method_t
  • 类对象与元类对象
  • 消息传递
  • 方法缓存
  • 消息转发
  • Method-Swizzling 可做代码混淆
  • 动态添加方法
  • 动态方法解析

objc_object

objc_class

isa指针


isa是什么?
isa首先分为指针型isa和非指针型isa
对于ARM64架构下,只需要三四十位就能代表Class的地址,剩余的位数就可以存储其他数据。以此达到节省内存的目的。

isa指向

cache_t

  • 用于快速查找方法执行函数
  • 是可增量扩展哈希表结构
  • 局部性原理的最佳应用
    • 局部性原理的简单说明:一个类中部分方法使用频次较高,就将这些方法放入缓存中,这样下次调用方法,直接从缓存中读取的概率就比较高。 cache_t的数据结构
      是个数组类型,成员是bucket_t类型
      bucket_t包含 keyIMP
      key就是方法选择器的名称,通过SEL调用
      IMP是个无类型的函数指针
      根据key,用哈希查找算法,定位当前key所对应的bucket_t类型的数据结构,位于数据哪个位置,然后根据IMP调用函数

class_data_bits_t

  • class_data_bits_t主要是对class_rw_t的封装
  • class_rw_t代表了类相关的读写信息(如分类,协议,方法,属性等)、对class_ro_t的封装(readonly 成员变量,内部方法等)
  • class_ro_t代表了类相关的只读信息

class_rw_t

一般情况下,都是存储分类方法添加的内容

class_ro_t

method_t

Type Encodings

  • const char* types;

整体数据结构


对象、类对象、元类对象

  • 类对象存储实例方法列表等信息
  • 元类对象存储类方法列表等信息


Q:类对象和元类对象有什么区别?
A:1.实例对象可以通过isa指针找到类对象,访问实例方法列表并通过类的isa指针找到元类对象,访问类方法列表等。
2.任何一个元类对象的isa指针,都指向根元类对象,包括根元类对象本身。

Q:如果我们调用的一个类方法,没有对应的实现,但是根类有同名的实例方法的时候,这时候会不会发生崩溃,会不会产生实际调用?
A: 由于根元类的superClass指针,指向根类,所以会调用根类的同名实例方法。

消息传递简述
调用实例方法a,根据isa指针,找到对应的类的方法列表,然后找父类,然后父类,最后nil
调用类方法a,通过类的isa指针,找到对应的类,然后找类的方法列表,然后根据superClass指针找父类的,到根类,最后nil

消息传递

1
void objc_msgSend(void /* id self,SEL op, ... */)
1
void objc_msgSendSuper(void /* struct objc_super *super,SEL op, ...*/)


>答案:都是 Phone
>理由:因为objc_msgSend和objc_msgSendSuper的消息接收者,都是self这个实例对象。
>objc_msgSendSuper只是代表,寻找方法从父类开始查找,而class这个方法是NSObject的方法,父类Mobile中也没有,所以最终都是Phone

缓存查找

当前类中查找

  • 对于已排好序的列表,采用二分查找算法查找方法对应执行函数
  • 对于没有排序的列表,采用一般遍历查找方法对应执行函数

二分查找/折半查找

  • 1.必须采用顺序存储结构。
  • 2.必须按关键字大小有序排列。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    func binarySearch<T: Comparable>(_ a: [T], key: T) -> Int? {    
    var lowerBound = 0
    var upperBound = a.count
    while lowerBound < upperBound {
    let midIndex = lowerBound + (upperBound - lowerBound) / 2
    if a[midIndex] == key {
    return midIndex
    } else if a[midIndex] < key {
    lowerBound = midIndex + 1
    } else {
    upperBound = midIndex
    }
    }
    return nil
    }

父类逐级查找

消息转发流程

1 动态方法解析

  • 向当前类发送resolveInstanceMethod:信号,检查是否动态向该类添加了方法。
  • resolveInstanceMethod:参数是 SEL类型 ,返回BOOL 。告诉系统是否解决这个方法的实现。

2 快速消息转发

  • 检查该类是否实现了forwardingTargetForSelector: 方法,若实现了则调用这个方法。若该方法返回值对象非nil或非self,则向该返回对象重新发送消息。
  • forwardingTargetForSelector:参数是 SEL类型,返回 id。告诉系统这个方法的处理对象是谁。

3 标准消息转发

  • 为了达到间接实现多继承目的的方法,可利用消息转发
  • methodSignatureForSelector:参数是 SEL类型,返回 id 方法签名。是包含 方法选择器的返回值类型,参数个数和参数类型的一个封装。并取到返回的方法签名用于生成 NSInvocation对象

4 消息转发机制

  • forwardInvocation

Method-Swizzling

简单的来说。就是交换了方法的实现

RuntimeObject.h

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>

@interface RuntimeObject : NSObject

- (void)test;

- (void)otherTest;

@end

RuntimeObject.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "RuntimeObject.h"
#import <objc/runtime.h>

@implementation RuntimeObject

+ (void)load{
//获取test方法
Method test = class_getInstanceMethod(self, @selector(test));
//获取otherTest的结构体
Method otherTest = class_getInstanceMethod(self, @selector(otherTest));
//交换方法实现
method_exchangeImplementations(test, otherTest);
}

- (void)test{
NSLog(@"test");
}

- (void)otherTest{
//实际上是调用了test的具体实现
[self otherTest];
NSLog(@"otherTest");
}

实际业务场景:比如很多时候需要记录viewWillAppear viewDidLoad的操作,不可能每个页面去添加,可以替换系统的方法,然后加一句代码,就能统计到。

动态添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (BOOL)resolveInstanceMethod:(SEL)sel{

//如果是text方法。打印日志
if (sel == @selector(test)) {
NSLog(@"resolveInstanceMethod");

//动态添加test方法实现
class_addMethod(self, @selector(test), testImp, "v@:");


//如果返回YES,则消息转发流程就结束了。为了看后续效果,先返回NO
return YES;
}
else{
//返回父类的默认调用
return [super resolveInstanceMethod:sel];
}
}

动态方法解析

@dynamic

  • 动态运行时语言将函数决议推迟到运行时。
  • 编译时语言在编译期进行函数决议。

Runtime实战


[obj foo]objc_msgSend() 函数之间有什么关系?
- [obj foo] 等于 ojc_mesSend(obj,foo) "v@:"

runtime如何通过Selector找到对应IMP地址的?
- 首先找到当前实例变量对应的类,去这个类的方法缓存中查找方法,未命中就去类的方法列表查找,再去父类的方法缓存中查找,以此类推,如果最后都没找到,就返回nil

能否向编译后的类中增加实例变量?
不能。因为编译后的实例变量存储在class_ro_t中,是只读的。
但能在动态添加的方法中添加实例变量。