zl程序教程

您现在的位置是:首页 >  移动开发

当前栏目

iOS运行时文档解析(Runtime oc消息转发 objc_msgSend 动态加载方法)

ios文档方法消息 解析 运行 动态 加载
2023-09-14 09:04:15 时间

返回上级目录:iOS面试专题一

返回上级目录:iOS KVC-KVO-Runtime

官网文档链接:Objective-C Runtime Programming Guide

1.runtime是什么(Introduction)

Objective-C语言将尽可能多的决策从编译时和链接时推迟到运行时。只要有可能,它就动态地做事情。这意味着该语言不仅需要一个编译器,还需要一个运行时系统来执行编译后的代码。运行时系统作为Objective-C语言的一种操作系统;它使语言起作用。
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.

OC中NSObject、Class、Method、IMP等的定义

2.oc的消息机制(Messaging),即调用方法时oc底层是怎样通过runtime系统来实现的

2.1 objc_msgSend方法

在Objective-C中,消息(方法声明)直到运行时才绑定到方法实现。编译器将消息表达式转换为对消息传递函数objc_msgSend的调用。这个函数接受消息中提到的接收方和方法的名称——即方法选择器——作为它的两个主要参数:消息中传递的任何参数也会传递给objc_msgSend:

    Game *game = [[Game alloc]init];

我们日常的方法调用(即消息表达式):

    [game DoThings:@"test" Num:10];

编译器会将上面的方法调用转换为消息传递函数objc_msgSend的调用:

    objc_msgSend(game, @selector(DoThings:Num:),@"test",10);

消息传递函数完成动态绑定所需的一切:

  • 它首先找到选择器引用的过程(方法实现)。由于相同的方法可以通过不同的类以不同的方式实现,因此它所找到的精确过程取决于接收方的类。
  • 然后调用该过程(方法实现),向它传递接收对象(指向其数据的指针)以及为该方法指定的任何参数。
  • 最后,它将过程(方法实现)的返回值作为自己的返回值传递。

消息传递的关键在于编译器为每个类和对象构建的结构。每个类的结构都包括以下两个基本元素:

  • 超类的指针。
  • 一个类分派表。这个表中有一些条目,它们将方法选择器与它们所标识的方法的特定于类的地址相关联。

创建新对象时,将为其分配内存,并初始化其实例变量。在对象的变量中,首先是指向其类结构的指针。这个名为isa的指针使对象能够访问它的类,并通过类访问它所继承的所有类。

类和对象结构的这些元素如图3-1所示。
在这里插入图片描述

当向对象发送消息时,消息传递函数跟随对象的isa指针,该指针指向类结构,并在分派表中查找方法选择器。如果它在那里找不到选择器,objc_msgSend跟随超类的指针并尝试在它的分派表中找到选择器。连续失败会导致objc_msgSend在类层次结构中攀升,直到到达NSObject类。一旦找到选择器,该函数将调用表中输入的方法,并将接收对象的数据结构传递给它。

这就是在运行时选择方法实现的方式——或者,用面向对象编程的行话来说,方法被动态地绑定到消息。

为了加速消息传递过程,运行时系统在使用方法的选择器和地址时缓存它们。每个类都有一个单独的缓存,它可以包含用于继承方法和类中定义的方法的选择器。在搜索分派表之前,消息传递例程首先检查接收对象的类的缓存(根据使用过一次的方法可能会再次使用的理论)。如果方法选择器在缓存中,消息传递只比函数调用稍微慢一点。一旦一个程序运行了足够长的时间来“预热”它的缓存,几乎它发送的所有消息都会找到一个缓存的方法。缓存在程序运行时动态增长以适应新消息。

2.2 两个隐藏的参数(id self, SEL _cmd)

当objc_msgSend找到实现方法的过程时,它调用该过程并将消息中的所有参数传递给它。它还向过程传递了两个隐藏参数:

  • 接收对象
  • 方法的选择器

这些参数为每个方法实现提供了关于调用它的消息表达式的两部分的显式信息。它们被称为“隐藏的”,因为它们没有在定义方法的源代码中声明。它们在编译代码时插入到实现中。

虽然这些参数没有显式声明,但源代码仍然可以引用它们(就像它可以引用接收对象的实例变量一样)。方法将接收对象引用为self,将其自己的选择器引用为_cmd。

self是两个论点中更有用的一个。实际上,它是接收对象的实例变量对方法定义可用的方式。

示例1

ViewController.m

    Game *game = [Game new];
    game.name = @"王者";
    [game Play];

Game.m

- (void)Play{
    NSLog(@"%@",self);
    NSLog(@"%@",NSStringFromSelector(_cmd));
    NSLog(@"%@",self.name);
}

