【iOS】—— 方法交换

方法交换

1. 方法交换的原理

每个继承于NSObject的类都可以获得runtime的支持,在这样一个类中,有一个isa指针,指针指向该类定义的结构体,这个结构体中包括,指向其父类定义的指针以及Dispatch tableDispatch table是一张SELIMP的对应表。

也就是说需要通过方法编号SEL最后还有通过Dispatch table寻找对应的IMPIMP是函数指针,然后执行这个方法。

方法编号SEL和实现方法IMP的对应关系:

方法交换后的对应关系:

oriSEL的方法实现变成了swiIMP
swiSEL的方法实现变成了oriIMP

也就是调用oriSEL方法,最终方法实现是swiIMP

方法交换的方式:

objective-c 复制代码
  // 类中获取oriSEL对应的方法实现
  Method oriMethod = class_getInstanceMethod(cls, oriSEL);
  // 获取swiSEL对应的方法实现
  Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
  // 将两个方法实现进行交换,
  method_exchangeImplementations(oriMethod, swiMethod);

再进行方法交换操作,建议在单例模式下进行,避免重复调用交换。

2. 方法交换案例

1)递归问题分析

创建一个LGStudent类,类中有两个方法lg_studentInstanceMethodstudentInstanceMethod,在load方法对这两个方法进行交换,同时lg_studentInstanceMethod的实现中再次调用lg_studentInstanceMethod方法。

objective-c 复制代码
#import "ViewController.h"
#import "LGStudent.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    LGStudent *s = [[LGStudent alloc] init];
    [s studentInstanceMethod];
    
}
@end
  
#import "LGStudent.h"
#import <objc/runtime.h>

@implementation LGStudent
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"方法交换---:%s", __func__);
        Method oriIMP = class_getInstanceMethod(self, @selector(studentInstanceMethod));
        Method swiIMP = class_getInstanceMethod(self, @selector(lg_studentInstanceMethod));
        method_exchangeImplementations(oriIMP, swiIMP);
    });
}

- (void)lg_studentInstanceMethod {
    [self lg_studentInstanceMethod];
    NSLog(@"LGStudent对象方法:%s", __func__);
}

- (void)studentInstanceMethod {
    NSLog(@"LGStudent对类方法:%s", __func__);
}

@end

lg_studentInstanceMethod中再次调用该方法,是否会引起递归调用?

运行结果:

**分析:**没有引起递归,因为进行了方法交换,所以调用对象方法studentInstanceMethod,会找到lg_studentInstanceMethod的方法实现,而lg_studentInstanceMethod中调用lg_studentInstanceMethod,而此时该方法的指向已经指向studentInstanceMethod。如下图所示:

2)交换父类方法

创建一个LGSstudent类,类中有一个实例方法,lg_studentInstanceMethod,其父类LGPerson中有一个实例方法personInstanceMethod,在LGStudent类的load方法中对进行方法交换,将lg_studentInstanceMethod方法交换成父类中的personInstanceMethod方法。

objective-c 复制代码
#import "ViewController.h"
#import "LGStudent.h"
#import "LGPerson.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    LGPerson *p = [[LGPerson alloc] init];
    [p personInstanceMethod];
}
@end
  
  
  
#import "LGPerson.h"

@implementation LGPerson
- (void)personInstanceMethod{
    NSLog(@"person对象方法:%s",__func__);
}

@end

#import "LGStudent.h"
#import <objc/runtime.h>

@implementation LGStudent
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"方法交换---:%s", __func__);
        Method oriIMP = class_getInstanceMethod(self, @selector(personInstanceMethod));
        Method swiIMP = class_getInstanceMethod(self, @selector(lg_studentInstanceMethod));
        method_exchangeImplementations(oriIMP, swiIMP);
    });
}

- (void)lg_studentInstanceMethod {
    [self lg_studentInstanceMethod];
    NSLog(@"LGStudent对象方法:%s", __func__);
}

运行结果:

