1.4 黑魔法
在实际项目开发中,经常会用到黑魔法,所谓黑魔法就是通过Objective-C语言强大的runtime来给类的类方法或实例方法做交换,从而达到不用修改原类的代码就可以给原类中特定的方法做替换操作。虽然这只是runtime的其中一个功能,但可以用来做很多事,例如用在一些统计业务中,不用在每个类中都写一遍,而可以直接通过黑魔法来交换方法,把统计业务写在自定义的方法中可以神不知鬼不觉地达到我们想要的效果。此外还有一个好处,就是可以将所有的统计业务代码写在同一处,方便管理。下面通过实际操作来实践一下。
创建一个iOS的Single View Application,基于系统自动创建的工程来开展实验。
系统自动创建了一个继承自UIViewController的ViewController作为主视图控制器。
在ViewController中,覆盖-viewWillAppear:方法:
此时开发者Tom创建了一个基于UIViewController的Category,名为UIViewController+Tom,该控制器有一个方法-tom_viewWillAppear:,用于和UIViewController的-viewWillAppear:做交换。为了简单演示,下面给出代码。
然后在AppDelegate中提供一个交换方法的触发场景:
按Command+R组合键运行,打印结果如下。
tom_viewWillAppear origin viewWillAppear
简单分析一下,当程序运行起来的时候,先通过AppDelegate的代理方法——application:didFinishLaunchingWithOptions:方法,表示程序已完成启动,其中调用了交换方法。首先通过runtime函数class_getInstanceMethod获取到UIViewController系统自带的实例方法-viewWillAppear:,同理也获取到Tom提供的-tom_viewWillAppear:方法,最后用method_exchangeImplementations来实现两个方法的交换。
这里着重看一下UIViewController+Tom中-tom_viewWillAppear:的实现代码,其中又调用了一次本方法,有人认为调用该方法会造成死循环,其实并非如此,因为通过方法交换,在-tom_viewWillAppear:中还调用了-tom_viewWillAppear:方法,其实调用的已经是被替换了的原系统的-viewWillAppear:方法实现了,所以并不会造成死循环。再看一下完整顺序,当ViewController要出现在屏幕上时,系统自动调用-viewWillAppear:方法,此时该方法已经被替换了,其实调用的方法实现是-tom_viewWillAppear:的实现,在-tom_viewWillAppear:实现中,又调用了-tom_viewWillAppear:方法,此时-tom_viewWillAppear:的实现是被替换成了系统的-viewWillAppear:方法,虽然看起来很混乱,其实并不复杂。从整体来看,相当于我们在系统方法-viewWillAppear:调用的同时,顺带执行了一段自定义的代码,在该例子中其实是NSLog(@"tom_viewWillAppear");,不影响原来方法的使用。当然我们也可以在替换方法中不去调用原来的方法,这样就达到了一个完全替换的效果。这用在一些非系统方法中没有问题,但对于-viewWillAppear:这样的系统方法,如果不去调用其实现,将很有可能造成许多令人讨厌的麻烦。
以上这段代码其实有个问题,不知道有没有细心的读者发现,我们在-tom_viewWillAppear:中先调用了原来的实现。然而在这个时候,我们又有了一个开发者Jerry,在Jerry的代码中,同样需要在UIViewController的-viewWillAppear:方法中进行交换,为了简单演示,大致跟之前Tom的一样。创建UIViewController+Jerry:
同样,在AppDelegate中添加Jerry对于-viewWillAppear:方法的交换,最后的代码如下。
最后再按Command+R组合键运行之后,看一下打印结果:
tom_viewWillAppear jerry_viewWillAppear origin viewWillAppear
可以看出来先打印Tom的,然后打印Jerry的,最后打印原来的。
分析一下过程,首先Tom来交换方法,然后Jerry又交换方法,那Jerry交换的是原来的-viewWillAppear:还是-tom_viewWillAppear:呢?在一开始只有Tom交换方法的时候,打印顺序是tom->origin,然后Jerry也交换方法的时候,打印顺序是tom->jerry->origin,可以猜测Jerry交换的方法应该是origin的。究竟方法的交换顺序如何呢?
通过图1-3可以看出一开始的Selector与具体实现的对应关系,在没有交换之前,都是各方法对应各自的实现,系统原本的方法(Origin)对应的实现就是原本的viewWillAppear,Tom提供的方法对应的实现是tom_viewWillAppear,Jerry对应的实现是jerry_viewWillAppear,这个是很好理解的(注:因为通过Selector来获取Method以及Method对应的Implementation不太方便表述直接的关系,所以暂时用@selector()来表示Selector,而用IMP()来表示实现)。
图1-3 交换前的对应关系
首先Tom进行了交换方法,关系如图1-4所示。
图1-4 第一次交换方法后的对应关系
此时,从图1-4中可以很清楚地看到,与@selector(viewWillAppear:)对应的Implementation已经变为tom_viewWillAppear,而@selector(tom_viewWillAppear:)对应的Implementation也换作了viewWillAppear。
重新观察一下Jerry的交换方法,是@selector(jerry_viewWillAppear:)与@selector(viewWillAppear:)交换实现方法,而@selector(viewWillAppear:)对应的实现之前已经被替换成IMP(tom_viewWillAppear:),所以替换之后@selector(viewWillAppear:)的实现就是IMP(jerry_viewWillAppear:),而@selector(jerry_viewWillAppear:)对应的实现也成了IMP(tom_viewWillAppear:),如图1-5所示。
介绍完了交换顺序,下面再来分析一下调用的顺序。
因为UIViewController+Tom、UIViewController+Jerry两个Category是对UIViewController的方法进行替换,仅对UIViewController的-viewWillAppear:起作用,并不能对其子类ViewController有效。读者可能在此处有些不明白,因为在日常开发中Category的方法不仅对该类起作用,对其子类也可以直接使用,而此处却说不能对子类有效,这是因为此处的重点是Category的交换方法是仅针对UIViewController的,因此对于子类不适用,并非是Category的方法对于UIViewController的子类不适用。所以在ViewController将要显示在的时候,调用的并不是替换后的方法,而仍然是ViewController的原生方法-viewWillAppear:,在该方法中,代码如下:
图1-5 第二次交换方法后的对应关系
先是调用了[super viewWillAppear:animated],此时是调用ViewController父类方法表中的-viewWillAppear:,而父类正是UIViewController,即UIViewController的-viewWillAppear:方法是被替换过的了。通过图1-5,原生方法@selector(viewWillAppear:)对应的实现是IMP(jerry_viewWillAppear:),而-jerry_viewWillAppear:方法的实现如下。
可以看到-jerry_viewWillAppear:中先是调用了与其替换了的方法,那与之对应的实现是IMP(tom_viewWillAppear:),同理-tom_viewWillAppear:的代码如下。
发现在该方法实现中,先调用了与其交换的方法。那与其交换的是哪一个方法呢?通过图1-5也可以看到,@selector(jerry_viewWillAppear:)与@selector(viewWillAppear:),也就是到了此处又得先调用IMP(viewWillAppear:),此时读者可能感觉有些困惑,难道又要先调用IMP(jerry_viewWillAppear:)?怎么又回去了?是要死循环了吗?其实不然,上面提到,两个Category是基于UIViewController的,所以此处的IMP(viewWillAppear:)是UIViewController的,也就是ViewController中-viewWillAppear:中的[super viewWillAppear:animated]方法,这样一来就疏通了它们之间的关系。假如一开始没有交换方法,那么对于-viewWillAppear:方法来说,调用的顺序是:self→super,而通过两次交换方法之后的顺序则是:self→jerry→tom→super。
在此例中,打印结果的顺序是不具有代表性的,因为在每个方法中,如果先打印再去调用其替换方法则又是一个不一样的顺序,如果在此例中先打印再调用交换方法,则打印顺序如下,其中逻辑读者可以留作自己思考,这里不做赘述。
origin viewWillAppear jerry_viewWillAppear tom_viewWillAppear
同样,如果此例中交换方法仅仅是对于子类ViewController的,打印结果如下,同样不做赘述。
jerry_viewWillAppear tom_viewWillAppear origin viewWillAppear
交换系统的方法是交换方法中的其中一种用法,还有一种用法是交换自定义的方法。假设该自定义方法在交换方法中不用调用原实现,即完全替换的场景,那么多种交换会造成只有最后一次交换有效,之前的交换都会不起作用,这会给开发者造成一定的困扰,需要注意。又或者假如在原方法中是有返回值的,而交换方法是直接通过方法名来获取方法的,如果该交换方法是一个无返回值的,此时编译器也不会报任何警告,从而在实际开发中有一些安全隐患,甚至导致Crash。
之所以举例对于UIViewController来实现交换方法,是因为在实际开发中,这是比较常见的使用场景,交换方法通常使用在大规模的范围中,而写的时候却只要很简单的方式就能实现在项目中处处作用的功能,这也正是AOP(Aspect-Oriented Programming)的优势所在。
对于黑魔法,笔者的建议是要慎用!之所以称为黑魔法,有一些不可思议,当然也有些神秘不可知的意思。黑魔法的使用场景一般是用在比较大的范围,所以一旦使用不当出现问题,则也是大规模的,其中就包括此例中多个交换方法会造成不同调用顺序甚至不起作用所产生的一系列问题,如果需要使用,应当保证交换方法的统一以及规范。
本节小结
(1)了解黑魔法的原理,以及多次交换下的调用顺序,黑魔法的使用是AOP编程思想的重要实现,但在实际开发中需要谨慎;
(2)交换方法虽然给特定情况下的开发带来了便利,但同时也可能存在一些“侵入性”,可能会影响其他类的代码,因此不要为了使用而使用,应尽量将影响范围控制在期望中。