打印结果
2020-07-27 17:37:26.343658+0800 runtime[6025:223077] <Game: 0x600001c25220>
2020-07-27 17:37:26.343863+0800 runtime[6025:223077] Play
2020-07-27 17:37:26.344010+0800 runtime[6025:223077] 王者

在示例1中,Play方法的实现中被隐式的插入了两个参数self(接收对象)和_cmd(方法的选择器), 而且通过self可以在Play方法实现中获得对象的属性self.name

示例2

class_addMethod(kvoClass, @selector(setName:), (IMP)setName, "v@:@");
void setName(id self, SEL _cmd, NSString *name) {
}

示例2中,使用class_addMethod动态添加方法,方法setName的前两个参数就是id self(接收对象), SEL _cmd(方法的选择器)

2.3 获取方法地址

规避动态绑定的唯一方法是获取方法的地址,然后像调用函数一样直接调用它。当某个特定方法将连续多次执行,并且希望避免每次执行该方法时的消息传递开销时,这种方法可能比较合适。

使用在NSObject类中定义的方法methodForSelector:,你可以请求一个指向实现方法的过程的指针,然后使用该指针调用该过程。methodForSelector:返回的指针必须被强制转换为正确的函数类型。返回类型和参数类型都应该包含在类型转换中。

下面的例子展示了如何调用实现DoThings:Num:方法的过程:

ViewController.m

 Game *game = [Game new];
 [game Play];

Game.m

- (void)DoThings:(NSString *)Str Num:(NSInteger)num {
    NSLog(@"%ld\n",(long)num);
}

- (void)Play{
    void (*func)(id, SEL, NSString*, NSInteger);
    //获取DoThings:Num:方法的地址
    func = (void (*)(id, SEL, NSString*, NSInteger))[self methodForSelector:@selector(DoThings:Num:)];
    for (int i = 0; i < 10; i++) {
        func(self,@selector(DoThings:Num:),@"test",i);
    }
}

打印结果:
2020-07-27 22:31:45.719610+0800 runtime[7753:290611] 0
2020-07-27 22:31:45.719792+0800 runtime[7753:290611] 1
2020-07-27 22:31:45.719922+0800 runtime[7753:290611] 2
2020-07-27 22:31:45.720062+0800 runtime[7753:290611] 3
2020-07-27 22:31:45.720188+0800 runtime[7753:290611] 4
2020-07-27 22:31:45.720298+0800 runtime[7753:290611] 5
2020-07-27 22:31:45.720416+0800 runtime[7753:290611] 6
2020-07-27 22:31:45.720529+0800 runtime[7753:290611] 7
2020-07-27 22:31:45.720650+0800 runtime[7753:290611] 8
2020-07-27 22:31:45.720774+0800 runtime[7753:290611] 9

传递给过程的前两个参数是接收对象(self)和方法选择器(_cmd)。这些参数隐藏在方法语法中,但必须在方法作为函数调用时显式显示。

使用methodForSelector:绕过动态绑定节省了消息传递所需的大部分时间。但是,只有在重复了很多次特定消息的情况下(如上面所示的for循环可能重复非常多次),这种节省才会非常显著。

注意methodForSelector:是由Cocoa运行时系统提供的;它不是Objective-C语言本身的特性。

3.消息转发和动态方法解析

Game.h

#import <Foundation/Foundation.h>

@interface Game : NSObject

- (void)DoThings:(NSString *)Str Num:(NSInteger)num;

@end

Game.m

#import "Game.h"
@implementation Game

@end

ViewController.m

 Game *game = [[Game alloc]init];
 [game DoThings:@"test" Num:10];

如上面的示例,DoThings:Num方法没有实现,所以执行时程序会崩溃,并包如下的错误:
在这里插入图片描述
解决方案:在抛出错误前有三次补救机会

3.1 动态方法解析(Dynamic Method Resolution)

可以实现方法resolveInstanceMethod:和resolveClassMethod:分别为实例方法和类方法动态地提供给定选择器的实现。也就是在resolveInstanceMethod:方法中,利用runtime的class_addMethod方法动态的添加方法实现

下面会用到runtime的一些方法,需要进行下面的设置

在这里插入图片描述

3.1.1 代码:resolveInstanceMethod

方法一: +(BOOL)resolveInstanceMethod:(SEL)sel

Game.m

#import "Game.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation Game

//id self, SEL _cmd这两个参数不写,前面两个参数也会被默认为是id self, SEL _cmd
void MyMethodIMP(id self, SEL _cmd,NSString *name, NSInteger num) {
    NSLog(@"%s",__func__);
}