**分析:**成功调用,子类调用父类的方法personInstanceMethod,通过消息发送的原理,会进行慢速查找,找到父类方法,此时父类方法的实现变成了子类的lg_studentInstanceMethod方法,在子类中调用lg_studentInstanceMethod方法,最终的实现的方法为父类的personInstanceMethod

如果调用父类对象的personInstanceMethod方法?

objective-c 复制代码
#import "ViewController.h"
#import "LGStudent.h"
#import "LGPerson.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    LGPerson *p = [[LGPerson alloc] init];
    [p personInstanceMethod];
}
@end
  
  
  
#import "LGPerson.h"

@implementation LGPerson
- (void)personInstanceMethod{
    NSLog(@"person对象方法:%s",__func__);
}

@end

#import "LGStudent.h"
#import <objc/runtime.h>

@implementation LGStudent
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"方法交换---:%s", __func__);
        Method oriIMP = class_getInstanceMethod(self, @selector(personInstanceMethod));
        Method swiIMP = class_getInstanceMethod(self, @selector(lg_studentInstanceMethod));
        method_exchangeImplementations(oriIMP, swiIMP);
    });
}

- (void)lg_studentInstanceMethod {
    [self lg_studentInstanceMethod];
    NSLog(@"LGStudent对象方法:%s", __func__);
} 

运行结果:

**分析:**报错,父类调用personInstanceMethod方法,执行的是子类lg_studentInstanceMethod实现,并且同时调用lg_studentInstanceMethod方法,但是父类中并没有lg_studentInstanceMethod的实现,找不到这个方法,所以报错。

如果进行方法交换,一定要确保方法已经实现,否则会出现本例中啃爹的现象(方法交换,而父类没有方法的实现,导致报错)。所以在进行相关方法交换时,尽量避免涉及到其父类或者其子类的方法。

3. 方法交换思路

为避免上面案例的问题,总结以下实现思路:

为避免上面案例的问题,总结以下实现思路:

objective-c 复制代码
 + (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{

    if (!cls) NSLog(@"传入的交换类不能为空");
    
    // 获取类中的方法
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    // 要被交换的方法
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    // 判断类中是否存在该方法-避免动作没有意义
    if (!oriMethod) { 

        // 在oriMethod为nil时,添加oriSEL的方法,实现为swiMethod
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));

        // 替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){

            NSLog(@"来了一个空的 imp");
        }));
    }

    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
    //oriSEL:personInstanceMethod

    // 向类中添加oriSEL方法,方法实现为swiMethod
    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    
    // 自己有意味添加方法失败-所以这里会是false
    if (didAddMethod) {
        // 如果添加成功,表示原本没有oriMethod方法,此时将swizzledSEL的方法实现,替换成oriMethod实现
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        // 方法交换
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}
相关推荐
OKXLIN2 小时前
IOS UITextField 无法隐藏键盘问题
ios·objective-c
Macdo_cn9 小时前
My Metronome for Mac v1.4.2 我的节拍器 支持M、Intel芯片
macos·音视频
AL.千灯学长9 小时前
DeepSeek接入Siri(已升级支持苹果手表)完整版硅基流动DeepSeek-R1部署
人工智能·gpt·ios·ai·苹果vision pro
吹泡泡的派大星10 小时前
从0-1搭建mac环境最新版
macos
zhouwu_linux10 小时前
MT7628基于原厂的SDK包, 修改ra1网卡的MAC方法。
linux·运维·macos
丁总学Java10 小时前
在 macOS 的 ARM 架构上按住 Command (⌘) + Shift + .(点)。这将暂时显示隐藏文件和文件夹。
macos
青木川崎10 小时前
Mac下常用命令
macos
ClaNNEd@10 小时前
Mac端homebrew安装配置
macos·brew
nicekwell11 小时前
macos sequoia 禁用 ctrl+enter 打开鼠标右键菜单功能
macos
丁总学Java16 小时前
在 Mac ARM 架构的 macOS 系统上启用 F1 键作为 Snipaste 的截屏快捷键
macos·snipaste