iOS——方法交换Method Swizzing

2024-09-06 01:28

本文主要是介绍iOS——方法交换Method Swizzing,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

什么是方法交换

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。
利用Objective-C Runtimee的动态绑定特性,将一个方法的实现与另一个方法的实现进行交换。交换两个方法的实现一般写在分类的load方法里面,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用多次。

在这里插入图片描述

方法交换的方式

  1. 获取方法的 SEL 和 IMP
    • 使用 class_getInstanceMethodclass_getClassMethod 函数获取方法的 Method 结构体。
    • Method 结构体中获取 SEL 和 IMP。
  2. 交换方法的 IMP
    • 使用 method_exchangeImplementations 函数交换两个方法的实现。
    • 或者使用 class_replaceMethod 函数替换方法的实现。
    // 类中获取oriSEL对应的方法实现Method oriMethod = class_getInstanceMethod(cls, oriSEL);// 获取swiSEL对应的方法实现Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);// 将两个方法实现进行交换,method_exchangeImplementations(oriMethod, swiMethod);

在进行方法交换操作时,建议放在单例下进行,以确保该操作只执行一次,避免重复调用导致交换效果被反转,从而失去交换的目的。
通过上面的方法可以理解,交换的是两者的方法实现

方法交换的四个风险

直接使用 Runtime 的方法进行方法交换会有很多风险,RSSwizzle库里指出了四个典型的直接使用 Runtime 方法进行方法交换的风险。

  • 第一个风险是,需要在 +load 方法中进行方法交换。因为如果在其他时候进行方法交换,难以保证另外一个线程中不会同时调用被交换的方法,从而导致程序不能按预期执行。而在 +load 方法中执行方法交换,确保交换在类加载时完成,从而避免线程竞争和其他时机相关的问题。

  • 第二个风险是,被交换的方法必须是当前类的方法,不能是父类的方法,直接把父类的实现拷贝过来不会起作用。父类的方法必须在调用的时候使用,而不是方法交换时使用。方法交换只能作用于当前类的方法,不能影响父类的方法。

  • 第三个风险是,交换的方法如果依赖了 cmd,那么交换后,如果 cmd 发生了变化,就会出现各种奇怪问题,而且这些问题还很难排查。特别是交换了系统方法,你无法保证系统方法内部是否依赖了 cmd。(cmd参数表示当前调用的方法)

  • 第四个风险是,方法交换命名冲突。如果出现冲突,可能会导致方法交换失败。

load方法的特点
+load方法在类加载时调用,确保方法交换在任何实例方法调用之前完成。
一般情况下load方法在每个类中都只会调用一次。
+load方法自动调用,不会被多个线程同时调用,结合 dispatch_once 确保线程安全。
+load方法自动执行,减少开发者的工作量。

第三个风险详解

第三个风险的意思是,两个方法实现交换后,_cmd却不一定。

_cmd回顾
_cmd 是隐藏的参数,表示当前方法的selector,他和self表示当前方法调用的对象实例。
获取当前被调用方法: NSStringFromSelector(_cmd)

比如下面这个例子:
我们首先创建了一个ViewController类,在这个类中我们写出将被交换的原方法orimed,然后创建一个swizzled分类,在分类中写出交换后的方法

#import "ViewController.h"
#import "ViewController+swizzled.h"
#import <objc/runtime.h>@interface ViewController ()
@property (assign, nonatomic) int ticketsCount;
@end@implementation ViewController+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{SEL oriSEL = @selector(orimed);SEL swiSEL = @selector(swizzledSelector);Method oriMethod = class_getInstanceMethod([self class], oriSEL);Method swiMethod = class_getInstanceMethod([self class], swiSEL);method_exchangeImplementations(oriMethod, swiMethod);});
}- (void)viewDidLoad {[super viewDidLoad];[self orimed];
}- (void) orimed {NSLog(@"交换前的方法");
}
#import "ViewController.h"NS_ASSUME_NONNULL_BEGIN
@interface ViewController (swizzled)- (void) swizzledSelector;@end
NS_ASSUME_NONNULL_END
#import "ViewController+swizzled.h"@implementation ViewController (swizzled)- (void)swizzledSelector {NSLog(@"方法已交换");//然后我们在这个方法中打印当前方法的selectorNSLog(@"%@", NSStringFromSelector(_cmd));
}@end

打印的结果:
在这里插入图片描述

我们的代码明明执行了swizzledSelector中的代码,为什么打印出的_cmd还是orimed呢?
这是因为方法交换本质上是互换了两个方法的实现,而不是选择器,这段代码实际上是将orimed的SEL指向了swizzleSelector方法的imp,所以执行swizzledSelector的代码实现时,返回的_cmd(方法的selector)为orimed。
因此如果交换的方法依赖于 cmd 来决定行为,可能会导致日志输出的信息不符合实际调用的方法。

方法交换的实际用法

先给要替换的方法的类添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来替换的方法也写在这个Category中。就像上面那个例子一样。
由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,在每个类中都只会调用一次,并且不需要我们手动调用。

