Objective-C 之 Runtime 详解

208 阅读13分钟

Runtime 简介

Runtime又叫运行时,是一套底层的 C 语言 API,使我们可以在程序运行时动态的创建对象、检查对象,修改类和对象的方法。

将源代码转换为可执行的程序,通常要经过三个步骤:编译链接运行。C 语言作为一门静态类语言,在编译阶段就已经确定了所有变量的数据类型,同时也确定好了要调用的函数,以及函数的实现。而Objective-C语言是一门动态语言。在运行时,我们所编写的代码会转换成完整的确定的代码运行。所以在编译阶段并不知道变量的具体数据类型,也不知道所真正调用的哪个函数。只有在运行时间才检查变量的数据类型,同时在运行时才会根据函数名查找要调用的具体函数。

Runtime 作用

  1. 在程序运行过程中,动态的创建类,动态添加、修改这个类的属性和方法;
  2. 遍历一个类中所有的成员变量、属性、以及所有方法
  3. 消息传递、转发

与 Runtime 的交互

Objective-C程序在三种层面上与Runtime系统进行交互:

通过 Objective-C 源代码

多数情况我们只需要编写OC代码即可,Runtime系统自动在幕后搞定一切,编译器会将OC代码转换成运行时代码,在运行时确定数据结构和函数。

通过对 Runtime 库函数的直接调用

Runtime系统是具有公共接口的动态共享库。使用时只需要引入objc/Runtime.h头文件即可。

通过 Foundation 框架的 NSObject 类定义的方法

Cocoa 程序中绝大部分类都是 NSObject 类的子类,所以都继承了 NSObject 的行为。(NSProxy 类时个例外,它是个抽象超类)

一些情况下,NSObject类仅仅定义了完成某件事情的模板,并没有提供所需要的代码。例如 -description 方法,该方法返回类内容的字符串表示,该方法主要用来调试程序。NSObject类并不知道子类的内容,所以它只是返回类的名字和对象的地址,NSObject的子类可以重新实现。还有一些NSObject的方法可以从Runtime系统中获取信息,允许对象进行自我检查。例如:

- (Class)class;					 //返回对象的类;
- (BOOL)isKindOfClass:(Class)aClass;		 //检查对象是否存在于指定的类的继承体系中的实例;
- (BOOL)isMemberOfClass:(Class)aClass;		 //检查对象是是指定类的实例;
- (IMP)methodForSelector:(SEL)aSelector;	 //返回指定方法实现的地址。
- (BOOL)respondsToSelector:(SEL)aSelector;	 //检查对象能否响应指定的消息;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;//检查对象是否实现了指定协议类的方法;

Runtime 使用场景

  1. 给系统分类添加属性、方法

  2. 方法交换

  3. 获取对象的属性、私有属性

  4. 字典转换模型

  5. KVC、KVO

  6. 归档(编码、解码)

  7. NSClassFromString class<->字符串

  8. block

  9. 类的自我检测

    ·············

消息传递机制

Objc_msgSend

Objc_msgSend 简介

在消息的传递中,编译器会根据情况在 objc_msgSendobjc_msgSend_stretobjc_msgSendSuperobjc_msgSendSuper_stretobjc_msgSend_fpret这些方法中选择一个调用。

如果消息是传递给父类,那么会调用名字带有 Super 的函数;

如果消息返回值是数据结构时,会调用名字带有 stret 的函数。

如果消息返回值是浮点型时,会调用名字带有 fpret 的函数。

objc_msgSendRuntime的核心,Objective-C中调用对象方法就是消息传递。

objc_msgSend并不是直接调用方法实现(IMP)而是发送消息,让类的结构体去动态查到方法实现,所以在为查找到方法实现之前我们可以动态的去修改这个方法的实现

Objc_msgSend 工作原理
  1. 找到方法的实现(并不是直接调用方法实现(IMP)而是发送消息,让类的结构体去动态查到方法实现,所以在为查找到方法实现之前我们可以动态的去修改这个方法的实现),由于不同实例类对象那个可以创建同样的方法,每个实例对象中的该方法都是独立存在的。
  2. 调用该方法实现,将接收消息类指针,以及该方法的参数传递给这个类
  3. 最后将过程的返回值作为自己的返回值传递
