Runtime

Runtime

概念

Runtime是一套底层纯C语言API,OC代码最终都会被编译器转化为运行时代码,通过消息机制决定函数调用方式,这也是OC作为动态语言使用的基础。Runtime的最大特征就是实现了OC语言的动态特性。

消息机制原理

在Object-C的语言中,对象方法调用都是类似[receiver selector] 的形式,其本质:就是让对象在运行时发送消息的过程。

而方法调用[receiver selector] 分为两个过程:

  • 编译阶段

[receiver selector] 方法被编译器转化,分为两种情况:

  1. 不带参数的方法被编译为:objc_msgSend(receiver,selector)
  2. 带参数的方法被编译为:objc_msgSend(recevier,selector,org1,org2,…)
  • 运行时阶段

消息接收者recever寻找对应的selector,也分为两种情况:

  1. 接收者能找到对应的selector,直接执行接收receiver对象的selector方法。
  2. 接收者找不到对应的selector,消息被转发或者临时向接收者添加这个selector对应的实现内容,否则崩溃

总而言之:

OC调用方法[receiver selector],编译阶段确定了要向哪个接收者发送message消息,但是接收者如何响应决定于运行时的判断。

重要概念

objc_msgSend

所有 Objective-C 方法调用在编译时都会转化为对 C 函数 objc_msgSend 的调用。objc_msgSend(receiver,selector); 是 [receiver selector]; 对应的 C 函数。

Object(对象)

objc/runtime.h 中Object(对象) 被定义为指向 objc_object 结构体 的指针,objc_object结构体 的数据结构如下:

//runtime对objc_object结构体的定义
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

//id是一个指向objc_object结构体的指针,即在Runtime中:
typedef struct objc_object *id;

//OC中的对象虽然没有明显的使用指针,但是在OC代码被编译转化为C之后,每个OC对象其实都是拥有一个isa(指向对象的类)的指针的

Class(类)

objc/runtime.h 中Class(类) 被定义为指向 objc_class 结构体 的指针,objc_class结构体 的数据结构如下:

//runtime对objc_class结构体的定义
struct objc_class {
    Class _Nonnull isa;                                          // objc_class 结构体的实例指针

#if !__OBJC2__
    Class _Nullable super_class;                                 // 指向父类的指针
    const char * _Nonnull name;                                  // 类的名字
    long version;                                                // 类的版本信息,默认为 0
    long info;                                                   // 类的信息,供运行期使用的一些位标识
    long instance_size;                                          // 该类的实例变量大小;
    struct objc_ivar_list * _Nullable ivars;                     // 该类的实例变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定义的列表
    struct objc_cache * _Nonnull cache;                          // 方法缓存
    struct objc_protocol_list * _Nullable protocols;             // 遵守的协议列表
#endif

};


//class是一个指向objc_class结构体的指针,即在Runtime中:
typedef struct objc_class *Class; 

SEL (方法选择器)

typedef struct objc_selector *SEL;

//Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL

1.不同类中相同名字的方法对应的方法选择器是相同的。
2.即使是同一个类中,方法名相同而变量类型不同也会导致它们具有相同的方法选择器。

获取SEL有三种方法:

1.OC中,使用@selector(“方法名字符串”)
2.OC中,使用NSSelectorFromString(“方法名字符串”)
3.Runtime方法,使用sel_registerName(“方法名字符串”)

Method(方法)

objc/runtime.h 中Method(方法) 被定义为指向 objc_method 结构体 的指针,在objct_class定义中看到methodLists,其中的元素就是Method,objc_method结构体 的数据结构如下:

struct objc_method {
    SEL _Nonnull method_name;                    // 方法名
    char * _Nullable method_types;               // 方法类型
    IMP _Nonnull method_imp;                     // 方法实现
};

//Method表示某个方法的类型
typedef struct objc_method *Method;

Runtime消息转发

动态方法解析:动态添加方法

Runtime足够强大,能够在运行时动态添加一个未实现的方法,这个功能主要有两个应用场景:

1. 动态添加未实现方法,解决代码中因为方法未找到而报错的问题
2. 利用懒加载思路,若一个类有很多个方法,同时加载到内存中会耗费资源,可以使用动态解析添加方法

方法动态解析主要用到的方法如下:

//OC方法:
//类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel

//实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel

//Runtime方法:
/**
 运行时方法:向指定类中添加特定方法实现的操作
 @param cls 被添加方法的类
 @param name selector方法名
 @param imp 指向实现方法的函数指针
 @param types imp函数实现的返回值与参数类型
 @return 添加方法是否成功
 */
BOOL class_addMethod(Class _Nullable cls,
                     SEL _Nonnull name,
                     IMP _Nonnull imp,
                     const char * _Nullable types)

  • 解决方法无响应崩溃问题

执行OC方法其实就是一个发送消息的过程,若方法未实现,可以利用方法动态解析与消息转发来避免程序崩溃,这主要涉及下面一个处理未实现消息的过程:

在这个过程中,可能还会使用到的方法有:

img

例子:

#import "ViewController.h"
#import <objc/runtime.h>

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 执行 fun 函数
    [self performSelector:@selector(fun)];
}

// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(fun)) { // 如果是执行 fun 函数,就动态解析,指定新的 IMP
        class_addMethod([self class], sel, (IMP)funMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void funMethod(id obj, SEL _cmd) {
    NSLog(@"funMethod"); //新的 fun 函数
}
@end

//日志输出:

2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] funMethod

从执行任务的输出日志中,可以看到:

虽然没有实现 fun 方法,但是通过重写 resolveInstanceMethod: ,利用 class_addMethod 方法添加对象方法实现 funMethod 方法,并执行。从打印结果来看,成功调起了funMethod 方法。

