黑色魔法- Method Swizzling

简介: 由于最近开发新版本,就避免不了在开发和调试过程中引起崩溃,以及诱发一些之前的bug导致的崩溃。而且项目比较大也很不好排查,正好想起之前研究过的`Method Swizzling`,考虑是否能用这个苹果的“黑魔法”解决问题,当然用好这个黑魔法并不局限于解决这些问题.

这里写图片描述

由于最近开发新版本,就避免不了在开发和调试过程中引起崩溃,以及诱发一些之前的bug导致的崩溃。而且项目比较大也很不好排查,正好想起之前研究过的`Method Swizzling`,考虑是否能用这个苹果的“黑魔法”解决问题,当然用好这个黑魔法并不局限于解决这些问题....

小编自己有一个学习交流群,里面都是有多年开发经验的iOS大牛在里面交流,有需要的伙伴可以进来一起交流群号(681503716)(验证编码:大鲨),不定时也会分享ARKit技术,移动架构,支付宝,底层,高级进阶学习不等的视频教程资料
AI 代码解读

开发需求

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

- 手动添加

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

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

- 继承

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

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

- Category

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

- Method Swizzling

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

Method Swizzling

原理

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

这里写图片描述

使用注意

类簇设计模式

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

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

下面列举了NSArrayNSDictionary本类的类名,可以通过Runtime函数取出本类。
这里写图片描述

注意要点

  • 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);
AI 代码解读

错误剖析

在上面的例子中,如果只是单独对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);
}
AI 代码解读

这里由于在子类中调用了super,导致NSMutableArray执行时,父类NSArray也被执行了一次。
这里写图片描述
父类NSArray执行了第二次Swizzling,这时候就会出现问题,后面会讲具体原因。
这里写图片描述
这样就会导致程序运行过程中,子类调用Swizzling的方法是没有问题的,父类调用同一个方法就会发现Swizzling失效了…..具体原因我们后面讲!

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

问题原因

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

这里还要告诉大家一个调试小技巧,已经知道的可以略过。我们之前说过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));
AI 代码解读

看到这个打印结果,大家应该明白什么问题了吧:

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
AI 代码解读

Method Swizzling危险吗?

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

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

目录
打赏
0
0
0
0
1
分享
相关文章
开源云原生数据库PolarDB PostgreSQL 15兼容版本正式发布
PolarDB进行了深度的内核优化,从而实现以更低的成本提供商业数据库的性能。
|
10月前
|
【文件上传绕过】——条件竞争漏洞
【文件上传绕过】——条件竞争漏洞
267 5
深入解析OpenStack Cinder:块存储服务详解
本文介绍了OpenStack及其块存储服务Cinder。OpenStack是一个开源云计算管理平台,提供基础设施即服务(IaaS),核心服务包括计算、网络、存储等。Cinder主要用于为虚拟机提供持久性块存储,具备多种功能,如卷操作、备份、快照及与实例的交互等。此外,还详细介绍了Cinder的工作流程、命令行操作及不同存储插件的使用。
1275 8
Hibernate - Java 类型, Hibernate 映射类型及 SQL 类型之间的对应关系
Hibernate - Java 类型, Hibernate 映射类型及 SQL 类型之间的对应关系
166 0
阿里云ECS经典网络和专有网络有什么区别?
阿里云面向客户提供的网络类型服务有经典网络和专有网络两种,但这两者有什么区别呢?阿里官网给的解释是: 经典网络:IP地址由阿里云统一分配,配置简便,使用方便,适合对操作易用性要求比较高、需要快速使用 ECS 的用户。
25activiti - 流程变量(设置和获取流程变量)
25activiti - 流程变量(设置和获取流程变量)
92 0
Dreamweaver CS6破解教程[序列号+破解补丁
Adobe Dreamweaver CS6中文简体版下载地址:Dreamweaver CS6中文简体版下载[带破解] 该教程将不断更新,最新版请猛击:http://www.itxueyuan.org/view/6317.html 破解之前的准备 1) 序列号这里为大家生成了两个,可以通过软件验证:1325-0949-2080-9819-3777-32301325-0160-5283-9851-2671-89512) 破解补丁安装时会用到,请大家提前下载好(安装时需要断网)。
2453 0
微信团队分享:微信后台在海量并发请求下是如何做到不崩溃的
本文分享了微信基于大规模微服务架构的后台过载管控和保护策略,以及微信根据IM业务特点的一些独特的架构设计做法,其中很多方法很有借鉴意义,值得一读。
562 0
微信团队分享:微信后台在海量并发请求下是如何做到不崩溃的
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问