前言:什么是Method Swizzling,在iOS开发中它有什么做用?安全
简单来讲咱们主要是使用Method Swizzling来把系统的方法交换为咱们本身的方法,从而给系统方法添加一些咱们想要的功能。该篇文章主要列举Method Swizzling在开发中的一些现实用例,同时文中也有补充读者的一些疑点。但愿阅读文章的朋友们也能够提供一些文中还没有举出的例子,本文持续更新中。性能优化
目前已更新实例汇总:网络
Method Swizzling通用方法封装多线程
在列举以前,咱们能够将Method Swizzling功能封装为类方法,做为NSObject的类别,这样咱们后续调用也会方便些。app
#import <Foundation/Foundation.h> #import <objc/runtime.h> @interface NSObject (Swizzling) + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector; @end
#import "NSObject+Swizzling.h" @implementation NSObject (Swizzling) + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{ Class class = [self class]; //原有方法 Method originalMethod = class_getInstanceMethod(class, originalSelector); //替换原有方法的新方法 Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); //先尝试給源SEL添加IMP,这里是为了不源SEL没有实现IMP的状况 BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) {//添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else {//添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换便可 method_exchangeImplementations(originalMethod, swizzledMethod); } } @end
⚠️补充知识点jsp
SEL、Method、IMP的含义及区别ide
在运行时,类(Class)维护了一个消息分发列表来解决消息的正确发送。每个消息列表的入口是一个方法(Method),这个方法映射了一对键值对,其中键是这个方法的名字(SEL),值是指向这个方法实现的函数指针 implementation(IMP)。
伪代码表示:函数
Class { MethodList ( Method{ SEL:IMP; } Method{ SEL:IMP; } ); };
Method Swizzling就是改变类的消息分发列表来让消息解析时从一个选择器(SEL)对应到另一个的实现(IMP),同时将原始的方法实现混淆到一个新的选择器(SEL)。性能
为何要添加didAddMethod判断?字体
先尝试添加原SEL实际上是为了作一层保护,由于若是这个类没有实现originalSelector,但其父类实现了,那class_getInstanceMethod会返回父类的方法。这样method_exchangeImplementations替换的是父类的那个方法,这固然不是咱们想要的。因此咱们先尝试添加 orginalSelector,若是已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。
若是理解还不够透彻,咱们能够进入runtime.h中查看class_addMethod源码解释:
/** * Adds a new method to a class with a given name and implementation. * * @param cls The class to which to add a method. * @param name A selector that specifies the name of the method being added. * @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd. * @param types An array of characters that describe the types of the arguments to the method. * * @return YES if the method was added successfully, otherwise NO * (for example, the class already contains a method implementation with that name). * * @note class_addMethod will add an override of a superclass's implementation, * but will not replace an existing implementation in this class. * To change an existing implementation, use method_setImplementation. */
大概的意思就是咱们能够经过class_addMethod为一个类添加方法(包括方法名称(SEL)和方法的实现(IMP)),返回值为BOOL类型,表示方法是否成功添加。须要注意的地方是class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。也就是说若是class_addMethod返回YES,说明子类中没有方法originalSelector,经过class_addMethod为其添加了方法originalSelector,并使其实现(IMP)为咱们想要替换的实现。
class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
同时再将原有的实现(IMP)替换到swizzledMethod方法上,
class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
从而实现了方法的交换,而且未影响父类方法的实现。反之若是class_addMethod返回NO,说明子类中自己就具备方法originalSelector的实现,直接调用交换便可。
method_exchangeImplementations(originalMethod, swizzledMethod);
这一部份内容比较绕口,但愿你们能够耐下心来仔细反复阅读。
-------------------------------实例列举-------------------------------
实例一:替换ViewController生命周期方法
App跳转到某具备网络请求的界面时,为了用户体验效果常会添加加载栏或进度条来显示当前请求状况或进度。这种界面都会存在这样一个问题,在请求较慢时,用户手动退出界面,这时候须要去除加载栏。
固然能够依次在每一个界面的viewWillDisappear方法中添加去除方法,但若是相似的界面过多,一味的复制粘贴也不是方法。这时候就能体现Method Swizzling的做用了,咱们能够替换系统的viewWillDisappear方法,使得每当执行该方法时即自动去除加载栏。
#import "UIViewController+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UIViewController (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(viewWillDisappear:) bySwizzledSelector:@selector(sure_viewWillDisappear:)]; }); } - (void)sure_viewWillDisappear:(BOOL)animated { [self sure_viewWillDisappear:animated]; [SVProgressHUD dismiss]; }
代码如上,这样就不用考虑界面是否移除加载栏的问题了。补充一点,一般咱们也会在生命周期方法中设置默认界面背景颜色,因若背景颜色默认为透明对App的性能也有必定影响,这你们能够在UIKit性能优化那篇文章中查阅。但相似该类操做也能够书写在通用类中,因此具体使用还要靠本身定夺。
⚠️补充知识点
为何方法交换调用在+load方法中?
在Objective-C runtime会自动调用两个类方法,分别为+load与+ initialize。+load 方法是在类被加载的时候调用的,也就是必定会被调用。而+initialize方法是在类或它的子类收到第一条消息以前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说+initialize方法是以懒加载的方式被调用的,若是程序一直没有给某个类或它的子类发送消息,那么这个类的+initialize方法是永远不会被调用的。此外+load方法还有一个很是重要的特性,那就是子类、父类和分类中的+load方法的实现是被区别对待的。换句话说在 Objective-C runtime自动调用+load方法时,分类中的+load方法并不会对主类中的+load方法形成覆盖。综上所述,+load 方法是实现 Method Swizzling 逻辑的最佳“场所”。如需更深刻理解,可参考Objective-C 深刻理解 +load 和 +initialize。为何方法交换要在dispatch_once中执行?
方法交换应该要线程安全,并且保证在任何状况下(多线程环境,或者被其余人手动再次调用+load方法)只交换一次,防止再次调用又将方法交换回来。除非只是临时交换使用,在使用完成后又交换回来。 最经常使用的解决方案是在+load方法中使用dispatch_once来保证交换是安全的。以前有读者反馈+load方法自己即为线程安全,为何仍需添加dispatch_once,其缘由就在于+load方法自己没法保证其中代码只被执行一次。为何没有发生死循环?
必定有不少读者有疑惑,为何sure_viewWillDisappear方法中的代码没有发生递归死循环。其缘由很简单,由于方法已经执行过交换,调用[self sure_viewWillDisappear:animated]本质是在调用原有方法viewWillDisappear,反而若是咱们在方法中调用[self viewWillDisappear:animated]才真的会发生死循环。是否是很绕?仔细看看。实例二:解决获取索引、添加、删除元素越界崩溃问题
对于NSArray、NSDictionary、NSMutableArray、NSMutableDictionary难免会进行索引访问、添加、删除元素的操做,越界问题也是很常见,这时咱们能够经过Method Swizzling解决这些问题,越界给予提示防止崩溃。
这里以NSMutableArray为例说明
#import "NSMutableArray+Swizzling.h" #import "NSObject+Swizzling.h" @implementation NSMutableArray (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObject:) bySwizzledSelector:@selector(safeRemoveObject:) ]; [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(addObject:) bySwizzledSelector:@selector(safeAddObject:)]; [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObjectAtIndex:) bySwizzledSelector:@selector(safeRemoveObjectAtIndex:)]; [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(insertObject:atIndex:) bySwizzledSelector:@selector(safeInsertObject:atIndex:)]; [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(objectAtIndex:) bySwizzledSelector:@selector(safeObjectAtIndex:)]; }); } - (void)safeAddObject:(id)obj { if (obj == nil) { NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__); } else { [self safeAddObject:obj]; } } - (void)safeRemoveObject:(id)obj { if (obj == nil) { NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__); return; } [self safeRemoveObject:obj]; } - (void)safeInsertObject:(id)anObject atIndex:(NSUInteger)index { if (anObject == nil) { NSLog(@"%s can't insert nil into NSMutableArray", __FUNCTION__); } else if (index > self.count) { NSLog(@"%s index is invalid", __FUNCTION__); } else { [self safeInsertObject:anObject atIndex:index]; } } - (id)safeObjectAtIndex:(NSUInteger)index { if (self.count == 0) { NSLog(@"%s can't get any object from an empty array", __FUNCTION__); return nil; } if (index > self.count) { NSLog(@"%s index out of bounds in array", __FUNCTION__); return nil; } return [self safeObjectAtIndex:index]; } - (void)safeRemoveObjectAtIndex:(NSUInteger)index { if (self.count <= 0) { NSLog(@"%s can't get any object from an empty array", __FUNCTION__); return; } if (index >= self.count) { NSLog(@"%s index out of bound", __FUNCTION__); return; } [self safeRemoveObjectAtIndex:index]; } @end
对应你们能够触类旁通,相应的实现添加、删除等,以及NSArray、NSDictionary等操做,因代码篇幅较大,这里就不一一书写了。
这里没有使用self来调用,而是使用objc_getClass("__NSArrayM")来调用的。由于NSMutableArray的真实类只能经过后者来获取,而不能经过[self class]来获取,而method swizzling只对真实的类起做用。这里就涉及到一个小知识点:类簇。补充以上对象对应类簇表。
类簇表.png
实例三:防止按钮重复暴力点击
程序中大量按钮没有作连续响应的校验,连续点击出现了不少没必要要的问题,例如发表帖子操做,用户手快点击屡次,就会致使同一帖子发布屡次。
#import <UIKit/UIKit.h> //默认时间间隔 #define defaultInterval 1 @interface UIButton (Swizzling) //点击间隔 @property (nonatomic, assign) NSTimeInterval timeInterval; //用于设置单个按钮不须要被hook @property (nonatomic, assign) BOOL isIgnore; @end
#import "UIButton+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UIButton (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(sendAction:to:forEvent:) bySwizzledSelector:@selector(sure_SendAction:to:forEvent:)]; }); } - (NSTimeInterval)timeInterval{ return [objc_getAssociatedObject(self, _cmd) doubleValue]; } - (void)setTimeInterval:(NSTimeInterval)timeInterval{ objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } //当按钮点击事件sendAction 时将会执行sure_SendAction - (void)sure_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{ if (self.isIgnore) { //不须要被hook [self sure_SendAction:action to:target forEvent:event]; return; } if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) { self.timeInterval =self.timeInterval == 0 ?defaultInterval:self.timeInterval; if (self.isIgnoreEvent){ return; }else if (self.timeInterval > 0){ [self performSelector:@selector(resetState) withObject:nil afterDelay:self.timeInterval]; } } //此处 methodA和methodB方法IMP互换了,实际上执行 sendAction;因此不会死循环 self.isIgnoreEvent = YES; [self sure_SendAction:action to:target forEvent:event]; } //runtime 动态绑定 属性 - (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{ // 注意BOOL类型 须要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,不然set方法会赋值出错 objc_setAssociatedObject(self, @selector(isIgnoreEvent), @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isIgnoreEvent{ //_cmd == @select(isIgnore); 和set方法里一致 return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setIsIgnore:(BOOL)isIgnore{ // 注意BOOL类型 须要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,不然set方法会赋值出错 objc_setAssociatedObject(self, @selector(isIgnore), @(isIgnore), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isIgnore{ //_cmd == @select(isIgnore); 和set方法里一致 return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)resetState{ [self setIsIgnoreEvent:NO]; } @end
实例四:全局更换控件初始效果
以UILabel为例,在项目比较成熟的基础上,应用中须要引入新的字体,须要更换全部Label的默认字体,可是同时,对于一些特殊设置了字体的label又不须要更换。乍看起来,这个问题确实十分棘手,首先项目比较大,一个一个设置全部使用到的label的font工做量是巨大的,而且在许多动态展现的界面中,可能会漏掉一些label,产生bug。其次,项目中的label来源并不惟一,有用代码建立的,有xib和storyBoard中的,这也将浪费很大的精力。这时Method Swizzling能够解决此问题,避免繁琐的操做。
#import "UILabel+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UILabel (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(init) bySwizzledSelector:@selector(sure_Init)]; [self methodSwizzlingWithOriginalSelector:@selector(initWithFrame:) bySwizzledSelector:@selector(sure_InitWithFrame:)]; [self methodSwizzlingWithOriginalSelector:@selector(awakeFromNib) bySwizzledSelector:@selector(sure_AwakeFromNib)]; }); } - (instancetype)sure_Init{ id __self = [self sure_Init]; UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize]; if (font) { self.font=font; } return __self; } - (instancetype)sure_InitWithFrame:(CGRect)rect{ id __self = [self sure_InitWithFrame:rect]; UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize]; if (font) { self.font=font; } return __self; } - (void)sure_AwakeFromNib{ [self sure_AwakeFromNib]; UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize]; if (font) { self.font=font; } } @end
这一实例我的认为使用率可能不高,对于产品的设计这些点都是已经肯定好的,更改的概率很低。何况咱们也可使用appearance来进行统一设置。
实例五:App热修复
由于AppStore上线审核时间较长,且若是在线上版本出现bug修复起来也是很困难,这时App热修复就能够解决此问题。热修复即在不更改线上版本的前提下,对线上版本进行更新甚至添加模块。国内比较好的热修复技术:JSPatch。JSPatch能作到经过JS调用和改写OC方法最根本的缘由是Objective-C是动态语言,OC上全部方法的调用/类的生成都经过Objective-C Runtime在运行时进行,咱们能够经过类名/方法名反射获得相应的类和方法,进而替换出现bug的方法或者添加方法等。bang的博客上有详细的描述有兴趣能够参考,这里就不赘述了。
实例六:App异常加载占位图通用类封装(更新于:2016/12/01)
详情可见文章:《零行代码为App添加异常加载占位图》
在该功能模块中,使用Runtime Method Swizzling进行替换tableView、collectionView的reloadData方法,使得每当执行刷新操做时,自动检测当前组数与行数,从而实现零代码判断占位图是否显示的功能,一样也适用于网络异常等状况,详细设置可前往阅读。
实例七:全局修改导航栏后退(返回)按钮(更新于:2016/12/05)
在真实项目开发中,会全局统一某控件样式,以导航栏后退(返回)按钮为例,一般项目中会固定为返回字样,或者以图片进行显示等。
iOS默认的返回按钮样式以下,默认为蓝色左箭头,文字为上一界面标题文字。
默认返回按钮样式
这里咱们仍能够经过Runtime Method Swizzling来实现该需求,在使用Method Swizzling进行更改以前,必须考虑注意事项,即尽量的不影响原有操做,好比对于系统默认的返回按钮,与其对应的是有界面边缘右滑返回功能的,所以咱们进行统一更改后不可以使其功能废弃。
闲话少说,咱们建立基于UINavigationItem
的类别,在其load
方法中替换方法backBarButtonItem
代码以下
#import "UINavigationItem+Swizzling.h" #import "NSObject+Swizzling.h" static char *kCustomBackButtonKey; @implementation UINavigationItem (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(backBarButtonItem) bySwizzledSelector:@selector(sure_backBarButtonItem)]; }); } - (UIBarButtonItem*)sure_backBarButtonItem { UIBarButtonItem *backItem = [self sure_backBarButtonItem]; if (backItem) { return backItem; } backItem = objc_getAssociatedObject(self, &kCustomBackButtonKey); if (!backItem) { backItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:NULL]; objc_setAssociatedObject(self, &kCustomBackButtonKey, backItem, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return backItem; } @end
这里进行将返回按钮的文字清空操做,其余需求样式你们也可随意替换,如今再次运行程序,就会发现全部的返回按钮均只剩左箭头,并右滑手势依然有效。如图所示
全局统一设置返回按钮
文/卖报的小画家Sure(简书做者) 原文连接:http://www.jianshu.com/p/f6dad8e1b848