Objc_msgSend 发送消息的过程

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

  1. 检测这个 selectorreceiver是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。
  2. receiver通过isa指针 找到 recevier 所在的 Class(类);
  3. 优先在Class(类)的cache(方法缓存)中找对应的 函数指针
  4. 如果在cache(方法缓存)中没有找到对应的 selector ,就继续在 Class(类)methodLists(方法列表)中找,如果找到,填充到cache(方法缓存)中,并调用方函数实现(IMP);
  5. 如果在Class(类)中没有找到 selector,就继续在它的 superClass(父类)中找,一直找到NSObject类为止;
  6. 如果还找不到,就要开始进入动态方法解析

发送消息隐藏的参数

当给一个对象发送消息时(调用方法),传递的所有参数,还包括两个隐藏的参数

objc_msgSend 找到方法对应实现时,它将直接调用该方法实现,并将消息中所有参数都传递给方法实现,同时,它还将传递两个隐藏参数:

  • 接受消息的对象(self 所指向的内容,当前方法的对象指针)
  • 方法选择器(_cmd 指向的内容,当前方法的 SEL 指针)

获取方法地址

规避动态绑定的唯一方法是获取方法的地址并直接调用它,就好像它是一个函数一样。

正常消息传递,需要使用Objc-msgSend将方法动态绑定到消息,当连续多次执行特定方法,并且希望每次执行该方法时都要避免消息传递的开销时,就需要拿到方法的IMP,直接使用IMP中的方法。

使用NSObject类中定义的方法, methodForSelector:,可以用它来获取某个方法选择器对应的 IMP ,返回的指针必须仔细转换为正确的函数类型。返回类型和参数类型都应包含在强制类型转换中。

@interface ViewController (){
    NSInteger _num;  
}
@end

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  
    void (*setter)(id, SEL, BOOL);
  //返回类型和参数类型都应包含在强制类型转换中
    setter = (void (*)(id, SEL, BOOL))[self methodForSelector:@selector(setFilled:)];
  
    for (int i = 0 ; i < 1000 ; i++ )
        setter(self, @selector(setFilled:), YES);
}
- (void)setFilled:(NSInteger)number{
    NSLog(@"%ld",_num++);
}              

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

使用methodForSelector:规避动态绑定可以节省消息传递所需的大部分时间。但是,仅在重复多次特定消息的情况下,这种节省才是可观的,如for上面所示的循环。

请注意,这methodForSelector:是由Cocoa运行时系统提供的;它不是Objective-C语言本身的功能。

消息转发

消息动态解析

当方法未找到时,Runtime 系统会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,我们可以通过重写这两个方法,添加函数实现,并返回 YES, Runtime 系统就会重新启动一次消息发送的过程。使用如下:

/// 为 类方法选择器 动态提供实现。类方法未找到时调用,可以在此添加方法实现
/// @param sel 需要解析的方法选择器
/// @return 如果找到方法并将其添加到接收器中,则为 YES,否则为 NO(默认)。
+ (BOOL)resolveClassMethod:(SEL)sel;
  
/// 为 对象方法选择器 动态提供实现。对象方法未找到时调用,可以在此添加方法实现
/// @param sel 需要解析的方法选择器
/// @return 如果找到方法并将其添加到接收器中,则为 YES,否则为 NO(默认)。
+ (BOOL)resolveInstanceMethod:(SEL)sel;

/** 
 * class_addMethod    向具有给定名称和实现的类中添加新方法
 * @param cls         被添加方法的类
 * @param name        selector 方法名
 * @param imp         实现方法的函数指针
 * @param types imp   指向函数的返回值与参数类型
 * @return            如果添加方法成功返回 YES,否则返回 NO
 */
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char * _Nullable types);
#import "ViewController.h"
#include "objc/runtime.h"
@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self performSelector:@selector(fun)];
}
// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(fun)) {
          class_addMethod([self class], sel, (IMP)dynamicFunIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:sel];
}
void dynamicFunIMP(id self, SEL _cmd) {
	NSLog(@"%s", __func__);
}
@end

上面的例子为 fun 方法添加了实现内容,就是 dynamicFunIMP 方法中的代码。其中 "v@:" 表示返回值和参数,这个符号表示的含义见:Type Encoding

动态方法解析会在消息转发机制 生效之前执行,动态方法解析器将会首先给予提供该方法选择器对应的 IMP 的机会。如果你想让该方法选择器被传送到转发机制,就让 resolveInstanceMethod: 方法返回 NO

