iOS ------ Method Swizzling (动态方法交换)

一,Method Swizzling 简介

Method(方法)对应的是objc_method结构体;而objc_method结构体中包含了SEL method_name(方法名),IMP method_imp(方法实现)

// objc_method 结构体
typedef struct objc_method *Method;struct objc_method {SEL _Nonnull method_name;                    // 方法名char * _Nullable method_types;               // 方法类型IMP _Nonnull method_imp;                     // 方法实现
};

Method(方法),SEL(方法名),IMP(方法实现)三者的关系:

在运行中,class(类)维护了一个method list(方法列表)来确定消息的正确发送。OC中调用方法叫做发送消息,发送消息前会查找消息,查找过程就是通过SEL查找IMP的过程。method list (方法列表)存放的元素就是Method(方法)。而Method(方法)中映射了一对键值对:SEL(方法名)IMP(方法实现)

原理:
Method swizzling修改了method list(方法列表),使不同Method(方法)中的键值对发生了交换。比如交换前两个键值对分别为SEL A:IMP A,SEL B:IMP B,交换之后就变为了SEL A : IMP B、SEL B : IMP A。
在这里插入图片描述

二,MethodSwizzling简单代码实现

在当前类的+(void)load方法中增加Method Swizzling操作,交换(void)originalFunction(void)swizzledFunction的方法实现。

#import "ViewController.h"
#import <objc/runtime.h>
@interface ViewController ()@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view.[self SwizzlingMethod];[self originalFunction];[self swizzledFunction];}
- (void)SwizzlingMethod {//当前类Class class = [self class];//方法名SEL originalSeletor = @selector(originalFunction);SEL swizzledSeletor = @selector(swizzledFunction);//方法结构体Method originalMethod = class_getInstanceMethod(class, originalSeletor);Method swizzledMethod = class_getInstanceMethod(class, swizzledSeletor);//调用交换两个方法的实现method_exchangeImplementations(originalMethod, swizzledMethod);
}
//原始方法
- (void)originalFunction {NSLog(@"originalFunction");
}
//替换方法
- (void)swizzledFunction {NSLog(@"swizzledFunction");
}
@end

在这里插入图片描述

上面的代码简单的将两个方法进行了交换。但在实际应用中并不是那么简单,更多的是为当前类添加一个分类,然后在分类中进行MethodSwimming操作,并且要考虑的东西要更多,且更复杂。

三,MethodSwizzling的使用方案

一般是在该类的分类中添加MethodSwizzling交换方法

@implementation UIViewController (Swizzling)// 交换 原方法 和 替换方法 的方法实现
+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{// 当前类Class class = [self class];// 原方法名 和 替换方法名SEL originalSelector = @selector(originalFunction);SEL swizzledSelector = @selector(swizzledFunction);// 原方法结构体 和 替换方法结构体Method originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);/* 如果当前类没有 原方法的 IMP,说明在从父类继承过来的方法实现,* 需要在当前类中添加一个 originalSelector 方法,* 但是用 替换方法 swizzledMethod 去实现它 */BOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));if (didAddMethod) {// 原方法的 IMP 添加成功后,修改 替换方法的 IMP 为 原始方法的 IMPclass_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));} else {// 添加失败(说明已包含原方法的 IMP),调用交换两个方法的实现method_exchangeImplementations(originalMethod, swizzledMethod);}});
}// 原始方法
- (void)originalFunction {NSLog(@"originalFunction");
}// 替换方法
- (void)swizzledFunction {NSLog(@"swizzledFunction");
}@end

一些用到的方法

通过SEL获取方法Method

// 获取实例方法
OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);// 获取类方法
OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name);

IMP的getter/setter方法

// 获取一个方法的实现
OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m); // 设置一个方法的实现
OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)

替换方法

// 获取方法实现的编码类型
OBJC_EXPORT const char * _Nullable
method_getTypeEncoding(Method _Nonnull m);// 添加方法实现
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types);// 替换方法的 IMP,如:A替换B(B指向A,A还是指向A)
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types);// 交换两个方法的 IMP,如:A交换B(B指向A,A指向B)
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);