注意:

  • Swizzling应该总在+load中执行
  • Swizzling应该总是在dispatch_once中执行
  • Swizzling在+load中执行时,不要调用[super load]。如果多次调用了[super load],可能会出现“Swizzle无效”的假象。
  • 为了避免Swizzling的代码被重复执行,我们可以通过GCD的dispatch_once函数来解决,利用dispatch_once函数内代码只会执行一次的特性。

方法交换的API

方案一:
提供了更精细的控制,可以选择性地添加、替换方法,并能处理方法不存在的情况。

//获取某个类的实例方法。
//cls:目标类。name:方法的选择器(selector)。
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
//获取方法的实现(IMP)
//m:方法(Method)
method_getImplementation(Method _Nonnull m) 
//向类中添加一个方法及其实现。
//cls:目标类。name:方法的选择器。imp:方法的实现。types:方法的类型编码(Type encoding)。
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 
//替换类中的方法实现。如果该方法不存在,则添加这个方法。
//cls:目标类。name:方法的选择器。imp:新的方法实现。types:方法的类型编码。
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

方案二:
直接交换两个方法的实现,步骤简单,但是少了一些灵活性。

//交换两个方法的实现。
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 

案例分析

案例一:递归调用

我们现在在上面原先代码的基础上修改一下:

#import "ViewController+swizzled.h"@implementation ViewController (swizzled)- (void)swizzledSelector {NSLog(@"方法已交换");//这里递归调用一下swizzledSelector方法[self swizzledSelector];
}@end

运行结果:
在这里插入图片描述

可以看出,并没有发生递归调用。反而只是打印出了原方法的内容,这是为什么呢?
这是因为进行了方法交换,所以调用方法swizzledSelector,会找到orimed的方法实现,而swizzledSelector中有调用swizzledSelector,而此时它的方法实现已经指向了orimed。见下图:
在这里插入图片描述

案例二:交换父类的方法

有如下代码:首先,我们创建一个FatherViewController类,该类有一个fatherMethod方法,然后该类有一个子类SonViewController,子类同样有一个sonMethod方法。在子类的实现中,我们将父类的fatherMethod和子类的sonMethod方法进行交换,然后在ViewController中调用父类的fatherMethod方法:

#import "FatherViewController.h"@interface FatherViewController ()
@end@implementation FatherViewController- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view.
}- (void)fatherMethod {NSLog(@"父类的方法");
}@end
#import "SonViewController.h"
#import <objc/runtime.h>@interface SonViewController ()@end@implementation SonViewController+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{SEL sonSEL = @selector(sonMethod);SEL fatherSEL = @selector(fatherMethod);Method sonMed = class_getInstanceMethod([self class], sonSEL);Method fatherMed = class_getInstanceMethod([self class], fatherSEL);method_exchangeImplementations(sonMed, fatherMed);});
}- (void)viewDidLoad {[super viewDidLoad];}- (void) sonMethod {//递归调用[self sonMethod];NSLog(@"子类的方法");
}@end

在ViewController中,使用子类的实例对象调用父类的方法:

- (void)viewDidLoad {[super viewDidLoad];[[[SonViewController alloc] init] fatherMethod];
}

执行结果:

在这里插入图片描述

可以得出,我们成功完成了在子类中和父类的方法进行交换。递归调用也没有出错。

但是如果此时我们在ViewController中使用父类的实例对象调用父类的方法呢?
我们现在修改ViewController中的代码:

- (void)viewDidLoad {[super viewDidLoad];[[[FatherViewController alloc] init] fatherMethod];
}

得到的结果却是:
在这里插入图片描述
代码运行时发生了错误,这是因为,使用父类的实例对象调用父类的方法时,由于发生了方法交换,因此父类执行的是子类的方法实现。在子类的方法实现中又调用了sonMethod方法,但是问题来了,此时实现子类方法的调用者是父类的实例对象,父类的实例对象中压根没有sonMethod方法的实现,这就导致了找不到sonMethod方法,因而产生了错误。

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

方法交换的应用

统计ViewController加载次数并打印

