【iOS】方法交换(Method Swizzling)

文章目录

  • 前言
  • 一、原理与注意
    • 用法
    • 注意要点
    • Method Swizzing涉及的相关API
  • 二、应用场景与实践
    • 1.统计VC加载次数并打印
    • 2.防止UI控件短时间多次激活事件
    • 3.防崩溃处理:数组越界问题
    • 4.防KVO崩溃
  • 总结


前言

上文讲到了iOS的消息发送机制,在消息机制中我们了解到了SEL、IMP等方法知识,由此延伸到iOS黑魔法方法交换,本篇着重讲解iOS的方法交换的应用场景与原理

一、原理与注意

我们在消息机制中说到了我们可以通过SEL方法选择器查找Method方法,从而得到对应的IMP,方法交换的实质就是交换SELIMP从而改变方法的实现

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。

在这里插入图片描述

用法

先给要替换的方法的类添加一个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函数内代码只会执行一次的特性,防止方法的重复交换,使方法sel的指向又恢复成原来的imp的问题

Method Swizzing涉及的相关API

通过SEL获取方法Method

  • class_getInstanceMethod:获取实例方法
  • class_getClassMethod:获取类方法
  • method_getImplementation:获取一个方法的实现
  • method_setImplementation:设置一个方法的实现
  • method_getTypeEncoding:获取方法实现的编码类型
  • class_addMethod:添加方法实现
  • class_replaceMethod:用一个方法的实现,替换另一个方法的实现,即aIMP 指向 bIMP,但是bIMP不一定指向aIMP
  • method_exchangeImplementations:交换两个方法的实现,即 aIMP -> bIMP, bIMP -> aIMP

二、应用场景与实践

1.统计VC加载次数并打印

UIViewController+Logging.m

#import "UIViewController+Logging.h"
#import "objc/runtime.h"
@implementation UIViewController (Logging)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));});
//    [self swizzleMethod:[self class] andO:@selector(viewDidAppear:) andS:@selector(swizzled_viewDidAppear:)];
}- (void)swizzled_viewDidAppear:(BOOL)animated
{//此处为实现原来的方法
//    [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);// 如果添加成功则让原方法的imp指向新方法BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));// the method doesn’t exist and we just added oneif (didAddMethod) {// 然后让新方法的imp指向原方法class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));}else {method_exchangeImplementations(originalMethod, swizzledMethod);}}// 方法交换必须设计为类方法
//+(void)swizzleMethod:(Class)class andO:(SEL)originalSelector andS:(SEL)swizzledSelector
//{
//    // the method might not exist in the class, but in its superclass
//    Method originalMethod = class_getInstanceMethod(class, originalSelector);
//    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//    // 如果添加成功则让原方法的imp指向新方法BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));// the method doesn’t exist and we just added oneif (didAddMethod) {// 然后让新方法的imp指向原方法class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));}else {method_exchangeImplementations(originalMethod, swizzledMethod);}
//    method_exchangeImplementations(originalMethod, swizzledMethod);
//
//}
@end

我们这里即可以设置C类型的交换函数,也可以实现类方法实现的交换方法

同时+load这个类方法中只能调用类方法,不能调用实例方法,也就是说我们的swizzleMethod如果要在+load中调用不能是实例方法

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

需求:
我们不想让按钮短时间内被多次点击该如何做呢?
比如我们想让APP所有的按钮1秒内不可连续点击

方案:
给按钮添加分类,并且添加一个需要间隔多少时间的属性,实行事件的时候判断间隔是否已经到了,如果不到就会拦截点击事件,就是不会触发点击事件

操作:
在自己写的交换方法中判断是否需要执行点击事件,这里记得仍然会调用原来的方法,只是增加了判断逻辑

实践:
由于UIButtonUIControl的子类,因而根据UIControl新建一个分类即可

  • UIControl+Limit.h
#import <UIKit/UIKit.h>NS_ASSUME_NONNULL_BEGIN@interface UIControl (Limit)
@property (nonatomic, assign)BOOL UIControl_ignoreEvent;
@property (nonatomic, assign)NSTimeInterval UIControl_acceptEventInterval;@endNS_ASSUME_NONNULL_END
  • UIControl+Limit.m
#import "UIControl+Limit.h"
#import "objc/runtime.h"@implementation UIControl (Limit)- (void)setUIControl_acceptEventInterval:(NSTimeInterval)UIControl_acceptEventInterval {objc_setAssociatedObject(self, @selector(UIControl_acceptEventInterval), @(UIControl_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSTimeInterval)UIControl_acceptEventInterval {return [objc_getAssociatedObject(self, _cmd) doubleValue];
}-(void)setUIControl_ignoreEvent:(BOOL)UIControl_ignoreEvent{objc_setAssociatedObject(self, @selector(UIControl_ignoreEvent), @(UIControl_ignoreEvent), OBJC_ASSOCIATION_ASSIGN);
}-(BOOL)UIControl_ignoreEvent{return [objc_getAssociatedObject(self,_cmd) boolValue];
}+(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.UIControl_ignoreEvent){NSLog(@"btnAction is intercepted");return;}if(self.UIControl_acceptEventInterval>0){self.UIControl_ignoreEvent=YES;[self performSelector:@selector(setIgnoreEventWithNo)  withObject:nil afterDelay:self.UIControl_acceptEventInterval];}[self swizzled_sendAction:action to:target forEvent:event];
}-(void)setIgnoreEventWithNo{self.UIControl_ignoreEvent=NO;
}@end
  • ViewController.m
    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.UIControl_ignoreEvent=NO;btn.UIControl_acceptEventInterval = 3;[self.view addSubview:btn];[btn addTarget:self action:@selector(btnAction)forControlEvents:UIControlEventTouchUpInside];