四,注意事项

1.保证方法交换只执行一次

为了保证方法交换的代码可以优先交换,一般会将其写在+load方法中,但是+load的方法也能被主动调用,如果多次调用就会被还原,如果调用[super load] 方法也会造成这样的结果;所以我们要保证方法只交换一次,可选择在单例模式下。

+ (void)load{static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{[self lz_methodSwizzlingWithClass:self oriSEL:@selector(study) swizzledSEL:@selector(play)];});
}

2.用子类方法替换父类方法

在子类中用子类的方法subFuntionA替换父类的方法function A。子类实例和父类实例分别调用function A,最终都实现的是subFuntionA。

如果我们在子类的方法subFuntionA1替换了父类中的方法functionA后想要继续调用functionA,同理应该这样写

- (void)subFunctionA {[self subFunctionA];NSLog(@"%s", __func__);
}

再用子类实例和父类实例分别调用function A。

父类调用时就会报错,子类调用就不会。

在上面的函数中调用subFuntionA,但父类本身方法列表中没subFuntionA,所以父类也就报了unrecognized selector 的错误。

出现上面找不到方法的原因是:子类用自己的实现直接替换了父类的方法。

如果我们能不能为子类添加一个和父类一样的方法,子类中进行替换就不会影响父类了。

+ (void)swizzingClassB:(Class)cls oldSEL:(SEL)oldSel toNewSel:(SEL)newSel {if (!cls) { return; }Method oldM = class_getInstanceMethod(cls, oldSel);Method newM = class_getInstanceMethod(cls, newSel);// 先尝试给 cls 添加方法(SEL: oldSel  IMP: newM),防止子类直接替换父类中的方法BOOL addSuccess = class_addMethod(cls, oldSel, method_getImplementation(newM), method_getTypeEncoding(oldM));if (addSuccess) { // 添加成功即:原本没有 oldSel,成功为子类添加了一个 oldSel - newM 的方法// 这里将原 newSel的imp替换为 oldM 的 IMPclass_replaceMethod(cls, newSel, method_getImplementation(oldM), method_getTypeEncoding(oldM));}else {method_exchangeImplementations(oldM, newM);}
}
  • 使用class_addMethod为当前类添加functionA方法,关联subFuntionA方法的imp
  • 返回值为NO,说明子类已经实现了subFuntionA,则直接进行方法交换,不会影响父类
  • 返回值为YES,说明子类未实现了subFuntionA,添加成功后,使用class_replaceMethod将sub functionA替换为functionA的imp

再用子类实例和父类实例分别调用function A。
这时父类实例调用functionA没有受到子类方法交换的影响,实现的就是functionA。
而子类实例就会在实现subfunctionA中实现function A。

五,MethodSwizzling应用场景

1,为UITableView的异常加载占位图

对于UITableView的异常加载情况分为无数据或网络异常。
对于检测tableView是否为空,借助tableView的代理dataSource即可。核心代码是,依次获取table View所具有的组数和行数,通过isEmpty这个flag标示最后确定是否添加占位图。

- (void)checkEmpty {BOOL isEmpty = YES;//flag标示id  dataSource = self.dataSource;NSInteger sections = 1;//默认一组if ([dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {sections = [dataSource numberOfSectionsInTableView:self];//获取当前TableView组数}for (NSInteger i = 0; i < sections; i++) {NSInteger rows = [dataSource tableView:self numberOfRowsInSection:sections];//获取当前TableView各组行数if (rows) {isEmpty = NO;//若行数存在,不为空}}if (isEmpty) {//若为空,加载占位图if (!self.placeholderView) {//若未自定义,展示默认占位图[self makeDefaultPlaceholderView];}self.placeholderView.hidden = NO;[self addSubview:self.placeholderView];} else {//不为空,隐藏占位图self.placeholderView.hidden = YES;}
}

接下来实现如何添加占位图
如果可以让tableView在执行reloadData时自动检查其行数就可以了。也就是我们在原有的reload Data方法的基础上添加checkEmpty此方法。这里我们可以通过MethodSwizzling替换reload Data方法,给予它新的实现。

