Effective Objective-C 2.0 读书笔记—— 消息转发

Effective Objective-C 2.0 读书笔记------ 消息转发

文章目录

前言

在前面我学习了关联对象和objc_msgSend的相关内容,初步了解了OC的动态机制,在我们的objc_msgSend的执行操作之中,我们提到了,如果对象接受了无法解读的消息之后,就会进行消息转发。那么什么消息可以被理解呢?最基本的就是,我们的程序要实现对应的方法,由于OC动态语言的特性,我们在编译期的时候仍可以在类之中添加方法,所以当对象接受到无法解析的消息时就会启动消息转发机制(message forwarding)。

消息转发机制概述

消息转发一共由两种情况

  1. 动态方法解析(Dynamic Method Resolution):如果一个对象没有实现某个方法,Objective-C 会尝试在运行时为该方法动态添加实现。
  2. 消息转发(Message Forwarding):如果对象无法处理该消息且无法动态解析方法,系统会尝试将该消息转发给其他对象来处理。

动态方法解析

对于动态方法解析来说,在这个阶段之中先征询接受者,所属的类,看其是否能动态的添加方法去处理这个未知的选择子

如果是实例方法未能识别,那么首先将调用其所属类的下列类方法:

objc 复制代码
+(BOOL) resolveInstanceMethod: (SEL) selector

如果是类方法尚未被实现,则调用一下方法

objc 复制代码
+(BOOL) resolveClassMethod: (SEL) selector

这两个方法都返回的是Boolean类型,表示能否新增这个方法处理这个选择子

处理@dynamic的属性

书中用一个被@dynamic修饰的属性为例,使用这个方法为属性生成setter和getter方法

objc 复制代码
id autoDictionaryGetter(id self, SEL _cmd) {
    // 这里可以实现获取字典的逻辑,可能是从某个缓存或者实际数据源获取
    return objc_getAssociatedObject(self, _cmd);
}