在这里插入图片描述

3.防崩溃处理:数组越界问题

需求:
众所周知如果我们对NSArray进行操作,但是没有进行防越界处理,很有可能在读取数组的时候发生越界问题。
我们前面说到了App即使不能功能也不能crash,这就需要我们对数组进行兜底操作

思路
NSArrayobjectAtIndex:方法进行Swizzling,替换一个有处理逻辑的方法。但是,这时候还是有个问题,就是类簇的Swizzling没有那么简单。

类簇:
在iOS中NSNumberNSArrayNSDictionary等这些类都是类簇(Class Clusters),一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行Swizzling,必须获取到其真身进行Swizzling,直接对NSArray进行操作是无效的。这是因为Method Swizzling对NSArray这些的类簇是不起作用的

因此我们应该对其真身进行操作,而非NSArray自身

下面列举了NSArray和NSDictionary本类的类名,可以通过Runtime函数取出本类

类名真身
NSArray__NSArrayI
NSMutableArray__NSArrayM
NSDictionary__NSDictionaryI
NSMutableDictionary__NSDictionaryM

有时候会根据数组长短不同,NSArray的真身也会不同,例如如下数组的真身就不是NSArrayI
在这里插入图片描述
真身就是NSConstantArray

实践:
NSArray+crash.m

