iOS - Runtime基础(下)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: Runtime合集 iOS - isa、superclass指针,元类superclass指向基类本身

5. Runtime消息转发


3. 消息机制的基本原理最后一步我们提到:若找不到对应的selector,消息被转发或者临时向receiver添加这个selector对应的实现方法,否则就会崩溃


当一个方法找不到的时候,Runtime提供了消息动态解析、消息接受者重定向、消息定向等三步处理消息,具体流程如下


image.png

image


5.1 消息动态解析(动态添加方法)


Objective-C运行时会调用+resolveClassMethod+resolveInstanceMethod,让你有机会提供一个函数实现。前者在对象方法未找到时调用,后者在类方法未找到时调用。我们可以通过重写这两个方法,添加其他函数实现,并返回YES,那运行时系统就会重新启动一次消息发送的过程


主要用到的方法如下


动态解析的方法位于

// 位于objc/NSObject.h
+ (BOOL)resolveClassMethod:(SEL)sel ;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//位于 objc/runtime.h
/** 
 * 向一个类添加新方法,此方法需要给定名称及参数
 * 
 * @param cls 要被添加方法的类
 * @param name selector方法名称
 * @param imp 实现方法的函数指针
 * @param types 只想函数的返回值与参数类型 
 * 
 * @return 如果添加方法成功返回YES,否则返回NO
 */
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types) ;


代码示例:

//
//  ViewController.m
//  RuntimeDemo
//
//  Created by Terence on 2021/5/10.
//  Copyright © 2021年 Terence. All rights reserved.
//
#import "ViewController.h"
#import "objc/runtime.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(eat)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(eat)) {
        class_addMethod(self.class, sel, (IMP)eatMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
void eatMethod(id obj, SEL _cmd) {
    NSLog(@"eat food");
}
@end


输出结果:

2021-05-10 23:10:23.110858+0800 RuntimeDemo[3451:122697] eat food


从上边的例子中,我们可以看出,虽然我们没有实现fun方法,但是通过重写resolveInstanceMethod方法,利用class_addMethod 方法动态的添加了对象方法eatMethod,并执行,成功调用了eatMethod方法


class_addMethod方法中的特殊参数v@:,可参考苹果官方文档中关于Type Encodings的说明:Type Encodings


5.2 消息动态转发


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


如果当前对象实现了- forwardingTargetForSelector:+forwardingTargetForSelector:方法,Runtime就会调用这个方法,允许我们将消息的接收者转发给其它对象


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


注意:


  1. 类方法和对象方法消息转发第二步调用的方法不一样,前者是+forwardingTargetForSelector方法,后者是-forwardingTargetForSelector方法


  1. 这里-resolveClassMethod:或者-resolveInstanceMethod无论是返回YES还是NO,只要其中没有添加其它函数实现,运行时都会进行下一步


代码示例:

@implementation Person
- (void)eatFood:(NSString *)foodName {
    NSLog(@"person eat food : %@", foodName);
}
- (void)personSleep {
    NSLog(@"person is sleeping...");
}
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(personSleep)];
}
+ (BOOL)resolveClassMethod:(SEL)sel {
    return YES;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(personSleep)) {
        return [[Person alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end


打印输出:

2021-05-11 11:50:55.352147+0800 LoadInitializeDemo[47468:1985216] person is sleeping...


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


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


5.3 消息重定向


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


  • 如果methodSignatureForSelector返回了一个NSMethodSignature对象(函数签名),Runtime系统就会创建一个NSInvocation对象。并通过forwardInvocation:消息通知当前对象,给予此次消息发送最后依次寻找IMP的机会


  • 如果methodSignatureForSelecotr:返回nil,则Runtime系统会发出doesNotRecognizeSelector:消息,程序也就崩溃了
    所以我们可以在forwardingInvocation:方法中对消息进行转发


注意:类方法和对象方法消息转发第三步调用的方法同样不一样

类方法调用的是:


  1. +methodSignatureForSelector
  2. + forwardInvocation:
  3. doesNotRecognizeSelector:


对象方法调用的是


-methodSignatureForSelector:

-forwardingInvocation:

doesNotRecognizeSelector:


用到的方法

//获取类方法函数的参数和返回值类型,返回签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 类方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"aInvocation: %@", anInvocation);
// 获取对象方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 对象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"aInvocation: %@", anInvocation);
}
}


代码示例

#import "ViewController.h"
#import "Person.h"
#import <objc/runtime.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [ViewController performSelector:@selector(personWakeup)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    //为了进行下一步,消息接收者重定向
    return YES;
}
//消息接收者重定向
+ (id)forwardingTargetForSelector:(SEL)aSelector {
//为了进行下一步,消息重定向
    return [super forwardingTargetForSelector:aSelector];
}
// 获取函数的参数和返回值类型,返回签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"personWakeup"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
// 消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"aInvocation: %@", anInvocation);
    SEL sel = anInvocation.selector;
    if ([Person respondsToSelector:sel]) { //判断Person类对象是否可以响应sel
        [anInvocation invokeWithTarget:Person.class]; // 若可以响应,则将消息转发给其它对象处理
    } else {
            [anInvocation doesNotRecognizeSelector:sel];//若仍然无法响应,则报错:找不到方法
    }
}
@end


