内容简介:就拿我们公司项目来说吧,我们公司是做导航的,而且项目规模比较大,各个控制器功能都已经实现。突然有一天老大过来,说我们要在所有页面添加统计功能,也就是用户进入这个页面就统计一次。我们会想到下面的一些方法:直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴... 上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。我们可以使用
就拿我们公司项目来说吧,我们公司是做导航的,而且项目规模比较大,各个控制器功能都已经实现。突然有一天老大过来,说我们要在所有页面添加统计功能,也就是用户进入这个页面就统计一次。我们会想到下面的一些方法:
手动添加
直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴... 上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。
继承
我们可以使用 OOP 的特性之一,继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。
然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。
Category
我们可以为 UIViewController 建一个 Category ,然后在所有控制器中引入这个 Category 。当然我们也可以添加一个 PCH 文件,然后将这个 Category 添加到 PCH 文件中。
我们创建一个 Category 来覆盖系统方法,系统会优先调用 Category 中的代码,然后在调用原类中的代码。
我们可以通过下面的这段伪代码来看一下:
#import "UIViewController+EventGather.h"
@implementation UIViewController (EventGather)
- (void)viewDidLoad {
NSLog(@"页面统计:%@", self);
}
@end
复制代码
Method Swizzling
我们可以使用苹果的“黑魔法” Method Swizzling , Method Swizzling 本质上就是对 IMP 和 SEL 进行交换。
Method Swizzling原理
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使用
在实现 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]; ,这难道不会产生递归调用吗? 答:然而....并不会:smirk:。
还记得我们上面的图一和图二吗? Method Swizzling 的实现原理可以理解为”方法互换“。假设我们将A和B两个方法进行互换,向A方法发送消息时执行的却是B方法,向B方法发送消息时执行的是A方法。
例如我们上面的代码,系统调用 UIViewController 的 viewDidLoad 方法时,实际上执行的是我们实现的 swizzlingViewDidLoad 方法。而我们在 swizzlingViewDidLoad 方法内部调用 [self swizzlingViewDidLoad]; 时,执行的是 UIViewController 的 viewDidLoad 方法。
Method Swizzling类簇
之前我也说到,在我们项目开发过程中,经常因为 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 又不一样:joy:。我们可以通过 runtime 函数获取真正的类:
objc_getClass("__NSArrayI");
复制代码
举例
下面我们列举一些常用的类簇的“真身”:
| 类 | “真身” |
|---|---|
| NSArray | __NSArrayI |
| NSMutableArray | __NSArrayM |
| NSDictionary | __NSDictionaryI |
| NSMutableDictionary | __NSDictionaryM |
其他自行Google....
JRSwizzle
在项目中我们肯定会在很多地方用到 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); 复制代码
Method Swizzling 错误剖析
在上面的例子中,如果只是单独对 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 被交换了多次。这就会导致第一次执行成功交换了、第二次执行又换回去了、第三次执行.....这样换来换去的结果,能不能成功就看运气了:smile:,这也是好多人说 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);
});
}
复制代码
这里还要告诉大家一个调试小技巧,已经知道的可以略过:blush:。我们之前说过 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 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危险吗?
既然 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 麻烦各位大佬点个赞,谢谢!:grin:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 资料 | Git 魔法.pdf
- 【译】canvas笔触魔法师
- (Angular)模版引用变量的魔法
- 跟着 WWDC 一起探秘符号解析的魔法
- 魔法书2:测试Arduino 执行速度极限
- 深度學習世界的魔法陣們
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
NoSQL精粹
[美]Pramod J. Sadalage、[美]Martin Fowler / 爱飞翔 / 机械工业出版社 / 2013-8 / 49.00元
《NoSQL精粹》为考虑是否可以使用和如何使用NoSQL数据库的企业提供了可靠的决策依据。它由世界级软件开发大师和软件开发“教父”Martin Fowler与Jolt生产效率大奖图书作者Pramod J. Sadalage共同撰写。书中全方位比较了关系型数据库与NoSQL数据库的异同;分别以Riak、MongoDB、Cassandra和Neo4J为代表,详细讲解了键值数据库、文档数据库、列族数据库......一起来看看 《NoSQL精粹》 这本书的介绍吧!