当前位置: 首页 > article >正文

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

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 和 uppercaseString
    NSLog(@"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.kler.cn/a/528537.html

相关文章:

  • vue2项目(一)
  • Spring Boot 中的事件发布与监听:深入理解 ApplicationEventPublisher(附Demo)
  • Flutter_学习记录_Tab的简单Demo~真的很简单
  • 玩转大语言模型——配置图数据库Neo4j(含apoc插件)并导入GraphRAG生成的知识图谱
  • 线段树(Segment Tree)和树状数组
  • 愿景:做机器视觉行业的颠覆者
  • 【自然语言处理(NLP)】深度学习架构:Transformer 原理及代码实现
  • 2025_2_1 C语言中关于字符串
  • 从 HTTP/1.1 到 HTTP/3:如何影响网页加载速度与性能
  • 交易股指期货有什么技巧吗?
  • C++中的构造器(Constructor)(也称为构造函数)
  • 三、js笔记
  • 扬帆启航于数据结构算法之雅舟旅程,悠然漫步于C++秘境——探索线性表之栈的绮丽定义与精妙实现
  • 10.[前端开发-CSS]Day10-CSS的浮动和flex布局
  • 【LeetCode: 81. 搜索旋转排序数组 II + 二分查找】
  • 汽车中控屏HMI界面,安全和便捷是设计的两大准则。
  • 调音基础学习
  • 【LLM-agent】(task3)数据库对话Agent和RAG接入Agent
  • 【数据结构-前缀树】力扣208. 实现 Trie (前缀树)
  • Baklib揭示内容中台实施最佳实践的策略与实战经验
  • 好用的翻译工具
  • 基于VMware的ubuntu与vscode建立ssh连接
  • java练习(1)
  • 网络编程套接字(中)
  • 力扣动态规划-17【算法学习day.111】
  • 深入解析“legit”的地道用法——从俚语到正式表达:Sam Altman用来形容DeepSeek: legit invigorating(真的令人振奋)