#import "NSArray+crash.h"
#import "objc/runtime.h"
@implementation NSArray (crash)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class cls = objc_getClass("NSConstantArray");if (cls) {Method fromMethod = class_getInstanceMethod(cls, @selector(objectAtIndex:));Method toMethod = class_getInstanceMethod(cls, @selector(cm_objectAtIndex:));if (fromMethod && toMethod) {method_exchangeImplementations(fromMethod, toMethod);} else {NSLog(@"Swizzle failed: methods not found.");}} else {NSLog(@"Swizzle failed: class not found.");}});
}- (id)cm_objectAtIndex:(NSUInteger)index {if (index >= self.count) {// 越界处理NSLog(@"Index %lu out of bounds, array count is %lu.", (unsigned long)index, (unsigned long)self.count);return nil;} else {// 正常访问,注意这里调用的是替换后的方法,因为实现已经交换return [self cm_objectAtIndex:index];}
}

ViewController.m

- (void)viewDidLoad {[super viewDidLoad];NSArray *array = @[@0, @1, @2, @3];NSLog(@"%@", [array objectAtIndex:3]);//本来要奔溃的NSLog(@"%@", [array objectAtIndex:4]);
}

在这里插入图片描述

4.防KVO崩溃

有许多的第三方库,比如 KVOController 用更优的API来规避这些crash,但是侵入性比较大,必须编码规范来约束所有人都要使用该方式。有没有什么更优雅,无感知的接入方式?

我们这里可以考虑建立一个哈希表,用来保存观察者、keyPath的信息,如果哈希表里已经有了相关的观察者,keyPath信息,那么继续添加观察者的话,就不载进行添加,同样移除观察的时候,也现在哈希表中进行查找,如果存在观察者,keypath信息,那么移除,如果没有的话就不执行相关的移除操作。

下面是核心的swizzle方法:

原函数swizzle后的函数
addObserver:forKeyPath:options:context:cyl_crashProtectaddObserver:forKeyPath:options:context:
removeObserver:forKeyPath:cyl_crashProtectremoveObserver:forKeyPath:
removeObserver:forKeyPath:context:cyl_crashProtectremoveObserver:forKeyPath:context:
- (void)cyl_crashProtectaddObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{if (!observer || !keyPath || keyPath.length == 0) {return;}@synchronized (self) {NSInteger kvoHash = [self _cyl_crashProtectHash:observer :keyPath];if (!self.KVOHashTable) {self.KVOHashTable = [NSHashTable hashTableWithOptions:NSPointerFunctionsStrongMemory];}if (![self.KVOHashTable containsObject:@(kvoHash)]) {[self.KVOHashTable addObject:@(kvoHash)];[self cyl_crashProtectaddObserver:observer forKeyPath:keyPath options:options context:context];[self cyl_willDeallocWithSelfCallback:^(__unsafe_unretained id observedOwner, NSUInteger identifier) {[observedOwner cyl_crashProtectremoveObserver:observer forKeyPath:keyPath context:context];}];__unsafe_unretained typeof(self) unsafeUnretainedSelf = self;[observer cyl_willDeallocWithSelfCallback:^(__unsafe_unretained id observerOwner, NSUInteger identifier) {[unsafeUnretainedSelf cyl_crashProtectremoveObserver:observerOwner forKeyPath:keyPath context:context];}];}}}- (void)cyl_crashProtectremoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {//TODO:  加上 context 限制,防止父类、子类使用同一个keyPath。[self cyl_crashProtectremoveObserver:observer forKeyPath:keyPath];}- (void)cyl_crashProtectremoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{//TODO:  white listif (!observer || !keyPath || keyPath.length == 0) {return;}@synchronized (self) {if (!observer) {return;}NSInteger kvoHash = [self _cyl_crashProtectHash:observer :keyPath];NSHashTable *hashTable = [self KVOHashTable];if (!hashTable) {return;}if ([hashTable containsObject:@(kvoHash)]) {[self cyl_crashProtectremoveObserver:observer forKeyPath:keyPath];[hashTable removeObject:@(kvoHash)];}}}
  • 添加观察者 (cyl_crashProtectaddObserver:forKeyPath:options:context:)

参数校验:首先检查传入的 observerkeyPath 是否为空或无效。
线程安全:使用 @synchronized 块确保线程安全。
哈希表初始化:如果 KVOHashTable 不存在,则初始化一个新的 NSHashTable 以存储观察者哈希。
避免重复添加:计算当前观察者和 keyPath 的哈希值,并检查此哈希是否已存在于哈希表中。如果不存在,则添加到哈希表并执行原生的 KVO 添加观察者方法。
销毁时自动移除:注册回调以确保在观察者或被观察对象销毁时自动移除观察者。

  • 移除观察者 (cyl_crashProtectremoveObserver:forKeyPath:context: 和 cyl_crashProtectremoveObserver:forKeyPath:)

参数校验:检查 observerkeyPath 的有效性。
线程安全:使用 @synchronized 块确保线程安全。
安全移除:如果哈希表存在并且包含相应的观察者哈希,则从哈希表中移除该哈希,并调用原生的 KVO 移除观察者方法。

总结

这篇文章主要总结了Method Swizzling的各种应用场景,例如防止按钮被多次点击,进行hook操作以及数组与KVO的兜底操作,应用场景非常广泛,值得深入学习

参考博客:
iOS Crash防护系统-IronMan
iOS KVO 崩溃防护笔记

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

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

相关文章

【3dmax笔记】023:阵列工具(移动+一维+二维+三维)

文章目录 一、阵列工具二、案例演示 一、阵列工具 【阵列】命令将显示【阵列】对话框&#xff0c;使用该对话框可以基于当前选择创建对象阵列。 菜单栏&#xff1a;【工具】菜单 > 【阵列】 二、案例演示 首先&#xff0c;画一个物体&#xff0c;如茶壶&#xff0c;如下图…

鸿蒙 DevEcoStudio:组件实例(页面及组件生命周期函数)

【使用onPageshow等生命周期函数】 在entry/src/main/ets/pages路径下创建Page1.ets: import router from ohos.router Entry Component struct Page1 {State message: string Hello WorldState show: booleantrueaboutToAppear(){console.log(Page1组件创建实例)}aboutToDisa…

中国教育界的泰斗级人物颜廷利:一位在多个领域具有深远影响的学者

**中国教育界的泰斗级人物颜廷利教授是一位在多个领域都有着深远影响的学者**。 山东籍文化名人颜廷利教授是一位世界级的哲学大师&#xff0c;他在学术界拥有多项创造性的成果。他不仅是国际十大姓名学专家排行榜上的佼佼者&#xff0c;颜廷利还被评为颜氏家族十大杰出名人教育…

在Windows 11环境下,生成自签名证书

在Windows 11环境下&#xff0c;使用上述命令生成自签名证书时&#xff0c;需要注意的是Windows命令行不直接支持<(command)这样的进程替换语法。因此&#xff0c;您需要稍微调整方法来实现相同的目标。下面是分步骤的操作指南&#xff1a; ### 1. 安装OpenSSL 确保您已经…

python分析预测退休后养老金金额

欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 目录 一.前言 二.代码 三.总结 一.前言 养老金,也称为退休金或退休费,是一种主要的社会养老保险待遇。它旨在保障职工退休后的基本生活需要,根据劳动者对社会所作的贡献和所具备的享受养老保险资格或退休条件,按月或一次…

【Word】写论文,参考文献涉及的上标、尾注、脚注 怎么用

一、功能位置 二、脚注和尾注区别 1.首先脚注是一个汉语词汇&#xff0c;论文脚注就是附在论文页面的最底端&#xff0c;对某些内容加以说明&#xff0c;印在书页下端的注文。脚注和尾注是对文本的补充说明。 2.其次脚注一般位于页面的底部&#xff0c;可以作为文档某处内容的…

从招标到合作:如何筛选与企业需求匹配的6sigma咨询公司

在市场竞争激烈的环境中&#xff0c;领军企业需要不断改进和创新才能在行业中保持竞争优势。为了解决产品质量、生产流程和客户满意度等方面的挑战&#xff0c;许多企业选择与6sigma咨询公司合作&#xff0c;推动企业的全面变革和持续发展。下面是企业在选择合作伙伴时通常会经…

GPT-3

论文&#xff1a;Language Models are Few-Shot Learners&#xff08;巨无霸OpenAI GPT3 2020&#xff09; 摘要 最近的工作表明&#xff0c;通过对大量文本进行预训练&#xff0c;然后对特定任务进行微调&#xff0c;在许多NLP任务和基准方面取得了实质性进展。虽然这种方法…

【LLama】Llama3 的本地部署与lora微调(基于xturn)

系列课程代码文档&#xff08;前2节课可跳过&#xff09;&#xff1a;https://github.com/SmartFlowAI/Llama3-Tutorial 课程视频&#xff1a;https://space.bilibili.com/3546636263360696/channel/series XTuner &#xff1a;https://github.com/InternLM/xtuner/blob/main/R…

如何完美解决Outlook大文件传送问题,提升办公协作效率?

在日常工作中&#xff0c;邮件是一种常用的通信方式&#xff0c;经常用来发送各类文件&#xff0c;比如报告和文档、合同和协议、财务报表、营销资料、设计文件等。但有时文件会比较大&#xff0c;因此Outlook大文件传送时&#xff0c;会遇到附件大小受限的情况。常用的解决发送…

Leetcode—724. 寻找数组的中心下标【简单】

2024每日刷题&#xff08;129&#xff09; Leetcode—724. 寻找数组的中心下标 实现代码 class Solution { public:int pivotIndex(vector<int>& nums) {int sum accumulate(nums.begin(), nums.end(), 0);int prefix 0;for(int i 0; i < nums.size(); i) {i…

ai写作工具推荐:如何用AI人工智能进行写作

AI写作工具&#xff1a;提升创作效率的秘密武器 在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;已经渗透到我们生活的方方面面&#xff0c;包括写作。AI写作工具&#xff0c;就是利用人工智能技术&#xff0c;帮助我们进行文本生成、语言优化等工作的工…

[综合应用]dns nfs httpd php mysql

第一步&#xff1a;搭建三台主机 主机名称 Ip地址 角色 503A 192.168.68.10 Mysql从 503B 192.168.68.11 Mysql从&#xff0c;nfs服务端&#xff0c;dns服务端 503Cmysql 192.168.68.12 MySQL主&#xff0c;web客户端 第二步&#xff1a;在503B上配置DNS 2.1 下载…

Hbase 常用shell操作

目录 1、创建表 1.1、启动HBase Shell 1.2、创建表 1.3、查看表 1.4、删除表 2、插入数据 2.1、put命令 3、查看数据 3.1、get命令 3.2、查询数据中文显示 4、更新数据 4.1、使用put来更新数据 5、删除数据 5.1、delete命令 5.2、删除指定列的数据 5.3、delete…

Django开发实战之登录用户鉴权登录界面实现

Django自带的鉴权系统非常的安全&#xff0c;大家可以放心使用&#xff0c;那么如何使用呢&#xff1f; 1、首先需要检查settings文件种的INSTALLED_APPS&#xff0c;有没有这两部分内容&#xff1a; 2、检查中间件&#xff0c;比如这两个中间件&#xff0c;一个是用于登录&a…

如何选择最佳的机器学习分类模型?基于使用贝叶斯和异步连续减半算法(ASHA)优化的最佳分类模型自动选择方法

目录 一、主要内容&#xff1a; 二、贝叶斯优化算法&#xff1a; 三、异步连续减半优化算法&#xff1a; 四、代码运行效果&#xff1a; 五、代码下载&#xff1a; 一、主要内容&#xff1a; 对于分类问题&#xff0c;不同机器学习模型分类的效果不同&#xff0c;而且在同…

新代数控Syntec网络IP配置设定教程

点击面板【维护】→【网络设定】→【IP地址取得方法&#xff1a;直接指定IP地址】→【IP地址&#xff1a;输入采集需要设定的IP】→【子网掩码&#xff1a;255.255.255.0】→【预设网关】 输入方法&#xff1a;点击面板上的【ENTER】输入键&#xff0c;输入相关参数即可。

LeetCode-1463. 摘樱桃 II【数组 动态规划 矩阵】

LeetCode-1463. 摘樱桃 II【数组 动态规划 矩阵】 题目描述&#xff1a;解题思路一&#xff1a;动态规划一般有自顶向下和自底向上两种编写方式&#xff0c;其中自顶向下也被称为「记忆化搜索」。解题思路二&#xff1a;0解题思路三&#xff1a;0 题目描述&#xff1a; 给你一…

Codeforces Round 456 (Div. 2) - B. New Year‘s Eve (位运算,Bitsmasks)

由于格里莎去年表现良好&#xff0c;新年前夕&#xff0c;戴德-莫罗兹带着一大包礼物来看望他&#xff01;袋子里装着 n 颗来自老式面包店的糖果&#xff0c;每颗糖果都按照口味从 1 到 n 贴上标签。没有两颗糖果的口味是相同的。 糖果的选择直接影响到格里莎的幸福感。我们可…

[CR]厚云填补_M3R-CR Dataset and Align-CR

Multimodal and Multiresolution Data Fusion for High-Resolution Cloud Removal: A Novel Baseline and Benchmark Abstract 去云(Cloud Removal)是遥感领域的一个重要且具有挑战性的问题&#xff0c;近年来在这一领域取得了显著进展。两个主要问题仍然阻碍着CR的发展&#…