消息接收者重定向

如果上一步中 +resolveInstanceMethod:或者 +resolveClassMethod: 没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向。

如果当前对象实现了 -forwardingTargetForSelector:Runtime 就会调用这个方法,允许将消息的接受者转发给其他对象,其主要方法如下:

//重定向类方法的消息接收者,返回一个类
- (id)forwardingTargetForSelector:(SEL)aSelector

//重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector

例子:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Person : NSObject
- (void)fun;
@end

@implementation Person

- (void)fun {
    NSLog(@"fun");
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 执行 fun 方法
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 为了进行下一步 消息接受者重定向
}

// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(fun)) {
        return [[Person alloc] init];
        // 返回 Person 对象,让 Person 对象接收这个消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

//日志输出:

2019-09-01 23:24:34.911774+0800 XKRuntimeKit[3064:521123] fun

从执行任务的输出日志中,可以看到:

虽然当前 ViewController 没有实现 fun 方法,+resolveInstanceMethod: 也没有添加其他函数实现。
但是我们通过 forwardingTargetForSelector 把当前 ViewController 的方法转发给了 Person 对象去执行了。

通过forwardingTargetForSelector 可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是 nil,也不是 self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向流程

消息重定向

如果经过消息动态解析、消息接受者重定向,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统会利用 -methodSignatureForSelector: 方法获取函数的参数和返回值类型。

其过程:

  1. 如果 -methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,
    并通过 -forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP(指向实现方法的函数指针) 的机会。
  2. 如果 -methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 -doesNotRecognizeSelector: 消息,程序也就崩溃了。

所以可以在-forwardInvocation:方法中对消息进行转发。

其主要方法:

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;

// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;

例子:

#import "ViewController.h"
#import <objc/runtime.h>

@interface Person : NSObject
- (void)fun;
@end

@implementation Person
- (void)fun {
    NSLog(@"fun");
}
@end


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 执行 fun 函数
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 为了进行下一步 消息接受者重定向
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil; // 为了进行下一步 消息重定向
}

// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;   // 从 anInvocation 中获取消息
    
    Person *p = [[Person alloc] init];

    if([p respondsToSelector:sel]) {   // 判断 Person 对象方法是否可以响应 sel
        [anInvocation invokeWithTarget:p];  // 若可以响应,则将消息转发给其他对象处理
    } else {
        [self doesNotRecognizeSelector:sel];  // 若仍然无法响应,则报错:找不到响应方法
    }
}
@end

//日志输出:
2019-09-01 23:24:34.911774+0800 XKRuntimeKit[30032:8724248] fun

从执行任务的输出日志中,可以看到:

在 -forwardInvocation: 方法里面让 Person 对象去执行了 fun 函数

问:既然 -forwardingTargetForSelector:-forwardInvocation: 都可以将消息转发给其他对象处理,那么两者的区别在哪?

答:区别就在于 -forwardingTargetForSelector: 只能将消息转发给一个对象。而 -forwardInvocation: 可以将消息转发给多个对象。

Runtime的应用

动态方法交换

实现动态方法交换(Method Swizzling )是Runtime中最具盛名的应用场景,其原理是:

通过Runtime获取到方法实现的地址,进而动态交换两个方法的功能。

类目添加新的属性

在日常开发过程中,常常会使用类目Category为一些已有的类扩展功能。虽然继承也能够为已有类增加新的方法,而且相比类目更是具有增加属性的优势,但是继承毕竟是一个重量级的操作,添加不必要的继承关系无疑增加了代码的复杂度。

获取类详细属性

  • 获取属性列表
  • 获取所有成员变量
  • 获取所有方法
  • 获取当前遵循的所有协议

解决同一方法高频率调用的效率问题

Runtime源码中的IMP作为函数指针,指向方法的实现。通过它,可以绕开发送消息的过程来提高函数调用的效率。当需要持续大量重复调用某个方法的时候,会十分有用。

动态操作属性

  • 修改私有属性
  • 改进iOS归档和解档
  • 实现字典与模型的转换

利用Runtime实现的思路大体如下:

借助Runtime可以动态获取成员列表的特性,遍历模型中所有属性,然后以获取到的属性名为key,在JSON字典中寻找对应的值value;再将每一个对应Value赋值给模型,就完成了字典转模型的目的。

Swift中的Runtime

Swift是静态语言,本身没有动态特性。

结论:

  • 对于纯Swift类来说,没有动态特性。方法和属性不加任何修饰符的情况下,这个时候已经不具备我们所谓的Runtime特性了。
  • 对于纯Swift类,方法和属性添加@objc标识的情况下,当前我们可以通过Runtime API拿到,但是在我们的OC中是没办法进行调度的。
  • 对于继承自NSObject类来说,如果我们想要动态的获取当前的属性和方法,必须在其声明前添加@objc关键字,方法交换需要添加 dynamic 标识,否则也是无法通过Runtime API获取的。

反射

反射是Swift中动态获取的一种方法,可以动态获取类型、成员信息,在运行时可以调用方法、属性等行为的特性。上面的结论说了对于一个纯Swift类来说,并不支持像OC那样操作,但是Swift标准库依然提供了反射机制让我们访问成员信息。

用法如下:

import UIKit

//下方OC的部分可以不加没问题
class LGTeacher: NSObject{
    @objc var age: Int = 18
    
    @objc dynamic func teach(){
        print("teach")
    }
}

let t = LGTeacher()

let mirror = Mirror(reflecting: t.self)
for pro in mirror.children{
    print("\(pro.label):\(pro.value)")
}

运行结果:

Optional("age"):18
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值