#import "UIViewController+Logging.h"
#import <objc/runtime.h>@implementation UIViewController (Logging)+ (void)load
{swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}- (void)swizzled_viewDidAppear:(BOOL)animated
{// call original implementation[self swizzled_viewDidAppear:animated];// LoggingNSLog(@"%@", NSStringFromClass([self class]));
}void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{// the method might not exist in the class, but in its superclassMethod originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);// class_addMethod will fail if original method already existsBOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));// the method doesn’t exist and we just added oneif (didAddMethod) {class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));}else {method_exchangeImplementations(originalMethod, swizzledMethod);}}

防止UI控件短时间多次激活事件

有时候会有这种需求,项目中的写好的按钮要求不能连续点击,这时候最方便的方法就是使用方法交换将系统的sendAction:to:forEvent:方法替换为自定义的swizzled_sendAction:to:forEvent:方法。在自定义方法中判断是否需要拦截点击事件。
UIControl+Limit.m:

#import "UIControl+Limit.h"
#import <objc/runtime.h>static const char *UIControl_acceptEventInterval="UIControl_acceptEventInterval";
static const char *UIControl_ignoreEvent="UIControl_ignoreEvent";@implementation UIControl (Limit)#pragma mark - acceptEventInterval
- (void)setAcceptEventInterval:(NSTimeInterval)acceptEventInterval
{objc_setAssociatedObject(self,UIControl_acceptEventInterval, @(acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}-(NSTimeInterval)acceptEventInterval {return [objc_getAssociatedObject(self,UIControl_acceptEventInterval) doubleValue];
}#pragma mark - ignoreEvent
-(void)setIgnoreEvent:(BOOL)ignoreEvent{objc_setAssociatedObject(self,UIControl_ignoreEvent, @(ignoreEvent), OBJC_ASSOCIATION_ASSIGN);
}-(BOOL)ignoreEvent{return [objc_getAssociatedObject(self,UIControl_ignoreEvent) boolValue];
}#pragma mark - Swizzling
+(void)load {Method a = class_getInstanceMethod(self,@selector(sendAction:to:forEvent:));Method b = class_getInstanceMethod(self,@selector(swizzled_sendAction:to:forEvent:));method_exchangeImplementations(a, b);//交换方法
}- (void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event
{if(self.ignoreEvent){NSLog(@"btnAction is intercepted");return;}if(self.acceptEventInterval>0){self.ignoreEvent=YES;[self performSelector:@selector(setIgnoreEventWithNo)  withObject:nil afterDelay:self.acceptEventInterval];}[self swizzled_sendAction:action to:target forEvent:event];
}-(void)setIgnoreEventWithNo{self.ignoreEvent=NO;
}@end

ViewController.m:

-(void)setupSubViews{UIButton *btn = [UIButton new];btn =[[UIButton alloc]initWithFrame:CGRectMake(100,100,100,40)];[btn setTitle:@"btnTest"forState:UIControlStateNormal];[btn setTitleColor:[UIColor redColor]forState:UIControlStateNormal];btn.acceptEventInterval = 3;[self.view addSubview:btn];[btn addTarget:self action:@selector(btnAction)forControlEvents:UIControlEventTouchUpInside];
}- (void)btnAction{NSLog(@"btnAction is executed");
}

防奔溃处理:数组越界问题

在实际项目中,有时会因为数组越界导致崩溃,需要一个解决方案来防止这种情况,即使数组越界也不会崩溃。
通过方法交换(Swizzling)替换NSArray的objectAtIndex:方法,添加防越界处理逻辑。

无痕埋点

这篇关于iOS——方法交换Method Swizzing的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1140604

相关文章

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

C#中读取XML文件的四种常用方法

《C#中读取XML文件的四种常用方法》Xml是Internet环境中跨平台的,依赖于内容的技术,是当前处理结构化文档信息的有力工具,下面我们就来看看C#中读取XML文件的方法都有哪些吧... 目录XML简介格式C#读取XML文件方法使用XmlDocument使用XmlTextReader/XmlTextWr

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

oracle DBMS_SQL.PARSE的使用方法和示例

《oracleDBMS_SQL.PARSE的使用方法和示例》DBMS_SQL是Oracle数据库中的一个强大包,用于动态构建和执行SQL语句,DBMS_SQL.PARSE过程解析SQL语句或PL/S... 目录语法示例注意事项DBMS_SQL 是 oracle 数据库中的一个强大包,它允许动态地构建和执行

Ubuntu固定虚拟机ip地址的方法教程

《Ubuntu固定虚拟机ip地址的方法教程》本文详细介绍了如何在Ubuntu虚拟机中固定IP地址,包括检查和编辑`/etc/apt/sources.list`文件、更新网络配置文件以及使用Networ... 1、由于虚拟机网络是桥接,所以ip地址会不停地变化,接下来我们就讲述ip如何固定 2、如果apt安

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.

在不同系统间迁移Python程序的方法与教程

《在不同系统间迁移Python程序的方法与教程》本文介绍了几种将Windows上编写的Python程序迁移到Linux服务器上的方法,包括使用虚拟环境和依赖冻结、容器化技术(如Docker)、使用An... 目录使用虚拟环境和依赖冻结1. 创建虚拟环境2. 冻结依赖使用容器化技术(如 docker)1. 创

Spring排序机制之接口与注解的使用方法

《Spring排序机制之接口与注解的使用方法》本文介绍了Spring中多种排序机制,包括Ordered接口、PriorityOrdered接口、@Order注解和@Priority注解,提供了详细示例... 目录一、Spring 排序的需求场景二、Spring 中的排序机制1、Ordered 接口2、Pri

Idea实现接口的方法上无法添加@Override注解的解决方案

《Idea实现接口的方法上无法添加@Override注解的解决方案》文章介绍了在IDEA中实现接口方法时无法添加@Override注解的问题及其解决方法,主要步骤包括更改项目结构中的Languagel... 目录Idea实现接China编程口的方法上无法添加@javascriptOverride注解错误原因解决方

MySql死锁怎么排查的方法实现

《MySql死锁怎么排查的方法实现》本文主要介绍了MySql死锁怎么排查的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录前言一、死锁排查方法1. 查看死锁日志方法 1:启用死锁日志输出方法 2:检查 mysql 错误