Effective Objective-C 2.0 读书笔记—— 方法调配(method swizzling)

news/2025/2/2 18:08:17/

Effective Objective-C 2.0 读书笔记—— 方法调配(method swizzling)

文章目录

  • Effective Objective-C 2.0 读书笔记—— 方法调配(method swizzling)
    • 前言
    • IMP
      • **`SEL` 和 `IMP` 在 `objc_msgSend` 中的关系**
    • 方法调配
      • 实现方法交换
    • 用于调试程序
      • **示例:调试 `viewWillAppear:` 方法**
    • 总结

前言

前面我们说到了,我们在调用方法时,实际上就是在调用objc_msgSend这个C语言函数,让OC之中的方法能够被成功解析。那么,就有一个随之而来的问题,我们是否可以在选择子有具体对应方法时,在运行期之中对其进行改变?这个答案是肯定的,这个方法可以让我门不需要源代码,也不需要继承子类来覆写方法就能改变这个类本身的功能,这样子这个新功能就能够在所有的类中的实例中生效,这个方法我们就称之为 方法调配(method swizzling)

IMP

在前面的笔记之中,我提到了IMP其实就是一个函数指针,原型如下:

typedef id (*IMP)(id, SEL, ...)

如果说SEL存储的是方法的具体地址,那么IMP这个指针指向的就是方法的C语言底层实现,我们知道方法名(SEL)是哈希化存储的,可以快速查找对应的 IMP,而不是遍历所有方法,我们只需要查找相同的 SEL 在指向的 IMP列表。

SELIMPobjc_msgSend 中的关系

当我们调用一个方法时,本质上是:

[obj someMethod];  // 实际上是等价于:
objc_msgSend(obj, @selector(someMethod));

执行过程

  1. objc_msgSend 通过 SEL 在类的方法列表中查找方法实现(IMP)。
  2. 找到 IMP 后,直接调用该方法的实现。

如果我们手动查找 IMP 并调用,就可以跳过 objc_msgSend 的方法查找,提高执行效率:

IMP imp = [obj methodForSelector:@selector(someMethod)];
imp(obj, @selector(someMethod));  // 直接调用方法

方法调配

书中拿NSString类举例,NSString类可以相关lowercaseString、uppercaseString、capitalizedString的方法,那么每一个方法都映射在了不一样的IMP之中

image-20250129164440119

OC提供了几个C语言方法,去操作这张NSString类的方法映射表,我们可以在这个表中新增选择子,也可以改变选择子的实现,若经过几次操作就可以变成一下列表

请添加图片描述

在新的映射表中,多了一个名为newSelector的选择子,capitalizedString的实现也变了, 而lowercaseStringuppercaseString的实现则互换了。上述修改均无须编写子类,只要修改 了“方法表” 的布局,就会反映到程序中所有的NSString实例之上

实现方法交换

想交换方法实 现 ,可用下列函数 :

void method_exchangeImplementations (Method ml, Method m2)

此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:

Method class_getInstanceMethod (Class aClass, SEL aSelector)

获取方法的 IMP 指针(函数指针)

method_getImplementation(Method method)

还是拿书中的例子,执行下列代码,即可交换前面提到 的l owercaseString 与uppercaseString 方法实现:

void swizzleNSStringMethods(void) {Class stringClass = [NSString class];Method m1 = class_getInstanceMethod(stringClass, @selector(lowercaseString));Method m2 = class_getInstanceMethod(stringClass, @selector(uppercaseString));method_exchangeImplementations(m1, m2);
}- (void)viewDidLoad {[super viewDidLoad];NSString *testString = @"Hello World!";NSLog(@"Original lowercaseString: %@", [testString lowercaseString]);NSLog(@"Original uppercaseString: %@", [testString uppercaseString]);// 进行方法交换swizzleNSStringMethods();// 再次调用 lowercaseString 和 uppercaseStringNSLog(@"After Swizzling lowercaseString: %@", [testString lowercaseString]);NSLog(@"After Swizzling uppercaseString: %@", [testString uppercaseString]);
}

我们可以看到,编译器出现以下结果,我们调用了方法交换,使得两个方法的实现方式变交换了

image-20250130155833499

但是我们一般也不用这些方法进行交换,因为这些方法已经被封装的很好了,把两个方法交换再使用,会对程序的可读性造成很大影响,着实是没有必要的多余之举。

按照书里的例子,我们一般是将方法进行修改,即重写子类的方法,例如:

新方法可以添加至NSString的 一个“分类”(category)中:

@interface NSString (E0CMyAdditions)- (NSString*) eoc_myLowercaseString; 
@end

新方法的实现代码可以这样写:

@implementation NSString (EOCMyAdditions)- (NSString*) eoc_myLowercaseString (NSString *lowercase = [self eoc_myLowercaseString) ;NSLog (@"80 >= %@", self, lowercase) ;return lowercase;
}
@end

这个程序看似会造成循环调用的问题,但是由于我们进行了方法调配,所以实际上调用的是lowercaseString的方法,这样子就可以在不重写子类的情况下进行

用于调试程序

示例:调试 viewWillAppear: 方法

在 iOS 开发中,我们可以通过 方法交换拦截 UIViewControllerviewWillAppear: 方法,并在调用时打印日志,从而调试某个界面是否被正确加载。

