需求
就拿我们公司项目来说吧,我们公司是做导航的,而且项目规模比较大,各个控制器功能都已经实现。突然有一天老大过来,说我们要在所有页面添加统计功能,也就是用户进入这个页面就统计一次。郑州iOS培训会想到下面的一些方法:
手动添加
直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴...
上面这种方法太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
的实现原理
上面图一中selecor2
原本对应着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 @implementation UIViewController (swizzling) + (void)load { [super load]; // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。 Method fromMethod = class_getInstanceMethod([self class], @selecor(viewDidLoad)); Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad)); if (!class_addMethod([selfclass], @selector(viewDidLoad), 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
方法。
Method Swizzling类簇
之前郑州iOS培训也说到,在我们项目开发过程中,经常因为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 { [super load]; Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selecor(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 [selflxz_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
,而且在使用这个特性时有很多需要注意的地方。我们可以将Method Swizzling
封装起来,也可以使用一些比较成熟的第三方。
在这里我推荐Github上星**多的一个第三方-jrswizzle
里面核心就两个类,代码看起来非常清爽。
#import @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 BOOLClassMethodSwizzle(Class klass, SEL origSel, SEL altSel); BOOL MethodSwizzle(Class klass, SEL origSel, SEL altSel);
Method Swizzling危险吗?
既然Method Swizzling
可以对这个类的Dispatch Table
进行操作,操作后的结果对所有当前类及子类都会产生影响,所以有人认为Method Swizzling
是一种危险的技术,用不好很容易导致一些不可预见的bug,这些bug一般都是非常难发现和调试的。
这个问题可以引用念茜大神的一句话:使用 Method Swizzling 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。
更多技术文章关注蓝鸥iOS培训资讯
蓝鸥郑州iOS开发培训是业界口碑**iOS培训机构,培训期是四个月左右,四个月脱产班(脱产班顾名思义就是脱离生产,也就是不工作,全身心的学习,也就是全日制的学习),可以免费试听,蓝鸥的老师在行业里有3年以上工作项目经验的。目前iOS开发是非常有前景的职业方向,月薪维持在8k-10k,随着时间及经验的积累薪资待遇也逐渐提升(看个人技术、项目、开发、及管理水平,15k-20k的也大有人在),学生可选择全款学习亦可贷学费方式参加学习,工作之后2-3个月就有能力将学费还清。
QQ:2290909800 QQ交流群:493076664;97682894;咨询电话(Phone):0371-55397597 15838221845
地址(Address): 郑州市高新区莲花街牡丹路教育科技产业园区B座 网址(website):http://hn.lanou3g.com/