首页 > 其他分享 >黑色魔法- Method Swizzling

黑色魔法- Method Swizzling

时间:2023-05-29 14:06:43浏览次数:38  
标签:魔法 method fromMethod Swizzling SEL toMethod Method

开发需求

如果产品经理突然说:”在所有页面添加统计功能,也就是用户进入这个页面就统计一次”。我们会想到下面的一些方法:

- 手动添加

直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴…

上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。

- 继承

我们可以使用继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。

然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。

- Category

我们可以为UIViewController建一个Category,然后在所有控制器中引入这个Category。当然我们也可以添加一个PCH文件,然后将这个Category添加到PCH文件中。

- Method Swizzling

我们可以使用苹果的“黑色魔法”Method SwizzlingMethod Swizzling本质上就是对IMPSEL进行交换。

Method Swizzling

原理

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。

黑色魔法- Method Swizzling_#import

黑色魔法- Method Swizzling_父类_02

使用注意

类簇设计模式

在iOS中NSNumberNSArrayNSDictionary等这些类都是类簇(Class Clusters),一个NSArray的实现可能由多个类组成。

所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的。

下面列举了NSArrayNSDictionary本类的类名,可以通过Runtime函数取出本类。

黑色魔法- Method Swizzling_父类_03

注意要点

  • Swizzling应该总在+load中执行
  • Swizzling应该总是在dispatch_once中执行
  • Swizzling+load中执行时,不要调用[super load]。如果多次调用了[super load],可能会出现“Swizzle无效”的假象,原理见下图:

封装

在项目中我们肯定会在很多地方用到Method Swizzling,而且在使用这个特性时有很多需要注意的地方。我们可以将Method Swizzling封装起来,也可以使用一些比较成熟的第三方。
里面核心就两个类,代码看起来非常清爽。

#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);

错误剖析

在上面的例子中,如果只是单独对NSArrayNSMutableArray中的单个类进行Method Swizzling,是可以正常使用并且不会发生异常的。如果进行Method Swizzling的类中,有两个类有继承关系的,并且Swizzling了同一个方法。例如同时对NSArrayNSMutableArray中的objectAtIndex:方法都进行了Swizzling,这样可能会导致父类Swizzling失效的问题。

对于这种问题主要是两个原因导致的,首先是不要在+ (void)load方法中调用[super load]方法,这会导致父类的Swizzling被重复执行两次,这样父类的Swizzling就会失效。例如下面的两张图片,你会发现由于NSMutableArray调用了[super load]导致父类NSArraySwizzling代码被执行了两次。

#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也被执行了一次。

黑色魔法- Method Swizzling_父类_04


父类NSArray执行了第二次Swizzling,这时候就会出现问题,后面会讲具体原因。

黑色魔法- Method Swizzling_3c_05


这样就会导致程序运行过程中,子类调用Swizzling的方法是没有问题的,父类调用同一个方法就会发现Swizzling失效了…..具体原因我们后面讲!

还有一个原因就是因为代码逻辑导致Swizzling代码被执行了多次,这也会导致Swizzling失效,其实原理和上面的问题是一样的,我们下面讲讲为什么会出现这个问题。

问题原因

我们上面提到过Method Swizzling的实现原理就是对类的Dispatch Table进行操作,每进行一次Swizzling就交换一次SELIMP(可以理解为函数指针),如果Swizzling被执行了多次,就相当于SELIMP被交换了多次。这就会导致第一次执行成功交换了、第二次执行又换回去了、第三次执行…..这样换来换去的结果,能不能成功就看运气了,这也是好多人说Method Swizzling不好用的原因之一。

黑色魔法- Method Swizzling_#import_06

从这张图中我们也可以看出问题产生的原因了,就是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本质上就是函数指针,所以我们可以通过打印函数指针的方式,查看SELIMP的交换流程。

先来一段测试代码:

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可以对这个类的Dispatch Table进行操作,操作后的结果对所有当前类及子类都会产生影响,所以有人认为Method Swizzling是一种危险的技术,用不好很容易导致一些不可预见的bug,这些bug一般都是非常难发现和调试的。