打印结果:

2021-05-11 16:40:10.119025+0800 LoadInitializeDemo[93832:2252330] person will wake up...


可以看到,我们在+forwardingInvocation:方法里面让Peron对象去执行了personWakeup函数


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


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


以上就是Runtime消息转发的整个流程


结合之前讲的3.消息机制的基本原理,就构成了整个消息发送及转发的流程,下面我们来总结下整个流程


6. 消息发送一级转发机制总结


调用[receiver selector]后,进行的流程:


  1. 编译阶段:[receiver selector]方法被编译器转换为:
  1. objc_msgSend(receiver, selector)(不带参数)
  2. objc_msgSend(receiver,selector, org1, org2, ...)(带参数)


  1. 运行时阶段:消息接收者receiver寻找对应的selector


  1. 通过receiverisa指针找到receiverClass
  2. 在Class的cache(方法缓存)的散列表中寻找对应的IMP(方法实现)
  3. 如果在cache(方法缓存)中没有找到对应的IMP(方法实现),则继续在Class(类)methodLists中寻找对应的selector,如果找到,填充到cache(方法缓存)中,并返回selector
  4. 如果在class(类)中没有找到这个selector,就继续在它的superclass(父类)中找
  5. 一旦找到对应的selector,直接指向receiver对应的selector方法实现的IMP(方法实现)
  6. 若找不到对应的selectorRuntime系统进入消息转发机制
    3.运行时消息转发阶段:
  7. 动态解析:通过重写resolveInstanceMethodresolveClassMethod,利用class_addMethod动态添加方法
  8. 消息接收者重定向:如果上一步没有添加其它函数实现,可在当前对象中利用forwardingTargetForSelector将消息的接收者转给其它对象
  9. 消息重定向: 如果上一步没有返回值为nil,返回了一个NSMethodSignature对象(函数签名),Runtime系统就会创建一个NSIncovation对象,并通过forwardingInvocation:消息通知当前对象,给予此次消息发送最后依次寻找IMP的机会
  10. 如果methodSignationForSelector返回nil,则Runtime系统就会发出doesNotRecoginzerSelector:消息,程序也就崩溃了


参考:


  1. Objective-C Runtime 苹果官方文档
  2. Objective-C Runtime Programming Guide
  3. 『Runtime』详解(一)基础知识


相关文章
|
API iOS开发
iOS面试关于runtime
iOS面试关于runtime
119 0
15-iOS之Runtime常用API以及使用
15-iOS之Runtime常用API以及使用
107 0
|
编译器 iOS开发
iOS Runtime详细介绍及实战使用(二)
iOS Runtime详细介绍及实战使用
 iOS Runtime详细介绍及实战使用(二)
|
API iOS开发
iOS Runtime详细介绍及实战使用(一)
iOS Runtime详细介绍及实战使用
|
iOS开发
iOS开发- runtime基本用法解析和用runtime给键盘添加工具栏和按钮响应事件
iOS开发- runtime基本用法解析和用runtime给键盘添加工具栏和按钮响应事件
144 0
|
前端开发 API 开发工具
基础篇必看,史上最全的iOS开发教程集锦,没有之一
基础篇必看,史上最全的iOS开发教程集锦,没有之一
|
程序员 API iOS开发
iOS开发:个人对于textView基础用法的总结(其一)
从事了这么久ios开发,对于textView的使用并不陌生,它和textfield有相似的地方,也有不同的地方,这里只对textView的一些基础用法进行描述,textfield不在这里描述。
340 0
|
JSON 前端开发 JavaScript
iOS Principle:Runtime(下)
iOS Principle:Runtime(下)
97 0
iOS Principle:Runtime(下)
|
缓存 编译器 Swift
iOS Principle:Runtime(中)
iOS Principle:Runtime(中)
173 0
iOS Principle:Runtime(中)
|
存储 缓存 编译器
iOS Principle:Runtime(上)
iOS Principle:Runtime(上)
114 0
iOS Principle:Runtime(上)