文章目录
- 前言
- 一、原理与注意
- 用法
- 注意要点
- Method Swizzing涉及的相关API
- 二、应用场景与实践
- 1.统计VC加载次数并打印
- 2.防止UI控件短时间多次激活事件
- 3.防崩溃处理:数组越界问题
- 4.防KVO崩溃
- 总结
前言
上文讲到了iOS的消息发送机制,在消息机制中我们了解到了SEL、IMP等方法知识,由此延伸到iOS黑魔法方法交换,本篇着重讲解iOS的方法交换的应用场景与原理
一、原理与注意
我们在消息机制中说到了我们可以通过SEL
方法选择器查找Method
方法,从而得到对应的IMP
,方法交换的实质就是交换SEL
的IMP
从而改变方法的实现
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不一定指向aIMPmethod_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秒内不可连续点击
方案:
给按钮添加分类,并且添加一个需要间隔多少时间的属性,实行事件的时候判断间隔是否已经到了,如果不到就会拦截点击事件,就是不会触发点击事件
操作:
在自己写的交换方法中判断是否需要执行点击事件,这里记得仍然会调用原来的方法,只是增加了判断逻辑
实践:
由于UIButton
是UIControl
的子类,因而根据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,这就需要我们对数组进行兜底操作
思路:
对NSArray
的objectAtIndex
:方法进行Swizzling
,替换一个有处理逻辑的方法。但是,这时候还是有个问题,就是类簇的Swizzling没有那么简单。
类簇:
在iOS中NSNumber
、NSArray
、NSDictionary
等这些类都是类簇(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:)
参数校验:首先检查传入的 observer
和 keyPath
是否为空或无效。
线程安全:使用 @synchronized
块确保线程安全。
哈希表初始化:如果 KVOHashTable
不存在,则初始化一个新的 NSHashTable 以存储观察者哈希。
避免重复添加:计算当前观察者和 keyPath
的哈希值,并检查此哈希是否已存在于哈希表中。如果不存在,则添加到哈希表并执行原生的 KVO 添加观察者方法。
销毁时自动移除:注册回调以确保在观察者或被观察对象销毁时自动移除观察者。
- 移除观察者
(cyl_crashProtectremoveObserver:forKeyPath:context: 和 cyl_crashProtectremoveObserver:forKeyPath:)
参数校验:检查 observer
和 keyPath
的有效性。
线程安全:使用 @synchronized
块确保线程安全。
安全移除:如果哈希表存在并且包含相应的观察者哈希,则从哈希表中移除该哈希,并调用原生的 KVO 移除观察者方法。
总结
这篇文章主要总结了Method Swizzling
的各种应用场景,例如防止按钮被多次点击,进行hook操作以及数组与KVO的兜底操作,应用场景非常广泛,值得深入学习
参考博客:
iOS Crash防护系统-IronMan
iOS KVO 崩溃防护笔记