#import <UIKit/UIKit.h>
#import <objc/runtime.h>@implementation UIViewController (Debugging)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{// 获取原始的 viewWillAppear: 方法Method originalMethod = class_getInstanceMethod([self class], @selector(viewWillAppear:));// 获取我们自定义的调试方法Method debugMethod = class_getInstanceMethod([self class], @selector(debug_viewWillAppear:));// 交换方法实现method_exchangeImplementations(originalMethod, debugMethod);});
}// 这个方法会替换掉原始的 viewWillAppear:
- (void)debug_viewWillAppear:(BOOL)animated {// 先调用原来的 viewWillAppear:(注意这里调用的是 debug_viewWillAppear:,但其实它已经是原来的 viewWillAppear:)[self debug_viewWillAppear:animated];// 打印日志NSLog(@"[DEBUG] %@ will appear", NSStringFromClass([self class]));
}@end

+load 方法中执行交换

+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Method originalMethod = class_getInstanceMethod([self class], @selector(viewWillAppear:));Method debugMethod = class_getInstanceMethod([self class], @selector(debug_viewWillAppear:));method_exchangeImplementations(originalMethod, debugMethod);});
}
  • +load 方法会在类被加载到内存时自动执行,确保方法交换只发生一次。
  • dispatch_once 避免 load 方法被执行多次,防止方法被重复交换。

拦截 viewWillAppear: 方法

- (void)debug_viewWillAppear:(BOOL)animated {[self debug_viewWillAppear:animated]; // 其实调用的是原来的 viewWillAppear:NSLog(@"[DEBUG] %@ will appear", NSStringFromClass([self class]));
}
  • 因为 method_exchangeImplementations,调用 debug_viewWillAppear: 实际上会执行原来的 viewWillAppear:
  • 这样,我们在不修改 UIViewController 原始代码的情况下,为所有 ViewController 统一添加了调试日志

总结

  1. 在运行期,可以向类中新增或替换选择子所对应的方法实现。
  2. 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此 技术向原有实现中添加新功能。
  3. 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。若是滥用,反而会令代 码变得不易读懂且难于维护。

http://www.ppmy.cn/news/1568758.html

相关文章

新能源算力战争:为什么AI大模型需要绿色数据中心?

新能源算力战争:为什么AI大模型需要绿色数据中心? 近年来,人工智能(AI)大模型的爆发式增长正在重塑全球科技产业的格局。以GPT-4、Gemini、Llama等为代表的千亿参数级模型,不仅需要海量数据训练,更依赖庞大的算力支撑。然而,这种算力的背后隐藏着一个日益严峻的挑战——…

Flutter开发环境配置

下载 Flutter SDK 下载地址&#xff1a;https://docs.flutter.cn/get-started/install M1/M2芯片选择带arm64字样的Flutter SDK。 解压 cd /Applications unzip ~/Downloads/flutter_macos_arm64_3.27.3-stable.zip执行 /Applications/flutter/bin/flutterManage your Flut…

怎么调整香港服务器硬盘分区大小?

调整香港服务器的硬盘分区大小需要小心操作&#xff0c;因为操作不当可能会导致数据丢失。以下是如何在 Linux 和 Windows 系统中调整硬盘分区大小的详细步骤&#xff1a; Linux 系统调整硬盘分区大小&#xff1a; 1. 检查当前分区情况 运行以下命令查看硬盘分区信息&#xff1…

vim多文件操作如何同屏开多个文件

[rootxxx ~]# vimdiff aa.txt bb.txt cc.txt #带颜色比较的纵向排列打开的同屏多文件操作 示例&#xff1a; [rootxxx ~]# vimdiff -o aa.txt bb.txt cc.txt #带颜色比较的横向排列打开的同屏多文件操作 示例&#xff1a; [rootxxx ~]# vim -O aa.txt bb.txt c…

Synology 群辉NAS安装(10)安装confluence

Synology 群辉NAS安装&#xff08;10&#xff09;安装confluence 写在前面本着一朝鲜吃遍天的原则&#xff0c;我又去了这个github的作者那里翻车的第一次尝试手工创建数据库制作一个新的docker-compose of confluence 不折腾但成功启动的版本 写在前面 在装完jira之后&#x…

react-native网络调试工具Reactotron保姆级教程

在React Native开发过程中&#xff0c;调试和性能优化是至关重要的环节。今天&#xff0c;就来给大家分享一个非常强大的工具——Reactotron&#xff0c;它就像是一个贴心的助手&#xff0c;能帮助我们更轻松地追踪问题、优化性能。下面就是一份保姆级教程哦&#xff01; 一、…

Golang 执行流程分析

文章目录 1. 编译和运行2. 编译和运行说明 1. 编译和运行 如果是对源码编译后&#xff0c;再执行&#xff0c;Go的执行流程如下图 如果我们是对源码直接 执行 go run 源码&#xff0c;Go的执行流程如下图 两种执行流程的方式区别 如果先编译生成了可执行文件&#xff0c;那么…

网络仿真工具Core环境搭建

目录 安装依赖包 源码下载 Core安装 FAQ 下载源码TLS出错误 问题 解决方案 找不到dbus-launch 问题 解决方案 安装依赖包 调用以下命令安装依赖包 apt-get install -y ca-certificates git sudo wget tzdata libpcap-dev libpcre3-dev \ libprotobuf-dev libxml2-de…