消息转发机制

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

+resolveInstanceMethod: 或者 +resolveClassMethod:无论返回 YES,还是 NO,只要其中没有添加其他函数实现,Runtime 系统允许我们可以通过重写 - (id)forwardingTargetForSelector:或者+ (id)forwardingTargetForSelector: 方法,替换消息的接收者为其他对象或者类。使用如下:

/// 转发选择器的目标对象
/// @param aSelector 未实现的方法的选择器。
/// @return 返回的对象将用作未识别的消息的新接收方(默认 nil)。。
/// 当 + (BOOL)resolveInstanceMethod:(SEL)sel 未给 对象方法选择器 动态提供实现。
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(unrecognizedMethod:)){
        return newObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

/// (NSObject 类中未定义,通过检测得知)
/// 转发选择器的目标对象
/// @param aSelector 未实现的方法的选择器。
/// @return 返回的类将用作未识别的消息的新接收方(默认 nil)。
/// 当 + (BOOL)resolveClassMethod:(SEL)sel 未给 类方法选择器 动态提供实现。
+ (id)forwardingTargetForSelector:(SEL)aSelector{
  if(aSelector == @selector(unrecognizedMethod:)){
        return newClass;
    }
    return [super forwardingTargetForSelector:aSelector];
}

如果此方法返回 nil 或者 self,则会进入消息转发机制,否则将向返回的新对象或类重新发送消息。

转发

如果经过消息动态解析、消息接受者重定向,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统允许我们可以通过重写 -methodSignatureForSelector: 或者 +methodSignatureForSelector: 方法获取 NSMethodSignature 对象(函数的参数和返回值类型)。

如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序崩溃。

如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,Runtime 系统将通过 forwardInvocation: 消息通知该对象。因为 NSObject 中的方法实现只是简单的调用了 doesNotRecognizeSelector:。所以通过重写 forwardInvocation: 方法,我们可以将消息转发给其他对象。

forwardInvocation: 方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的接收对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。这一切都取决于方法的具体实现。

使用如下:

/// 根据方法选择器获取 NSMethodSignature 对象(包含方法的返回值和参数的类型的对象)
/// @param aSelector 对象方法选择器
/// @return 返回一个NSMethodSignature对象,该对象包含由给定选择器标识的方法的描述。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
/// 将消息转发到其他对象
/// @param anInvocation 对象方法选择器
- (void)forwardInvocation:(NSInvocation *)invocation 
/// 处理接收方无法识别的对象方法选择器
/// @param aSelector 未识别对象方法选择器
- (void)doesNotRecognizeSelector:(SEL)sel
#import "ViewController.h"
#include "objc/runtime.h"
@interface Person : NSObject
- (void)fun;
@end

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self performSelector:@selector(fun)];
}
// 获取函数的参数和返回值类型,返回签名
- (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

-forwardingTargetForSelector:-forwardInvocation: 都可以将消息转发给其他对象处理,区别就在于 -forwardingTargetForSelector: 只能将消息转发给一个对象。而 -forwardInvocation: 可以将消息转发给多个对象。

转发和多继承

转发和继承相似,可用于为 Objc 编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好像它把另一个对象中的方法接过来或者“继承”过来一样。 这使得在不同继承体系分支下的两个类可以实现“继承”对方的方法,在上图中 WarriorDiplomat 没有继承关系,但是 Warriornegotiate 消息转发给了 Diplomat 后,就好似 DiplomatWarrior 的超类一样。

消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。

转发与继承

虽然转发可以实现继承的功能,但是 NSObject 还是必须表面上很严谨,像 respondsToSelector:isKindOfClass: 这类方法只会考虑继承体系,不会考虑转发链。

如果上图中的 Warrior 对象被问到是否能响应 negotiate消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] ) {}

回答当然是 NO, 尽管它能接受 negotiate 消息而不报错,因为它靠转发消息给 Diplomat 类响应消息。

如果你就是想要让别人以为 Warrior 继承到了 Diplomatnegotiate 方法,你得重新实现 respondsToSelector:isKindOfClass: 来加入你的转发算法:

- (BOOL)respondsToSelector:(SEL)aSelector {
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;
}

除了 respondsToSelector:isKindOfClass: 之外,instancesRespondToSelector: 中也应该写一份转发算法。如果使用了协议,conformsToProtocol: 同样也要加入到这一行列中。