+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{//方法交换,将reloadData实现交换为sure_reloadData[self methodSwizzlingWithOriginalSelector:@selector(reloadData) bySwizzledSelector:@selector(sure_reloadData)];});
}- (void)sure_reloadData {[self checkEmpty];[self sure_reloadData];
}

这样就可以在实现reloadData的同时检查行数从而判断我是否加载占位图的功能。

具体实现demo
tableView的异常加载占位图

2,处理UIButton的重复点击

避免一个按钮被快速点击多次。同样利用Method Swizzling

  • 为 UIControl 或 UIButton 建立一个 Category。
  • 在分类中添加一个 NSTimeInterval xxx_acceptEventInterval; 的属性,设定重复点击间隔
  • 在分类中实现一个自定义的 xxx_sendAction:to:forEvent: 方法,在其中添加限定时间相应的方法。
  • 利用 Method Swizzling 将 sendAction:to:forEvent: 方法和 xxx_sendAction:to:forEvent: 进行方法交换。
#import "UIButton+TBCustom.h"
#import <objc/runtime.h>@interface UIButton()@property (nonatomic, assign) NSTimeInterval custom_acceptEventInterval; // 可以用这个给重复点击加间隔@end@implementation UIButton (TBCustom)+ (void)load{Method systemMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));SEL sysSEL = @selector(sendAction:to:forEvent:);Method customMethod = class_getInstanceMethod(self, @selector(custom_sendAction:to:forEvent:));SEL customSEL = @selector(custom_sendAction:to:forEvent:);//添加方法 语法:BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 若添加成功则返回No// cls:被添加方法的类  name:被添加方法方法名  imp:被添加方法的实现函数  types:被添加方法的实现函数的返回值类型和参数类型的字符串BOOL didAddMethod = class_addMethod(self, sysSEL, method_getImplementation(customMethod), method_getTypeEncoding(customMethod));//如果系统中该方法已经存在了,则替换系统的方法  语法:IMP class_replaceMethod(Class cls, SEL name, IMP imp,const char *types)if (didAddMethod) {class_replaceMethod(self, customSEL, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));}else{method_exchangeImplementations(systemMethod, customMethod);}
}- (NSTimeInterval )custom_acceptEventInterval{return [objc_getAssociatedObject(self, "UIControl_acceptEventInterval") doubleValue];
}- (void)setCustom_acceptEventInterval:(NSTimeInterval)custom_acceptEventInterval{objc_setAssociatedObject(self, "UIControl_acceptEventInterval", @(custom_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}- (NSTimeInterval )custom_acceptEventTime{return [objc_getAssociatedObject(self, "UIControl_acceptEventTime") doubleValue];
}- (void)setCustom_acceptEventTime:(NSTimeInterval)custom_acceptEventTime{objc_setAssociatedObject(self, "UIControl_acceptEventTime", @(custom_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}- (void)custom_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{// 如果想要设置统一的间隔时间,可以在此处加上以下几句// 值得提醒一下:如果这里设置了统一的时间间隔,只会影响UIButton, 如果想统一设置,也想影响UISwitch,建议将UIButton分类,改成UIControl分类,实现方法是一样的if (self.custom_acceptEventInterval <= 0) {// 如果没有自定义时间间隔,则默认为.4秒self.custom_acceptEventInterval = .4;}// 是否小于设定的时间间隔BOOL needSendAction = (NSDate.date.timeIntervalSince1970 - self.custom_acceptEventTime >= self.custom_acceptEventInterval);// 更新上一次点击时间戳if (self.custom_acceptEventInterval > 0) {self.custom_acceptEventTime = NSDate.date.timeIntervalSince1970;}// 两次点击的时间间隔小于设定的时间间隔时,才执行响应事件if (needSendAction) {[self custom_sendAction:action to:target forEvent:event];}
}

addTarget:action:forControlEvents: 方法将 buttonTapped: 方法与按钮的点击事件关联起来。当用户点击按钮时,按钮会调用 sendAction:to:forEvent: 方法,并将 buttonTapped: 方法作为动作发送给指定的目标对象(在这里是 self,即当前对象)

3,处理数组越界的问题

Method Swizzling 可以用于解决数组越界导致的崩溃问题。通过交换 NSArray 或 NSMutableArray 的方法实现,我们可以在访问数组元素之前进行边界检查,以防止越界访问。

#import <objc/runtime.h>@implementation NSArray (SafeAccess)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class class = [self class];SEL originalSelector = @selector(objectAtIndex:);SEL swizzledSelector = @selector(safe_objectAtIndex:);Method originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));if (didAddMethod) {class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));} else {method_exchangeImplementations(originalMethod, swizzledMethod);}});
}- (id)safe_objectAtIndex:(NSUInteger)index {if (index < self.count) {return [self safe_objectAtIndex:index];} else {NSLog(@"Array index out of bounds: %lu", (unsigned long)index);return nil;}
}@end

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/5898.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux下启动jenkins报错问题解决

jenkins端口报错 java.io.IOException: Failed to start Jettyat winstone.Launcher.<init>(Launcher.java:209)at winstone.Launcher.main(Launcher.java:496)at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at java.base/jdk.int…

【题目】2023年全国职业院校技能大赛 GZ073 网络系统管理赛项赛题第3套A模块

2023年全国职业院校技能大赛 GZ073网络系统管理赛项 赛题第3套 信息安全管理与评估 网络系统管理 网络搭建与应用 云计算 软件测试 移动应用开发等多个赛项技术支持 任务书&#xff0c;赛题&#xff0c;解析等资料&#xff0c;知识点培训服务 添加博主wx&#xff1a;liuliu54…

全国产化BMC子卡详细介绍

一款基于全国产的BMC子卡&#xff0c;可实现ChMC/IPMC功能。子卡遵循IPMI 1.5/2.0协议规范&#xff0c;也支持客制OEM命令。子卡可获取载板板卡环境信息&#xff0c;板卡属性信息和板卡状态信息等&#xff0c;其中包括温度、电压、电流等信息&#xff1b;FRU、系统版本、CPU型号…

Django整合多种认证方式

承接上一篇&#xff1a;Django知识点总结-CSDN博客 目录 25.使用 Django REST framework实现用户认证和授权 26.通过djangorestframework-simplejwt使用JWT(JSON Web Token) 27.使用django-auth-ldap进行用户认证 28. 使用django-cas-ng实现集中认证及实现单点登录 29. …

实时监控视频拼接系统:功能和拼接参数介绍

目录 一、实时视频拼接系统介绍 &#xff08;一&#xff09;实时视频拼接的定义 &#xff08;二&#xff09;主要功能 1、视频拼接 2、拼接形式选择 3、前端选择 4、拼接展示 5、数据处理效率提升 6、任务管理 &#xff08;三&#xff09;实时拼接效果 二、拼接需要…

【JavaEE】Thread的方法和属性

文章目录 1、Thread的常见构造方法2、Thread的几个常见属性2.1 ID2.2 名称2.3 状态2.4 优先级2.5 是否后台线程2.6 是否存活2.7 是否被中断 3.补充说明3.1 Thread.sleep()的作用3.2 Thread.sleep()的异常处理方式 1、Thread的常见构造方法 方法说明Thread()创建线程对象Thread…

10G MAC层设计系列-(2)MAC RX模块

一、概述 MAC RX模块的需要进行解码、对齐、CRC校验。 因为在空闲的时候10G PCS/PMA会一直向外吐空闲符&#xff08;x07&#xff09;所以需要根据开始符、结束符将有效数据从码流中截取&#xff0c;也就是解码。 因为开始字符的所在位置有两种形式&#xff0c;而结束字符的位…

白盒测试与黑盒测试区别和联系

一、概念辨析 白盒测试 黑盒测试 二、其他测试方法 三、相关练习

算法训练营第十三天 | LeetCode 239 滑动窗口最大值、LeetCode 347 前K个高频元素

LeetCode 239 滑动窗口最大值 本体初始思路是这样的&#xff0c;首先看下给定数组长度和维持一个滑动窗口所需要花费的时间复杂度之间的关系。初步判断是还行的&#xff0c;当然后面被样例打脸了。需要更新成优先队列的解法。原本的解法能通过37/51和46/51的测试用例。但这还不…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-6.5--I.MX6U启动方式

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

VS Code工具将json数据格式化

诉求&#xff1a;json数据格式化应该在工作中用到的地方特别多&#xff0c;为了更方便、更仔细的对json数据查看&#xff0c;将json数据格式化是非常有必要的。 VS Code中如何将json数据快速格式化 1、在VS Code中安装Beautify JSON插件 2、安装完后在需要格式化的文件中按住…

Web APIs 学习归纳6--- BOM浏览器对象

前面几节主要针对DOM进行了学习&#xff0c;现在开始新的内容的学习---DOM浏览器对象。 DOM是更注重页面&#xff08;document&#xff09;内容的设计&#xff0c;但是BOM不仅限于页面&#xff08;document&#xff09;的设计&#xff0c;而是更加全面包括页面的刷新&#xff0…

【linux学习指南】linux指令与实践文件编写

文章目录 &#x1f4dd;前言&#x1f320; linux目录结构&#x1f309;linux命令介绍 &#x1f320;pwd命令&#x1f309;mkdir指令&#xff08;重要&#xff09; &#x1f320;cd 指令&#x1f309;touch指令 &#x1f320;rmdir指令 && rm 指令&#xff08;重要&…

抖音评论区精准获客自动化获客释放双手

挺好用的&#xff0c;评论区自动化快速获客&#xff0c;如果手动点引流涨&#xff0c;那就很耗费时间了&#xff0c;不是吗&#xff1f; 网盘自动获取 链接&#xff1a;https://pan.baidu.com/s/1lpzKPim76qettahxvxtjaQ?pwd0b8x 提取码&#xff1a;0b8x

Dashboard 安装部署

Dashboard 安装部署 Dashboard 安装部署 一&#xff1a;下载 二&#xff1a;部署步骤 1.镜像下载及导入 国内直接拉外网镜像会失败&#xff0c;可在境外下载镜像 查看 deployment 里的镜像版本 Dashboard Deploymentcontainers:- name: kubernetes-dashboardimage: k8s.g…

Unity Audio Filter 入门

概述&#xff1a; 如果你在你项目中需要一些特殊的声音效果&#xff0c;那这部分声音过滤器的部分一定不要错过喔&#xff0c;让我们来学习这部分的内容吧&#xff01; 这部分理论性比较强&#xff0c;认真看我的注解哈&#xff0c;我尽量解释的易懂一点。 Audio Chorus Filter…

Intelij Idea Push失败,出现git Authentication failed(验证失败)

目录 1、出现问题的原因 2、解决之法 1、出现问题的原因 能出现这种问题&#xff0c;最主要的原因是链接对上了&#xff0c;但用户验证失败了&#xff0c;即登录失败。 因为服务器转移或者换了git项目链接&#xff0c;导致你忘记了用户名密码&#xff0c;随意输入之后&…

持续更新|UNIAPP适配APP遇到的问题以及解决方案

在使用UNIAPP开发APP的时候遇到的一些奇奇怪怪问题记录 组件样式丢失 问题&#xff1a;组件引入界面中&#xff0c;在小程序和H5环境下样式正常&#xff0c;而在APP中却出现高度异常问题 解决&#xff1a;增加view标签将组件包裹起来即可正常显示 解决前&#xff1a; 解决后…

数据结构:实验七:数据查找

一、 实验目的 &#xff08;1&#xff09;领会各种查找算法的过程和算法设计。 &#xff08;2&#xff09;掌握查找算法解决实际问题。 二、 实验要求 &#xff08;1&#xff09;编写一个程序exp8-1.cpp, 按提示输入10个任意的整形数据&#xff08;无序&#xff09;&…

Mysql_数据库事务

文章目录 &#x1f60a; 作者&#xff1a;Lion J &#x1f496; 主页&#xff1a; https://blog.csdn.net/weixin_69252724 &#x1f389; 主题&#xff1a; MySQL__事务&#xff09; ⏱️ 创作时间&#xff1a;2024年04月26日 ———————————————— 这里写目…