Runtime Method Swizzling开发实例汇总
wxwj7573
8年前
<p>什么是Method Swizzling,在iOS开发中它有什么作用?</p> <p>简单来说我们主要是使用Method Swizzling来把系统的方法交换为我们自己的方法,从而给系统方法添加一些我们想要的功能。已经有很多文章从各个角度解释Method Swizzling的涵义甚至实现机制,该篇文章主要列举Method Swizzling在开发中的一些现实用例。 希望阅读文章的朋友们也可以提供一些文中尚未举出的例子。</p> <p>在列举之前,我们可以将Method Swizzling功能封装为类方法,作为NSObject的类别,这样我们后续调用也会方便些。</p> <pre> <code class="language-objectivec">#import #import @interface NSObject (Swizzling) + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelectorbySwizzledSelector:(SEL)swizzledSelector; @end </code></pre> <pre> <code class="language-objectivec">#import "NSObject+Swizzling.h" @implementationNSObject (Swizzling) + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelectorbySwizzledSelector:(SEL)swizzledSelector{ Class class = [self class]; //原有方法 MethodoriginalMethod = class_getInstanceMethod(class, originalSelector); //替换原有方法的新方法 MethodswizzledMethod = 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 </code></pre> <h3><strong>实例一:替换ViewController生命周期方法</strong></h3> <p>App跳转到某具有网络请求的界面时,为了用户体验效果常会添加加载栏或进度条来显示当前请求情况或进度。这种界面都会存在这样一个问题,在请求较慢时,用户手动退出界面,这时候需要去除加载栏。</p> <p>当然可以依次在每个界面的viewWillDisappear方法中添加去除方法,但如果类似的界面过多,一味的复制粘贴也不是方法。这时候就能体现Method Swizzling的作用了,我们可以替换系统的viewWillDisappear方法,使得每当执行该方法时即自动去除加载栏。</p> <pre> <code class="language-objectivec">#import "UIViewController+Swizzling.h" #import "NSObject+Swizzling.h" @implementationUIViewController (Swizzling) + (void)load { static dispatch_once_tonceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(viewWillDisappear:) bySwizzledSelector:@selector(sure_viewWillDisappear:)]; }); } - (void)sure_viewWillDisappear:(BOOL)animated { [self sure_viewWillDisappear:animated]; [SVProgressHUDdismiss]; } </code></pre> <p>代码如上,这样就不用考虑界面是否移除加载栏的问题了。</p> <h3><strong>实例二:解决获取索引、添加、删除元素越界崩溃问题</strong></h3> <p>对于NSArray、NSDictionary、NSMutableArray、NSMutableDictionary不免会进行索引访问、添加、删除元素的操作,越界问题也是很常见,这时我们可以通过Method Swizzling解决这些问题,越界给予提示防止崩溃。</p> <p>这里以NSMutableArray为例说明</p> <pre> <code class="language-objectivec">#import "NSMutableArray+Swizzling.h" #import "NSObject+Swizzling.h" @implementationNSMutableArray (Swizzling) + (void)load { static dispatch_once_tonceToken; 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)anObjectatIndex:(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:anObjectatIndex: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 = self.count) { NSLog(@"%s index out of bound", __FUNCTION__); return; } [self safeRemoveObjectAtIndex:index]; } @end </code></pre> <p>对应大家可以举一反三,相应的实现添加、删除等,以及NSArray、NSDictionary等操作,因代码篇幅较大,这里就不一一书写了。</p> <p>这里没有使用self来调用,而是使用objc_getClass(“__NSArrayM”)来调用的。因为NSMutableArray的真实类只能通过后者来获取,而不能通过[self class]来获取,而method swizzling只对真实的类起作用。这里就涉及到一个小知识点:类簇。补充以上对象对应类簇表。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/26692326fc42034e894e900b4d1752a4.png"></p> <h3><strong>实例三:防止按钮重复暴力点击</strong></h3> <p>程序中大量按钮没有做连续响应的校验,连续点击出现了很多不必要的问题,例如发表帖子操作,用户手快点击多次,就会导致同一帖子发布多次。</p> <pre> <code class="language-objectivec">#import //默认时间间隔 #define defaultInterval 1 @interface UIButton (Swizzling) //点击间隔 @property (nonatomic, assign) NSTimeIntervaltimeInterval; //用于设置单个按钮不需要被hook @property (nonatomic, assign) BOOL isIgnore; @end </code></pre> <pre> <code class="language-objectivec">#import "UIButton+Swizzling.h" #import "NSObject+Swizzling.h" @implementationUIButton (Swizzling) + (void)load { static dispatch_once_tonceToken; 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)actionto:(id)targetforEvent:(UIEvent *)event{ if (self.isIgnore) { //不需要被hook [self sure_SendAction:actionto:targetforEvent: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:nilafterDelay:self.timeInterval]; } } //此处 methodA和methodB方法IMP互换了,实际上执行 sendAction;所以不会死循环 self.isIgnoreEvent = YES; [self sure_SendAction:actionto:targetforEvent: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 </code></pre> <p>实例四:全局更换控件初始效果</p> <p>以UILabel为例,在项目比较成熟的基础上,应用中需要引入新的字体,需要更换所有Label的默认字体,但是同时,对于一些特殊设置了字体的label又不需要更换。乍看起来,这个问题确实十分棘手,首先项目比较大,一个一个设置所有使用到的label的font工作量是巨大的,并且在许多动态展示的界面中,可能会漏掉一些label,产生bug。其次,项目中的label来源并不唯一,有用代码创建的,有xib和storyBoard中的,这也将浪费很大的精力。这时Method Swizzling可以解决此问题,避免繁琐的操作。</p> <pre> <code class="language-objectivec">#import "UILabel+Swizzling.h" #import "NSObject+Swizzling.h" @implementationUILabel (Swizzling) + (void)load { static dispatch_once_tonceToken; 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 = [UIFontfontWithName:@"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 = [UIFontfontWithName:@"Zapfino" size:self.font.pointSize]; if (font) { self.font=font; } return __self; } -(void)sure_AwakeFromNib{ [self sure_AwakeFromNib]; UIFont * font = [UIFontfontWithName:@"Zapfino" size:self.font.pointSize]; if (font) { self.font=font; } } @end </code></pre> <p>这一实例个人认为使用率可能不高,对于产品的设计这些点都是已经确定好的,更改的几率很低。况且我们也可以使用appearance来进行统一设置。</p> <h3><strong>实例五:App热修复</strong></h3> <p>因为AppStore上线审核时间较长,且如果在线上版本出现bug修复起来也是很困难,这时App热修复就可以解决此问题。热修复即在不更改线上版本的前提下,对线上版本进行更新甚至添加模块。国内比较好的热修复技术:JSPatch。JSPatch能做到通过JS调用和改写OC方法最根本的原因是Objective-C是动态语言,OC上所有方法的调用/类的生成都通过Objective-C Runtime在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法,进而替换出现bug的方法或者添加方法等。bang的博客上有详细的描述有兴趣可以参考,这里就不赘述了。</p> <p>暂时写到这里,部分内容来源于网络,后续还会更新。</p> <p>最后,还是希望看过的朋友们可以提供一些自己开发中的实例加以补充。</p> <p> </p> <p>来自:http://ios.jobbole.com/90809/</p> <p> </p>