<简书 — 刘小壮> http://www.jianshu.com/p/ff19c04b34d0git
公司年末要在新年前发一个版本,最近一直很忙,很久没有更新博客了。正好如今新版本开发的差很少了,抽空总结一下。github
因为最近开发新版本,就避免不了在开发和调试过程当中引发崩溃,以及诱发一些以前的__bug__致使的崩溃。并且项目比较大也很很差排查,正好想起以前研究过的
Method Swizzling
,考虑是否能用这个苹果的“黑魔法”解决问题,固然用好这个黑魔法并不局限于解决这些问题....编程
就拿咱们公司项目来讲吧,咱们公司是作导航的,并且项目规模比较大,各个控制器功能都已经实现。忽然有一天老大过来,说咱们要在全部页面添加统计功能,也就是用户进入这个页面就统计一次。咱们会想到下面的一些方法:设计模式
直接简单粗暴的在每一个控制器中加入统计,复制、粘贴、复制、粘贴... 上面这种方法太Low了,消耗时间并且之后很是难以维护,会让后面的开发人员骂死的。数组
咱们可使用OOP
的特性之一,继承的方式来解决这个问题。建立一个基类,在这个基类中添加统计方法,其余类都继承自这个基类。安全
然而,这种方式修改仍是很大,并且定制性不好。之后有新人加入以后,都要嘱咐其继承自这个基类,因此这种方式并不可取。函数
咱们能够为UIViewController
建一个Category
,而后在全部控制器中引入这个Category
。固然咱们也能够添加一个PCH
文件,而后将这个Category
添加到PCH
文件中。源码分析
咱们建立一个Category
来覆盖系统方法,系统会优先调用Category
中的代码,而后在调用原类中的代码。布局
咱们能够经过下面的这段伪代码来看一下:学习
#import "UIViewController+EventGather.h"
@implementation UIViewController (EventGather)
- (void)viewDidLoad {
NSLog(@"页面统计:%@", self);
}
@end
复制代码
咱们可使用苹果的“黑魔法”Method Swizzling
,Method Swizzling
本质上就是对IMP
和SEL
进行交换。
Method Swizzing
是发生在运行时的,主要用于在运行时将两个Method
进行交换,咱们能够将Method Swizzling
代码写到任何地方,可是只有在这段Method Swilzzling
代码执行完毕以后互换才起做用。
并且Method Swizzling
也是__iOS__中AOP
(面相切面编程)的一种实现方式,咱们能够利用苹果这一特性来实现AOP
编程。
首先,让咱们经过两张图片来了解一下Method Swizzling
的实现原理
上面图一中selector2
本来对应着IMP2
,可是为了更方便的实现特定业务需求,咱们在图二中添加了selector3
和IMP3
,而且让selector2
指向了IMP3
,而selector3
则指向了IMP2
,这样就实现了“方法互换”。
在OC
语言的runtime
特性中,调用一个对象的方法就是给这个对象发送消息。是经过查找接收消息对象的方法列表,从方法列表中查找对应的SEL
,这个SEL
对应着一个IMP
(一个IMP
能够对应多个SEL
),经过这个IMP
找到对应的方法调用。
在每一个类中都有一个Dispatch Table
,这个Dispatch Table
本质是将类中的SEL
和IMP
(能够理解为函数指针)进行对应。而咱们的Method Swizzling
就是对这个table
进行了操做,让SEL
对应另外一个IMP
。
在实现Method Swizzling
时,核心代码主要就是一个runtime
的C语言API:
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
复制代码
就拿上面咱们说的页面统计的需求来讲吧,这个需求在不少公司都很常见,咱们下面的Demo就经过Method Swizzling
简单的实现这个需求。
咱们先给UIViewController
添加一个Category
,而后在Category
中的+(void)load
方法中添加Method Swizzling
方法,咱们用来替换的方法也写在这个Category
中。因为load
类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,而且不须要咱们手动调用。并且这个方法具备惟一性,也就是只会被调用一次,不用担忧资源抢夺的问题。
定义Method Swizzling
中咱们自定义的方法时,须要注意尽可能加前缀,以防止和其余地方命名冲突,Method Swizzling
的替换方法命名必定要是惟一的,至少在被替换的类中必须是惟一的。
#import "UIViewController+swizzling.h"
#import <objc/runtime.h>
@implementation UIViewController (swizzling)
+ (void)load {
// 经过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,若是是类方法就使用class_getClassMethod()函数获取。
Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));
/** 咱们在这里使用class_addMethod()函数对Method Swizzling作了一层验证,若是self没有实现被交换的方法,会致使失败。 并且self没有交换的方法实现,可是父类有这个方法,这样就会调用父类的方法,结果就不是咱们想要的结果了。 因此咱们在这里经过class_addMethod()的验证,若是self实现了这个方法,class_addMethod()函数将会返回NO,咱们就能够对其进行交换了。 */
if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
method_exchangeImplementations(fromMethod, toMethod);
}
}
// 咱们本身实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
- (void)swizzlingViewDidLoad {
NSString *str = [NSString stringWithFormat:@"%@", self.class];
// 咱们在这里加一个判断,将系统的UIViewController的对象剔除掉
if(![str containsString:@"UI"]){
NSLog(@"统计打点 : %@", self.class);
}
[self swizzlingViewDidLoad];
}
@end
复制代码
看到上面的代码,确定有人会问:楼主,你太粗心了,你在swizzlingViewDidLoad
方法中又调用了[self swizzlingViewDidLoad];
,这难道不会产生递归调用吗? 答:然而....并不会😏。
还记得咱们上面的图一和图二吗?Method Swizzling
的实现原理能够理解为”方法互换“。假设咱们将A和B两个方法进行互换,向A方法发送消息时执行的倒是B方法,向B方法发送消息时执行的是A方法。
例如咱们上面的代码,系统调用UIViewController
的viewDidLoad
方法时,实际上执行的是咱们实现的swizzlingViewDidLoad
方法。而咱们在swizzlingViewDidLoad
方法内部调用[self swizzlingViewDidLoad];
时,执行的是UIViewController
的viewDidLoad
方法。
以前我也说到,在咱们项目开发过程当中,常常由于NSArray
数组越界或者NSDictionary
的key
或者value
值为nil
等问题致使的崩溃,对于这些问题苹果并不会报一个警告,而是直接崩溃,感受苹果这样确实有点“太狠了”。
由此,咱们能够根据上面所学,对NSArray
、NSMutableArray
、NSDictionary
、NSMutableDictionary
等类进行Method Swizzling
,实现方式仍是按照上面的例子来作。可是....你发现Method Swizzling
根本就不起做用,代码也没写错啊,究竟是什么鬼?
这是由于Method Swizzling
对NSArray
这些的类簇是不起做用的。由于这些类簇类,实际上是一种抽象工厂的设计模式。抽象工厂内部有不少其它继承自当前类的子类,抽象工厂类会根据不一样状况,建立不一样的抽象对象来进行使用。例如咱们调用NSArray
的objectAtIndex:
方法,这个类会在方法内部判断,内部建立不一样抽象类进行操做。
因此也就是咱们对NSArray
类进行操做其实只是对父类进行了操做,在NSArray
内部会建立其余子类来执行操做,真正执行操做的并非NSArray
自身,因此咱们应该对其“真身”进行操做。
下面咱们实现了防止NSArray
由于调用objectAtIndex:
方法,取下标时数组越界致使的崩溃:
#import "NSArray+LXZArray.h"
#import "objc/runtime.h"
@implementation NSArray (LXZArray)
+ (void)load {
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
- (id)lxz_objectAtIndex:(NSUInteger)index {
if (self.count-1 < index) {
// 这里作一下异常处理,否则都不知道出错了。
@try {
return [self lxz_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 lxz_objectAtIndex:index];
}
}
@end
复制代码
你们发现了吗,__NSArrayI
才是NSArray
真正的类,而NSMutableArray
又不同😂。咱们能够经过runtime
函数获取真正的类:
objc_getClass("__NSArrayI");
复制代码
下面咱们列举一些经常使用的类簇的“真身”:
类 | “真身” |
---|---|
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
其余自行Google....
在项目中咱们确定会在不少地方用到Method Swizzling
,并且在使用这个特性时有不少须要注意的地方。咱们能够将Method Swizzling
封装起来,也可使用一些比较成熟的第三方。 在这里我推荐__Github__上星最多的一个第三方-jrswizzle
里面核心就两个类,代码看起来很是清爽。
#import <Foundation/Foundation.h>
@interface NSObject (JRSwizzle)
+ (BOOL)jr_swizzleMethod:(SEL)origSel_ withMethod:(SEL)altSel_ error:(NSError**)error_;
+ (BOOL)jr_swizzleClassMethod:(SEL)origSel_ withClassMethod:(SEL)altSel_ error:(NSError**)error_;
@end
// MethodSwizzle类
#import <objc/objc.h>
BOOL ClassMethodSwizzle(Class klass, SEL origSel, SEL altSel);
BOOL MethodSwizzle(Class klass, SEL origSel, SEL altSel);
复制代码
在上面的例子中,若是只是单独对NSArray
或NSMutableArray
中的单个类进行Method Swizzling
,是能够正常使用而且不会发生异常的。若是进行Method Swizzling
的类中,有两个类有继承关系的,而且Swizzling
了同一个方法。例如同时对NSArray
和NSMutableArray
中的objectAtIndex:
方法都进行了Swizzling
,这样可能会致使父类Swizzling
失效的问题。
对于这种问题主要是两个缘由致使的,首先是不要在+ (void)load
方法中调用[super load]
方法,这会致使父类的Swizzling
被重复执行两次,这样父类的Swizzling
就会失效。例以下面的两张图片,你会发现因为NSMutableArray
调用了[super load]
致使父类NSArray
的Swizzling
代码被执行了两次。
错误代码:
#import "NSMutableArray+LXZArrayM.h"
@implementation NSMutableArray (LXZArrayM)
+ (void)load {
// 这里不该该调用super,会致使父类被重复Swizzling
[super load];
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
method_exchangeImplementations(fromMethod, toMethod);
}
复制代码
这里因为在子类中调用了super,致使NSMutableArray执行时,父类NSArray也被执行了一次。
父类NSArray执行了第二次Swizzling,这时候就会出现问题,后面会讲具体缘由。
这样就会致使程序运行过程当中,子类调用Swizzling
的方法是没有问题的,父类调用同一个方法就会发现Swizzling
失效了.....具体缘由咱们后面讲!
还有一个缘由就是由于代码逻辑致使Swizzling
代码被执行了屡次,这也会致使Swizzling
失效,其实原理和上面的问题是同样的,咱们下面讲讲为何会出现这个问题。
咱们上面提到过Method Swizzling
的实现原理就是对类的Dispatch Table
进行操做,每进行一次Swizzling
就交换一次SEL
和IMP
(能够理解为函数指针),若是Swizzling
被执行了屡次,就至关于SEL
和IMP
被交换了屡次。这就会致使第一次执行成功交换了、第二次执行又换回去了、第三次执行.....这样换来换去的结果,能不能成功就看运气了😄,这也是好多人说Method Swizzling
很差用的缘由之一。
从这张图中咱们也能够看出问题产生的缘由了,就是Swizzling
的代码被重复执行,为了不这样的缘由出现,咱们能够经过__GCD__的dispatch_once
函数来解决,利用dispatch_once
函数内代码只会执行一次的特性。
在每一个Method Swizzling
的地方,加上dispatch_once
函数保证代码只被执行一次。固然在实际使用中也能够对下面代码进行封装,这里只是给一个示例代码。
#import "NSMutableArray+LXZArrayM.h"
@implementation NSMutableArray (LXZArrayM)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
method_exchangeImplementations(fromMethod, toMethod);
});
}
复制代码
这里还要告诉你们一个调试小技巧,已经知道的能够略过😊。咱们以前说过IMP
本质上就是函数指针,因此咱们能够经过打印函数指针的方式,查看SEL
和IMP
的交换流程。
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);
NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);
NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
method_exchangeImplementations(fromMethod, toMethod);
NSLog(@"%p", method_getImplementation(fromMethod));
NSLog(@"%p", method_getImplementation(toMethod));
复制代码
看到这个打印结果,你们应该明白什么问题了吧:
2016-04-13 14:16:33.477 [16314:4979302] 0x1851b7020
2016-04-13 14:16:33.479 [16314:4979302] 0x1000fb3c8
2016-04-13 14:16:33.479 [16314:4979302] 0x1000fb3c8
2016-04-13 14:16:33.480 [16314:4979302] 0x1851b7020
2016-04-13 14:16:33.480 [16314:4979302] 0x1851b7020
2016-04-13 14:16:33.480 [16314:4979302] 0x1000fb3c8
2016-04-13 14:16:33.481 [16314:4979302] 0x1000fb3c8
2016-04-13 14:16:33.481 [16314:4979302] 0x1851b7020
复制代码
下面是Method Swizzling
的实现源码,从源码来看,其实内部实现很简单。核心代码就是交换两个Method
的imp
函数指针,这也就是方法被swizzling
屡次,可能会被换回去的缘由,由于每次调用都会执行一次交换操做。
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
rwlock_writer_t lock(runtimeLock);
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp;
flushCaches(nil);
updateCustomRR_AWZ(nil, m1);
updateCustomRR_AWZ(nil, m2);
}
复制代码
既然Method Swizzling
能够对这个类的Dispatch Table
进行操做,操做后的结果对全部当前类及子类都会产生影响,因此有人认为Method Swizzling
是一种危险的技术,用很差很容易致使一些不可预见的__bug__,这些__bug__通常都是很是难发现和调试的。
这个问题能够引用念茜大神的一句话:使用 Method Swizzling 编程就比如切菜时使用锋利的刀,一些人由于担忧切到本身因此惧怕锋利的刀具,但是事实上,使用钝刀每每更容易出事,而利刀更为安全。
在这个Demo
中经过Method Swizzling
,简单实现了一个崩溃拦截功能。实现方式就是将原方法Swizzling
为本身定义的方法,在执行时先在本身方法中作判断,根据是否异常再作下一步处理。
Demo
只是来辅助读者更好的理解文章中的内容,应该博客结合Demo
一块儿学习,只看Demo
仍是不能理解更深层的原理。Demo
中代码都会有注释,各位能够打断点跟着Demo
执行流程走一遍,看看各个阶段变量的值。
Demo地址:刘小壮的Github
简书因为排版的问题,阅读体验并很差,布局、图片显示、代码等不少问题。因此建议到我Github
上,下载Runtime PDF
合集。把全部Runtime
文章总计九篇,都写在这个PDF
中,并且左侧有目录,方便阅读。
下载地址:Runtime PDF 麻烦各位大佬点个赞,谢谢!😁