这个问题可以引用念茜大神的一句话:使用 Method Swizzling 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。

标签:魔法,method,fromMethod,Swizzling,SEL,toMethod,Method
From: https://blog.51cto.com/u_14682436/6370342

相关文章

  • java.lang.IllegalArgumentException: Invalid character found in method name [toke
    这个问题是本地用了https,只要将https改为http就可以解决。  参考:https://blog.csdn.net/weixin_44299027/article/details/109474606https://blog.csdn.net/jcmj123456/article/details/124002200......
  • P2801 教主的魔法
    点击查看代码#include<bits/stdc++.h>#definels(k<<1)#definers(k<<1|1)#definemid(l+r>>1)#defineintlonglongusingnamespacestd;intn,m;charopt;constintN=1e6+7;ints[N<<2],a[N],lazy[N<<2];intmaxx[N<<2......
  • ASP.NET MVC WebAPI Put和Delete请求出现405(Method not allowed)错误
    解决办法:在站点根目录下的web.config设置如下(主要参考添加项):<system.webServer></system.webServer>(End)转自:https://www.bbsmax.com/A/qVdepEM85P/......
  • Numpy_矩阵的multiply_python的属性以及类特性_装饰器——@property_@classmethod_@st
    Python类中有三个常用的装饰器分别是@property(使一个方法可以被当成属性调用,常用于直接返回某一不想被修改的属性)@classmethod(将一个方法定义为类方法,其中第一个参数要修改为cls,使得该方法可以不用实例化即可被调用)@staticmethod(静态方法,类似于类方法,也可以不用实例化,......
  • python类的静态方法@staticmethod
    要在类中使用静态方法,需在类成员方法前加上“@staticmethod”标记符,以表示下面的成员方法是静态方法。使用静态方法的好处是,不需要实例化对象即可使用该方法。  静态方法可以不带任何参数,由于静态方法没有self参数,所以它无法访问类的实例成员;静态方法也没有cls参数,所以它也无法......
  • 题解(教主的魔法)P2801
    题目教主的魔法题目描述教主最近学会了一种神奇的魔法,能够使人长高。于是他准备演示给XMYZ信息组每个英雄看。于是$N$个英雄们又一次聚集在了一起,这次他们排成了一列,被编号为$1,2,\ldots,N$。每个人的身高一开始都是不超过$1000$的正整数。教主的魔法每次可以把闭区......
  • 《花雕学AI》语言+想象+人工智能=图像魔法:微软 Bing 图像魔法师的功能、价值和评测
    你有没有想过,如果你能够用语言来创造图像,那该有多么神奇和有趣?你有没有想过,如果你能够看到你想象中的图像,那该有多么震撼和美妙?现在,这一切都可以实现了,因为微软Bing图像魔法师来了!微软Bing图像魔法师是一款能够根据用户的描述生成图像的人工智能产品,它可以让你的语言变成视觉,......
  • 多线程合集(三)---异步的那些事之自定义AsyncTaskMethodBuilder
    引言之前在上一篇文章中多线程合集(二)---异步的那些事,async和await原理抛析,我们从源码去分析了async和await如何运行,以及将编译后的IL代码写成了c#代码,以及实现自定义的Awaiter,自定义异步状态机同时将本系列的第一篇文章的自定义TaskScheduler和自定义的Awaiter......
  • springboot项目启动报错java.lang.NoSuchMethodError: org.springframework.boot.buil
    产生此问题的原因是由于springboot版本兼容性导致的:java.lang.NoSuchMethodError:org.springframework.boot.builder.SpringApplicationBuilder.<init>([Ljava/lang/Object;)V2019-08-2918:04:54.089ERROR[restartedMain][SpringApplication.java:842]-Applicationrunfail......
  • 【老王读SpringMVC-5】Controller method 是如何执行的?
    通过前面对Controllermethod参数绑定的分析,我们知道,被@RequestMapping标记handlermethod的执行是通过调用RequestMappingHandlerAdapter#handle()。RequestMappingHandlerAdapter#handle()具体的调用过程如下:参数解析、handlermethod的执行和对返回值的处理,最终......