//在此方法中添加方法的实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
   NSLog(@"%s",__func__);
       if(sel == @selector(DoThings:Num:)){
           class_addMethod([self class], sel, (IMP)MyMethodIMP, "v@:");
           return YES;
       }
       return  [super resolveInstanceMethod:sel];
}
@end

打印:
在这里插入图片描述

3.2 消息转发(Message Forwarding)

3.2.1 Fast forwarding:forwardingTargetForSelector

让别的对象去实现该方法

方法二 -(id)forwardingTargetForSelector:(SEL)aSelector

Game.m

#import "Game.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import "Tools.h"

@implementation Game

//让别的对象去实现方法,这个别的对象就是返回值id
-(id)forwardingTargetForSelector:(SEL)aSelector{
 NSLog(@"%s",__func__);
    if([NSStringFromSelector(aSelector) isEqualToString:@"DoThings:Num:"]){
        return [[Tools alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

Tools.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Tools : NSObject

@end

NS_ASSUME_NONNULL_END

Tools.m

#import "Tools.h"

@implementation Tools

- (void)DoThings:(NSString *)Str Num:(NSInteger)num{
    NSLog(@"%s",__func__);
    NSLog(@"%@",self);
        NSLog(@"%@",Str);
    NSLog(@"%ld",(long)num);
}

@end

打印:

在这里插入图片描述

3.2.2.Normal forwarding:methodSignatureForSelector,forwardInvocation

方法三:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-(void)forwardInvocation:(NSInvocation *)anInvocation

3.2.2.1 代码示例1

Game.m

#import "Game.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import "Tools.h"

@implementation Game
/*
 * 第三步 如果前两步未处理,这是最后处理的机会将目标函数以其他形式执行
 **/
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    NSString *SelStr = NSStringFromSelector(aSelector);
    if([SelStr isEqualToString:@"DoThings:Num:"]){
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//只要实现了这个方法,哪怕里面没有代码也不会崩溃  第二次执行resolveInstanceMethod后再执行这个方法
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    
    //如果再这个方法里什么都不做的话也不会报错,相当于把这个消息吞掉了
    
    //改变消息接受者对象  这个会和执行方法二结果一样
//    [anInvocation invokeWithTarget:[[Tools alloc]init]];

    //改变消息的SEL
    anInvocation.selector = @selector(flyGame);
    [anInvocation invokeWithTarget:self];
}

- (void)flyGame{
    NSLog(@"我要飞翔追逐梦想!");
}
@end

打印:
在这里插入图片描述

forwardInvocation方法:
所转发的消息的返回值返回给原始发送方。所有类型的返回值都可以传递给发送方,包括id、结构和双精度浮点数。
方法可以充当未识别消息的分发中心,将它们分发给不同的接收者。或者它可以是一个中转站,将所有的消息发送到同一个目的地。它可以将一条消息翻译成另一条消息,或者简单地“吞下”一些消息,这样就没有响应和错误了。方法还可以将多个消息合并到单个响应中。forwardInvocation:做什么取决于实现者。然而,它为在转发链中链接对象提供了机会,为程序设计提供了可能性。

3.2.2.1 代码示例2

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2.2.1 代码示例3

在这里插入图片描述

在这里插入图片描述

如果上面的三次机会都没有把握住会调用如下的方法,程序崩溃

#import "Game.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import "Tools.h"

@implementation Game
/*
 * 作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。
 * 虽然理论上可以重载这个函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。
 *
 ***/
- (void)doesNotRecognizeSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    NSLog(@"doesNotRecognizeSelector:%@",NSStringFromSelector(aSelector));
}

@end

在这里插入图片描述

流程图

在这里插入图片描述

 Game *game = [[Game alloc]init];
 _objc_msgForward(game, @selector(DoThings:Num:),@"test",10);

一旦调用_objc_msgForward,将跳过查找IMP,直接出发“消息转发”,方法的实现和动态添加的方法实现都会被跳过,直接跳到上面的流程图的forwardingTargetForSelector方法。即使这个对象实现了这个方法,也会告诉objc_msgSend:“我没有在这个对象里找到这个方法的实现”

相关测试截图(主要是给我自己看的)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.runtime的使用

  1. 动态添加方法
  2. 消息转发
    • 实现多继承(类似)
    • 方法交换
  3. 获取类的所有属性,方法,成员变量和协议
    - 归解档
    - 字典转模型
  4. 动态的添加类
    - KVO
  5. 对私有属性进行修改

参考博客

深入浅出Runtime (三) Runtime的消息转发
iOS Runtime的实际应用
iOS unrecognized selector 消息转发 _objc_msgForward