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

devtools/2025/2/3 15:41:24/

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/devtools/155757.html

相关文章

7 Spark 底层执行原理

7 Spark 底层执行原理 1. 从代码角度看 DAG 图的构建2. 将 DAG 划分为 Stage 核心算法3. 将 DAG 划分为 Stage 剖析4. 提交 Stages5. 监控 Job、Task、Executor6. 获取任务执行结果7. 任务调度总体诠释Spark 运行架构特点 Spark 运行流程 Spark运行流程 具体运行流程如下&…

ASP.NET Core 中使用依赖注入 (DI) 容器获取并执行自定义服务

目录 一、ASP.NET Core 中使用依赖注入 (DI) 容器获取并执行自定义服务 1. app.Services 2. GetRequiredService() 3. Init() 二、应用场景 三、依赖注入使用拓展 1、使用场景 2、使用步骤 1. 定义服务接口和实现类 2. 注册服务到依赖注入容器 3. 使用依赖注入获取并…

14 2D矩形模块( rect.rs)

一、 rect.rs源码 // Copyright 2013 The Servo Project Developers. See the COPYRIGHT // file at the top-level directory of this distribution. // // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or // http://www.apache.org/licenses/LICENS…

TypeScript语言的语法糖

TypeScript语言的语法糖 TypeScript作为一种由微软开发的开源编程语言&#xff0c;它在JavaScript的基础上添加了一些强类型的特性&#xff0c;使得开发者能够更好地进行大型应用程序的构建和维护。在TypeScript中&#xff0c;不仅包含了静态类型、接口、枚举等强大的特性&…

计算机毕业设计Python动漫推荐系统 漫画推荐系统 动漫视频推荐系统 机器学习 bilibili动漫爬虫 数据可视化 数据分析 大数据毕业设计

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

vim的多文件操作

[rootxxx ~]# vim aa.txt bb.txt cc.txt #多文件操作 next #下一个文件 prev #上一个文件 first #第一个文件 last #最后一个文件 快捷键: ctrlshift^ #当前和上个之间切换 说明&#xff1a;快捷键ctrlshift^&#xff0c…

书生大模型实战营7

文章目录 L1——基础岛提示词工程实践什么是Prompt(提示词)什么是提示工程提示设计框架CRISPECO-STAR LangGPT结构化提示词LangGPT结构编写技巧构建全局思维链保持上下文语义一致性有机结合其他 Prompt 技巧 常用的提示词模块 浦语提示词工程实践(LangGPT版)自动化生成LangGPT提…

计算机网络网络层进阶:NAT、ARP 与 IP 系列技术全析!!!

一、网络地址转换NAT 私有IP 地址(内网IP)&#xff1a; 10.0.0.0~10.255.255.255 172.16.0.0~172.31.255.255 192.168.0.0~192.168.255.255 只允许分配给局域网内部的节点,不允许分配给互联网上的节点每个局域网内部都可以自行分配这些私有 IP 地址私有 IP 地址是可复用的&…