本文Demo传送门:MethodSwizzlingDemogit
摘要:编程,只了解原理不行,必须实战才能知道应用场景。本系列尝试阐述runtime相关理论的同时介绍一些实战场景,而本文则是本系列的方法交换篇。本文中,第一节将介绍方法交换及注意点,第二节将总结一下方法交换相关的API,第三节将介绍方法交换几种的实战场景:统计VC加载次数并打印,防止UI控件短期屡次激活事件,防奔溃处理(数组越界问题)。github
Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,咱们能够将Method Swizzling代码写到任何地方,可是只有在这段Method Swilzzling代码执行完毕以后互换才起做用。编程
先给要替换的方法的类添加一个Category,而后在Category中的+(void)load
方法中添加Method Swizzling方法,咱们用来替换的方法也写在这个Category中。设计模式
因为load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,而且不须要咱们手动调用。数组
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
复制代码
method_getImplementation(Method _Nonnull m)
复制代码
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable 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 "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];
// 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);
// class_addMethod will fail if original method already exists
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// the method doesn’t exist and we just added one
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
复制代码
需求bash
当前项目写好的按钮,尚未全局地控制他们短期内不可连续点击(也许有过零星地在某些网络请求接口以前作过一些控制)。如今来了新需求:本APP全部的按钮1秒内不可连续点击。你怎么作?一个个改?这种低效率低维护度确定是不妥的。服务器
方案网络
给按钮添加分类,并添加一个点击事件间隔的属性,执行点击事件的时候判断一下是否时间到了,若是时间不到,那么拦截点击事件。ide
怎么拦截点击事件呢?其实点击事件在runtime里面是发送消息,咱们能够把要发送的消息的SEL 和本身写的SEL交换一下,而后在本身写的SEL里面判断是否执行点击事件。函数
实践
UIButton是UIControl的子类,于是根据UIControl新建一个分类便可
#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
复制代码
-(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");
}
复制代码
需求
在实际工程中,可能在一些地方(好比取出网络响应数据)进行了数组NSArray取数据的操做,并且之前的小哥们也没有进行防越界处理。测试方一不当心也没有测出数组越界状况下奔溃(由于返回的数据是动态的),结果觉得没有问题了,其实还隐藏的生产事故的风险。
这时APP负责人说了,即便APP即便不能工做也不能Crash,这是最低的底线。那么这对数组越界的状况下的奔溃,你有没有办法拦截?
思路:对NSArray的objectAtIndex:
方法进行Swizzling,替换一个有处理逻辑的方法。可是,这时候仍是有个问题,就是类簇的Swizzling没有那么简单。
类簇
在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇(Class Clusters),一个NSArray的实现可能由多个类组成。因此若是想对NSArray进行Swizzling,必须获取到其**“真身”**进行Swizzling,直接对NSArray进行操做是无效的。这是由于Method Swizzling对NSArray这些的类簇是不起做用的。
由于这些类簇类,实际上是一种抽象工厂的设计模式。抽象工厂内部有不少其它继承自当前类的子类,抽象工厂类会根据不一样状况,建立不一样的抽象对象来进行使用。例如咱们调用NSArray的objectAtIndex:
方法,这个类会在方法内部判断,内部建立不一样抽象类进行操做。
因此若是咱们对NSArray类进行Swizzling操做其实只是对父类进行了操做,在NSArray内部会建立其余子类来执行操做,真正执行Swizzling操做的并非NSArray自身,因此咱们应该对其“真身”进行操做。
下面列举了NSArray和NSDictionary本类的类名,能够经过Runtime函数取出本类:
类名 | 真身 |
---|---|
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
实践
好啦,新建一个分类,直接用代码实现,看看怎么取出真身的:
@implementation NSArray (CrashHandle)
// Swizzling核心代码
// 须要注意的是,好多同窗反馈下面代码不起做用,形成这个问题的缘由大多都是其调用了super load方法。在下面的load方法中,不该该调用父类的load方法。
+ (void)load {
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cm_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
// 为了不和系统的方法冲突,我通常都会在swizzling方法前面加前缀
- (id)cm_objectAtIndex:(NSUInteger)index {
// 判断下标是否越界,若是越界就进入异常拦截
if (self.count-1 < index) {
@try {
return [self cm_objectAtIndex:index];
}
@catch (NSException *exception) {
// 在崩溃后会打印崩溃信息。若是是线上,能够在这里将崩溃信息发送到服务器
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
}
@finally {}
} // 若是没有问题,则正常进行方法调用
else {
return [self cm_objectAtIndex:index];
}
}
复制代码
这里面可能有个误会,- (id)cm_objectAtIndex:(NSUInteger)index {
里面调用了自身?这是递归吗?其实不是。这个时候方法替换已经有效了,cm_objectAtIndex
这个SEL指向的实际上是原来系统的objectAtIndex:
的IMP。于是不是递归。
- (void)viewDidLoad {
[super viewDidLoad];
// 测试代码
NSArray *array = @[@0, @1, @2, @3];
[array objectAtIndex:3];
//原本要奔溃的
[array objectAtIndex:4];
}
复制代码
运行以后,发现没有崩溃,并打印了相关信息,以下所示。