void autoDictionarySetter(id self, SEL _cmd, id value) {
    // 这里可以实现设置字典的逻辑,可能是更新缓存或者实际数据源
    objc_setAssociatedObject(self, _cmd, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

+ (BOOL)resolveInstanceMethod:(SEL)selector {
    NSString *selectorString = NSStringFromSelector(selector);
    
    // 检查是否是动态属性的 getter 或 setter 方法
    if ([selectorString hasPrefix:@"set"]) {
        // 如果是 setter 方法(即 setAutoDictionary:)
        class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");  // 'v@:@' 表示返回 void 类型,self 和 _cmd 参数,最后是一个 id 类型的参数
        return YES;
    }
    
    // 如果是 getter 方法(即 autoDictionary)
    if ([selectorString hasPrefix:@"autoDictionary"]) {
        class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");  // '@@:' 表示返回 id 类型,self 和 _cmd 参数
        return YES;
    }
    
    // 如果方法不是 setter 或 getter,则调用父类的 resolveInstanceMethod:
    return [super resolveInstanceMethod:selector];
}

简单解释一下代码的内容:

class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");

  • class_addMethod 是运行时函数,允许你动态地为某个类添加方法。
  • 它的参数依次是:
    1. self:要为哪个类添加方法,通常是当前类。
    2. selector:方法选择器,表示要添加的方法的名字。
    3. (IMP)autoDictionarySetter:方法实现,IMP 是指向方法实现的指针,这里是 autoDictionarySetter函数的指针。
    4. "v@:@":方法的签名,描述了方法的参数和返回值类型------表示返回 void 类型,self 和 _cmd 参数,最后是一个 id 类型的参数

IMP是 Implementation Pointer(实现指针)的缩写,是一种在 Objective-C 中表示方法实现的指针类型。具体来说,它指向一个实际的函数实现,并允许在运行时动态地调用该函数。

IMP 的类型定义如下:

objc 复制代码
typedef id (*IMP)(id, SEL, ...);
用于懒加载

相比直接声明并实现方法,动态方法解析提供了更多的控制权,可以根据需要决定是否加载方法的具体实现。

使用场景:一个类可能定义了很多方法,但并不是所有方法都会被使用,但即使不被使用编译器也会为它分配元数据。通过动态方法解析,可以避免为未使用的方法占用内存。方法实现的绑定延迟到实际调用时完成,减少类加载和初始化的开销。

objc 复制代码
#import "JCClass.h"
#import <objc/runtime.h>
@implementation JCClass
+ (BOOL)resolveInstanceMethod:(SEL)selector {
    NSString *selectorString = NSStringFromSelector(selector);
    NSLog(@"enter");
    // 检查方法选择器是否为 'heavyComputation',这是我们需要懒加载的方法
    if ([selectorString isEqualToString:@"heavyComputation"]) {
        // 使用 class_addMethod 为 `heavyComputation` 方法动态添加实现
        class_addMethod(self, selector, (IMP)heavyComputation, "@@:");
        return YES;  // 返回 YES 表示我们已经为该方法添加了实现
    }
    
    return [super resolveInstanceMethod:selector];  // 调用父类的实现
}

// 重的计算过程,模拟复杂计算
id heavyComputation(id self, SEL _cmd) {
    NSLog(@"1");
    // 模拟一个复杂的计算过程
    NSLog(@"Performing heavy computation...");
    
    // 假设我们计算结果并缓存它
    NSString *result = @"This is the result of the heavy computation.";
    
    // 将计算结果存储到关联对象中,以便下次访问时直接返回
    objc_setAssociatedObject(self, _cmd, result, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    return result;
}
@end
  
  
JCClass *obj = [[JCClass alloc] init];
NSLog(@"%@",[obj heavyComputation]);
NSLog(@"%@",[obj heavyComputation]);
  

这个程序运行得到以下结果,可以看到当我们调用heavyComputation的第一次就会进入resolveInstanceMethod,第二次就会直接调用经过动态绑定的方法

heavyComputation 是一个普通的函数,它的存在独立于任何类。通过 class_addMethod,它才被绑定为某个类的方法。

这里需要注意,我们需要在我们的MyClass类的头文件声明这个heavyComputation的方法,让编译器相信这个函数的存在

objc 复制代码
- (id)heavyComputation;

抑或者我们可以不在头文件之中声明,直接使用,就可以绕过编译器的警告:

objc 复制代码
[obj performSelector:@selector(heavyComputation)];

当我在学习到这一部分的时候,其实是很有疑问的,懒加载的目的是不让编译器在编译的过程之中进行对方法进行加载,但是C语言写成的函数还是放在了程序之中,只是没有绑定对象,这样做能节省什么资源呢?

在C语言之中函数在程序启动前就已经存在,并且占用一定的内存资源 ,但是它的内存分配主要体现在程序的 代码段 ,相比 C 语言函数,OC 方法,由于其动态的性质,内存开销通常更大,因为方法不仅包括代码,还包括方法名称 (SEL),方法的实现地址 (IMP),方法所属的类(元类里存储方法列表),其他运行时需要的元信息。

所以我们在编写OC程序的时候,在遇到不一定需要的功能时,可以避免加载,有利于提高程序的使用效率

消息转发

消息转发又被分为两个部分,一个是快速消息转发(Forwarding to another object) ,另一个是完整消息转发(Forwarding the Message)

快速消息转发

当我们在动态方法解析没有找到处理选择子的方法时,当前对象还有第二次机会对这个选择子的信息进行转发,我们就称为快速消息转发

快速消息转发机制通过 forwardingTargetForSelector: 方法将消息转发给另一个对象,这个对象会尝试执行该方法。如果目标对象能响应该消息,则继续处理。

obj 复制代码
- (id)forwardingTargetForSelector:(SEL)selector {
    if (selector == @selector(foo)) {
        return someOtherObject;  // 将消息转发给 someOtherObject
    }
    return [super forwardingTargetForSelector:selector];
}

其中这个someOtherObject是一个实例,如果 someOtherObject 能响应 foo 方法,则该方法会在 someOtherObject 上执行。

完整消息转发

如果 forwardingTargetForSelector: 返回了 nil 或者目标对象不能处理该消息,系统会进入完整的消息转发阶段,即通过 methodSignatureForSelector:forwardInvocation: 来处理。

首先,创建一个 NSInvocation 对象,将与尚未处理的消息相关的所有细节封装在其中。该对象包含以下信息:

  • 选择子(Selector):即方法名称。
  • 目标(Target):接收消息的对象。
  • 参数:调用方法时传递给方法的参数。

当触发 NSInvocation 对象时,消息派发系统(message-dispatch system)会介入,负责将消息转发给目标对象,执行相应的方法。

然而这样实现出来的方法与"备援接收者" 方案所实现的方法等效,所以很少有人采用这么简单的实现方式。

流程图:

总结

  • 若对象无法响应某个选择子,则进人消息转发流程。
  • 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
  • 对象可以把其无法解读的某些选择子转交给其他对象来处理。
  • 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。
相关推荐
百锦再3 小时前
Android Studio 项目文件夹结构详解
android·java·ide·ios·app·android studio·idea
season_zhu5 小时前
iOS开发:关于URL解析
ios·json·swift
iOS大前端海猫5 小时前
深入解析 Swift 中的并发属性包装器:@Actor、@MainActor 和 @GlobalActor
ios·app
ZRD11125 小时前
SwiftUI 表达式
swiftui·swift
溪饱鱼7 小时前
DHgate爆火背后的技术原因
android·前端·ios
增强7 小时前
腾讯云 人脸核身 Flutter 插件功能开发(一 IOS 端实现)
ios
鸿蒙布道师10 小时前
鸿蒙NEXT开发图片相关工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
Unlimitedz1 天前
iOS内存管理中的强引用问题
macos·ios·cocoa
雨夜赶路人1 天前
iOS开发--接入ADMob广告失败
ios
旭日猎鹰1 天前
iOS崩溃堆栈分析
ios