【iOS】方法交换(Method Swizzling)

news2024/11/23 23:55:11

文章目录

  • 前言
  • 一、原理与注意
    • 用法
    • 注意要点
    • 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];
    // Logging
    NSLog(@"%@", NSStringFromClass([self class]));
}

// 方法交换模版
void swizzleMethod(Class class, SEL originalSelector, 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 one
    if (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 one
    if (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;

@end

NS_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 list
   if (!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.coloradmin.cn/o/1640348.html

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

相关文章

【打工日常】云原生之使用docker部署Web在线流程图软件

一、drawio介绍 1.drawio简介 draw.io是一款免费、开源、高质量的WEB在线流程图软件&#xff0c;无需注册登录&#xff0c;支持多种图表类型和元素&#xff0c;可在线编辑和导出。它是一个可配置的图表/白板可视化应用程序。该应用程序的设计主要是按原样使用。draw.io不适合作…

AI诗歌创作

诗歌作为一种文学形式&#xff0c;能够通过优美的语言和深刻的意境表达情感和思想&#xff0c;触动人心&#xff0c;引发共鸣。然而&#xff0c;如今随着生活节奏的加快和人们对实用性的追求&#xff0c;写诗这一传统艺术渐渐被人们所忽略。幸运的是&#xff0c;随着人工智能技…

【C++】双指针算法:四数之和

1.题目 2.算法思路 这道题目十分困难&#xff0c;在leetcode上的通过率只有36%&#xff0c;大家要做好心理准备。 在做个题目前强烈建议大家先看看我的上一篇博客&#xff1a;有效三角形个数&#xff0c;看完之后再去leetcode上写一写三数之和&#xff0c;搞懂那两个题目之后…

基于STM32的最小系统电路设计(STM32F103C8T6为例)

前言&#xff1a;本篇博客为嵌入式硬件领域的文章&#xff0c;对 STM32 的最小系统电路设计进行教学。本篇博客以嘉立创 EDA&#xff08;标准版&#xff09;进行绘制 STM32F103C8T6 的最小系统电路 PCB 板&#xff0c;STM32 的最小系统通常包括&#xff1a;微控制器、时钟电路、…

[Power Automate] 关联Teams,发起通知并进行审批

1、 workflow 创建一条teams chat,定义teams members 和 chat title 2、添加通知内容到刚刚创建的chat 里 3、发起teams 审批流程&#xff08;通过这个action&#xff0c;进行teams内部的审批&#xff09; 4、审批结果判断&#xff0c;在这个基础上进行审批之后的操作&#xff…

Mamba3D革新3D点云分析:超越Transformer,提升本地特征提取效率与性能!

DeepVisionary 每日深度学习前沿科技推送&顶会论文分享&#xff0c;与你一起了解前沿深度学习信息&#xff01; Mamba3D革新3D点云分析&#xff1a;超越Transformer&#xff0c;提升本地特征提取效率与性能&#xff01; 引言&#xff1a;3D点云分析的重要性与挑战 3D点云…

vscode设置免密登录远程服务器

文章目录 1. 问题描述2. 解决方案3. 原理 1. 问题描述 当我们使用vscode的ssh连接远程服务器后&#xff0c;过一段时间后&#xff0c;总是要求登录服务器的密码。 这就导致一个麻烦就是: 无论是在公司还是在学校&#xff0c;密码往往不是自己设置的&#xff0c;所以记忆起来就…

SQL事前巡检插件

背景: 事故频发 •在工作过程中每年都会看到SQL问题引发的线上问题&#xff0c;一条有问题的SQL足以拖垮整个数据库 不易发觉 •对于SQL性能问题测试在预发环境不易发现&#xff08;数据量小&#xff09; •SAAS系统隔离字段在SQL条件中遗漏&#xff0c;造成越权风险 •业…

navicat连接MySQL时1045报错

当登录MySQL数据库出现 Error 1045 错误时&#xff0c;表明你输入的用户名或密码错误被拒绝访问了&#xff1b;MySQL服务搭建后&#xff0c;默认root用户是不能进行远程访问连接的。 无法连接的可能有&#xff1a; 数据库没开启——开启数据库 用户名或密码错误——更改密码 连…

【探索】文字游侠AI新时代,每天5分钟自动化创作图文月入1万+,十分适合新手小白,附上渠道和教程(全面)

在这个信息爆炸的时代&#xff0c;内容创作者面临着空前的竞争。为了在今日头条这样的平台上脱颖而出并获取稳定收入&#xff0c;他们需要找到更高效、更创新的方法。而今&#xff0c;一款全新的AI工具正引领着一场革命&#xff0c;彻底改变了内容创作的生态。 自从GPT问世以来…

PZK via OWF

参考文献&#xff1a; [SMP88] Santis A, Micali S, Persiano G. Non-Interactive Zero-Knowledge with Preprocessing[C]//Advances in Cryptology—CRYPTO’88.[LS90] Lapidot D, Shamir A. Publicly verifiable non-interactive zero-knowledge proofs[C]//Advances in Cry…

C++ stack和queue的使用方法与模拟实现

文章目录 一、 stack的使用方法二、 queue的使用方法三、 容器适配器四、 stack的模拟实现五、 queue的模拟实现 一、 stack的使用方法 stack介绍文档 stack是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&#xff0c;其删除只能从容器的一端进行元素的…

8 聚类算法

目录 0 背景 1 Kmeans 1.1 聚类数量k的确定 2 DBSCAN 2.1 三个点 2.2 算法流程 3 层次聚类 3.1 过程 4 基于分布的聚类:高斯混合模型 0 背景 聚类算法是一种无监督学习技术&#xff0c;用于将数据集中的数据点划分为不同的组或簇&#xff0c;使得同一组内的数据点彼此相…

线性卷积和圆周卷积

文章目录 【 1. 线性卷积 】1.1 图解分析1.2 矩阵相乘实现线性卷积1.3 圆周卷积实现线性卷积1.4 实例:线性卷积的两种实现方法【 2. 圆周卷积 】2.1 图解分析2.2 矩阵相乘实现圆周卷积2.3 频域点乘实现圆周卷积2.4 实例:圆周卷积两种实现方法【 3. 线性卷积和圆周卷积的等价 …

算法导论 总结索引 | 第三部分 第十二章:二叉搜索树

1、搜索树数据结构 支持 许多动态集合操作&#xff0c;包括 SEARCH、MINIMUM、MAXIMUM、PREDECESSOR、SUCCESSOR、INSERT 和 DELETE 等。使用搜索树 既可以作为一个字典 又可以作为一个优先队列 2、二叉搜索树上的基本操作 所花费的时间 与这棵树的高度成正比。对于有n个结点的…

发卡盗u源码系统搭建ZHU

2024最新UI发卡盗U/支持多语言/更新UI界面/支持多个主流钱包去除后门板&#xff0c;最好是部署智能合约后用合约地址来授权包含转账支付页面盗U授权源码。 完美提U&#xff0c;教程包含如何提u 。功能完美。 1.Php静态 2.目录puicta 3.扩sal 4.ssl不强https

Messari 报告摘要 :Covalent Network(CQT)2024 年第一季度表现

摘要&#xff1a; 尽管 CQT 代币流通供应量增加了 20%&#xff08;新增 1.04 亿枚 CQT&#xff09;&#xff0c;但 CQT 的质押百分比仅从 2023 年第一季度的 22% 增长到了 2024 年第一季度的 29%。 CQT 的市值季度环比增长了 28%&#xff0c;多次达到 2.75 亿美元&#xff0c…

QT5之事件——包含提升控件

事件概述 信号就是事件的一种&#xff0c;事件由用户触发&#xff1b; 鼠标点击窗口&#xff0c;也可以检测到事件&#xff1b;产生事件后&#xff0c;传给事件处理&#xff0c;判断事件类型&#xff0c;后执行事件相应函数&#xff1b; 类似单片机的中断&#xff08;中断向量…

STL 标准模板库

以下是一些常用的STL容器&#xff1a; vector&#xff1a;动态数组&#xff0c;提供快速的随机访问。list&#xff1a;双向链表&#xff0c;支持快速插入和删除操作。set&#xff1a;有序集合&#xff0c;存储唯一的元素。map&#xff1a;有序映射&#xff0c;存储键值对。sta…

数据库(MySQL)—— DQL语句(基本查询和条件查询)

数据库&#xff08;MySQL&#xff09;—— DQL语句&#xff08;基本查询和条件查询&#xff09; 什么是DQL语句基本查询查询多个字段字段设置别名去除重复记录 条件查询语法条件 我们今天进入MySQL的DQL语句的学习&#xff1a; 什么是DQL语句 MySQL中的DQL&#xff08;Data Q…