今天咱们主要讨论iOS runtime中的一种黑色技术,称为Method Swizzling。字面上理解Method Swizzling可能比较晦涩难懂,毕竟不是中文,不过你能够理解为“移花接木”或者“偷天换日”。git
介绍某种技术的用途,最简单的方式就是抛出一些应用场景来引出这种技术的必要性。所以,这里我举个例子以下。github
假设工程中有不少ViewController,我须要你统计每一个页面间跳转的次数。要求:对原工程的改动越少越好。编程
针对以上需求,你可能会立马想出如下两种方案:框架
方案一:ide
在每一个ViewController的 viewWillAppear 或者 viewDidAppear 方法中对记录跳转次数的某个全局变量(设为 g_viewTransCount )进行计数自增,代码应该是这样的:函数
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; g_viewTransCount++; }
每一个ViewController类中都须要作此操做,显然不合适。由于跳转次数统计这种业务与APP的主业务并无强关联,上面的代码会形成耦合度太高。随着APP业务的不断扩大,代码中这样的杂质代码会愈来愈大,维护也愈来愈困难。并且该方案也违背了咱们的要求:对原工程的改动越少越好。所以方案一是个不好的方法。因而咱们有了方案二。spa
方案二:code
有没有某种方法能够不用对每一个ViewCotroller都修改呢?有!让每一个ViewController都继承某个新的ViewController(设为BaseViewController),而后将统计的代码放到BaseViewCotroller的 viewWillAppear或者viewDidAppear中。这种方案看似较合理,但有如下弊端:对象
可见,方案二虽然相比方案一少一些看获得的“代码杂质”,但对工程的改动一样是巨大的,尤为当工程比较庞大时。blog
正由于以上方案的不完美,才引出本文的黑科技:Method Swizzling。
先归纳一下在上述情景下使用Method Swizzling有哪些优点:
接下来就是激动人心的Coding Time了。让咱们解开Method Swizzling的神秘面纱。直接上代码,有注释。在工程中新建一个UIViewController的category:
#import "UIViewController+swizzling.h" #import <objc/runtime.h> @implementation UIViewController (swizzling) + (void)load { SEL origSel = @selector(viewDidAppear:); SEL swizSel = @selector(swiz_viewDidAppear:); [UIViewController swizzleMethods:[self class] originalSelector:origSel swizzledSelector:swizSel]; } //exchange implementation of two methods + (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel { Method origMethod = class_getInstanceMethod(class, origSel); Method swizMethod = class_getInstanceMethod(class, swizSel); //class_addMethod will fail if original method already exists BOOL didAddMethod = class_addMethod(class, origSel, method_getImplementation(swizMethod), method_getTypeEncoding(swizMethod)); if (didAddMethod) { class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); } else { //origMethod and swizMethod already exist method_exchangeImplementations(origMethod, swizMethod); } } - (void)swiz_viewDidAppear:(BOOL)animated { NSLog(@"I am in - [swiz_viewDidAppear:]"); //handle viewController transistion counting here, before ViewController instance calls its -[viewDidAppear:] method //须要注入的代码写在此处 [self swiz_viewDidAppear:animated]; } @end
上述代码作了这么一件事:在UIViewController的viewDidAppear:方法调用前插入了跳页计数处理,这一切都在运行时完成。对于上述代码有如下几处须要介绍的:
+ (void)load 方法是一个类方法,当某个类的代码被读到内存后,runtime会给每一个类发送 + (void)load 消息。所以 + (void)load 方法是一个调用时机至关早的方法,并且无论父类仍是子类,其 + (void)load 方法都会被调用到,很适合用来插入swizzling方法
最核心的代码要数 + (void)swizzleMethods:(Class)class originalSelector:(SEL)origSel swizzledSelector:(SEL)swizSel 了。从函数签名能够看出,该函数是为了交换两个方法内部实现。将目光移到Line23,交换两个方法的内部实现主要依靠两个runtime API:
class_replaceMethod(class, swizSel, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); method_exchangeImplementations(origMethod, swizMethod);
再看一下Line32, - (void)swiz_viewDidAppear:(BOOL)animated 函数看起来像死循环,实际上不会的。缘由请看我在下图的注释:
此外,经过断点能够进一步判断出view controller的viewDidAppear实际方法体与category的swiz_viewDidAppear方法的执行前后顺序。为了更直观地说明两者的顺序,咱们能够看一下我打出的Log:
经过Log所打印出的顺序足以验证咱们的想法。
以上的method swizzling能够应用于iOS的任何类中对其进行代码注入,而且丝绝不影响现有工程的代码。例如,我再举个例子(没办法,我就是喜欢举例子,但我无非是想让你掌握的更多一些)。你想统计整个工程中全部按钮的点击事件的次数,也就是touchUpInside event发生的次数。刚开始你可能会以为稍微有些没有头绪,由于注入代码的“切入点”相比于UIViewController的viewDidLoad等方法而言不是那么好找。这时候若是你能仔细考虑如下问题或许能找到思路:
第一个问题很好回答,event是发送给UIButton实例,本质上是发送给UIControl实例;
第二个问题你不懂的话就去看看UIControl的头文件找找线索,因而在头文件中咱们找到这样一个函数:
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event;
看起来很靠近咱们的需求, 事实上的确如此。这要从iOS的事件传递机制提及,当你在iOS设备上触摸一个点时这个触摸动做被包装成一个UIEvent按照UIApplication->UIWindow->UIView的顺序传递下去,当发现最后的接受者是UIControl时就会发送上述消息。所以,咱们能够对sendAction:方法进行swizzling代码注入来达到统计按钮点击次数的目的。更深刻一些,则须要针对不一样的action、target、event的状态进行判断,以达到更精准的统计。关于这一部份内容我将在下一篇iOS动态性系列文章中详细探讨,敬请期待!
OK,文章就到这里,小伙伴们洗洗睡吧。哈哈,开个玩笑,俗话说,“好戏都在后头”,接下来的部分更好用。看来以上的method swizzling代码你是否以为太复杂了?此外,当你尝试对多个类进行swizzle时会发现不少代码是冗余的,每一个category文件的框架都长得差很少。那是否有进一步封装的可能性呢?那是必须的。庆幸的是有团队已经帮咱们封装了,咱们直接拿来用就能够。这就是有名的Aspect库。
Aspect库是对面向切面编程(Aspect Oriented Programming)的实现,里面封装了Runtime的方法,也封装了上文的Method Swizzling方法。所以咱们也能够看到,Method Swizzling也是AOP编程的一种。Aspect的用途很普遍,这里不具体展开,想了解更多的能够看一下官方github的介绍,已经够详细了。这里咱们只介绍其基础应用。Aspect只提供了两个接口:
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error { return aspect_add((id)self, selector, options, block, error); } /// @return A token which allows to later deregister the aspect. - (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error { return aspect_add(self, selector, options, block, error); }
使用起来也很是方便,使用Aspect对本文最初提出的需求“统计每一个页面间跳转的次数”进行改造,代码变成这样子:
[UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info){ g_viewTransCount++ NSLog(@"[ASPECT] inject in class instance:%@", [info instance]); } error:NULL];
将以上代码放到AppDelegate的 didFinishLaunchingWithOptions 函数最开始处便可,你能够参考我在文末贴出的代码,使用一个专门的管理类来管理这些AOP代码。
相比于上半部分的原始Method Swizzling代码,使用Aspect有如下好处:
Method Swizzling以及Runtime的一些特性就是iOS里的黑科技,若是能灵活应用的话能够在保证解决问题的前提降低低模块之间的耦合度,提升代码的可复用性。至于Method Swizzling与Aspect库的选择因人而异,我我的建议在最初阶段先放下Aspect而只用Method Swizzling原始代码去实现代码注入。掌握本质老是不吃亏的。
本文的示例代码:Github
欢迎关注个人github上的其余代码,别忘记随手点个Star,给我更多支持与鼓励!
原创文章,转载请注明 编程小翁@博客园,邮件zilin_weng@163.com,欢迎各位与我在C/C++/Objective-C/机器视觉等领域展开交流!