SWIZZLE

由 王巍 (@ONEVCAT) 发布于 2015/09/30

Swizzle 是 Objective-C 运行时的黑魔法之一。我们可以通过 Swizzle 的手段,在运行时对某些方法的实现进行替换,这是 Objective-C 甚至说 Cocoa 开发中最为华丽,同时也是最为危险的技巧之一。

因为 Objective-C 在方法调用时是通过类的 dispatch table 来用 selector 对实现进行查找的,因此我们在运行时如果能够替换掉某个 selector 对应的实现,那么我们就能在运行时 “重新定义” 这个方法的行为。如果你不太理解的话,可以想象成某个类能响应的方法是存放在一个类似字典的结构中的,键为方法的名字 (也就是 selector),而值就是方法真正做的事情。执行某个方法时我们告诉 Objective-C 运行时想要执行的方法的名字,然后使用这个名字从这个 “字典” 中取值并执行。通过替换这里的值,我们就可以在不改变原来代码结构的情况下偷天换日了。

一般来说可能不太用得到这样的技术,但是在某些情况下会非常有用,特别是当我们需要触及到一些系统框架的东西的时候。比如我们已经有一个庞大的项目,并使用了很多 UIButton 来让用户交互。某一天,产品汪突然说我们需要统计一下整个 app 中用户点击所有按钮的次数。对于完全不懂技术的选手来说,在他们眼中这似乎不应该是什么难事 -- 只要弄个计数器然后在每次点按钮的时候加一就可以了嘛。但是对于每一个以代码为生的人来说,面临的一个严峻的问题是,这要怎么办。

我们当然可以寻遍项目里的所有按钮点击后的事件代码,然后建立一个全局计数器来计数,但是,之后的维护怎么办,寻找的时候发生了遗漏怎么办,新加入的人不知道这茬怎么办?显然这是最糟糕的一条路。另一个方法是创建一个 UIButton 的子类,然后重写它的点击事件的方法。这种策略虽然好些,但是我们需要找遍项目中的按钮,并改变它们的继承关系,上面的那些问题也依然存在,而且要是我们已经在项目中使用了其他 UIButton 的子类的话,我们就不得不再去为那些子类创建新的子类,费时费力。

这种时候就该轮到 Swizzle 大显身手了。我们在全局范围内将所有的 UIButton 的发送事件的方法换掉,就可以一劳永逸地解决这个问题 -- 没有一段段代码的替换查找,不会遗漏任何按钮,之后开发中也不需要对这个计数的功能特别地注意什么。

在 Swift 中,我们也可以利用 Objective-C 运行时来进行 Swizzle。比如上面的例子,我们就可以使用这样的扩展来完成:

extension UIButton {
class func xxx_swizzleSendAction() {
struct xxx_swizzleToken {
static var onceToken : dispatch_once_t = 0
}
dispatch_once(&xxx_swizzleToken.onceToken) {
let cls: AnyClass! = UIButton.self let originalSelector = Selector("sendAction:to:forEvent:")
let swizzledSelector = Selector("xxx_sendAction:to:forEvent:") let originalMethod =
class_getInstanceMethod(cls, originalSelector)
let swizzledMethod =
class_getInstanceMethod(cls, swizzledSelector) method_exchangeImplementations(originalMethod, swizzledMethod)
}
} public func xxx_sendAction(action: Selector,
to: AnyObject!,
forEvent: UIEvent!)
{
struct xxx_buttonTapCounter {
static var count: Int = 0
} xxx_buttonTapCounter.count += 1
print(xxx_buttonTapCounter.count)
xxx_sendAction(action, to: to, forEvent: forEvent)
}
}

在 xxx_swizzleSendAction 方法 (因为是向一个常用类中添加方法,最好还是加上前缀以防万一) 中,我们先获取将被替换的方法 (sendAction:to:forEvent:) 和用来替换它的方法 (xxx_sendAction:to:forEvent:) 的 selector,然后通过运行时对这两个方法的具体实现进行了交换。在 xxx_sendAction:to:forEvent: 的实现中,我们先将计数器进行加一,然后输出。最后我们看起来是在这个方法中调用了自己,似乎会形成一个死循环。但是因为我们实际上已经交换了sendAction:to:forEvent: 和 xxx_sendAction:to:forEvent: 的实现,所以在做这个调用时恰好调用到的是原来的那个方法的实现。同理,在外部使用 sendAction:to:forEvent: 的时候 (也就是点击按钮的时候),实际调用的实现会是我们在这里定义的带有计数器累加的实现。

最后我们需要在 app 启动时调用这个 xxx_swizzleSendAction 方法。在 Objective-C 中我们一般在 category 的 +load 中完成,但是 Swift 的 extension 和 Objective-C 的 category 略有不同,extension 并不是运行时加载的,因此也没有加载时候就会被调用的类似 load 的方法。另外,extension 中也不应该做方法重写去覆盖 load (其实重写也是无效的)。事实上,Swift 实现的 load并不是在 app 运行开始就被调用的。基于这些理由,我们使用另一个类初始化时会被调用的方法来进行交换:

extension UIButton {
override public class func initialize() {
if self != UIButton.self {
return
}
UIButton.xxx_swizzleSendAction()
}
}

和 +load 不同的是,+initialize 会在当前类以及它的子类被初始化时调用。在这里我们对当前类的类型进行了判断,来保证安全性。另外,在 xxx_swizzleSendAction 中,也使用一个 once_token 来保证交换代码仅会被执行一次。

现在,我们所有的按钮事件都会走我们替换进去的方法了,每点一次实际发送了事件的按钮,你都能在控制台看到当前点击数的输出了。

这种方式的 Swizzle 使用了 Objective-C 的动态派发,对于 NSObject 的子类是可以直接使用的,但是对于 Swift 的类,因为默认并没有使用 Objective-C 运行时,因此也没有动态派发的方法列表,所以如果要 Swizzle 的是 Swift 类型的方法的话,我们需要将原方法和替换方法都加上 dynamic 标记,以指明它们需要使用动态派发机制。关于这方面的知识,可以参看 @objc 和 dynamic 的内容。

swift swizzle的更多相关文章

  1. iOS开发系列--Swift进阶

    概述 上一篇文章<iOS开发系列--Swift语言>中对Swift的语法特点以及它和C.ObjC等其他语言的用法区别进行了介绍.当然,这只是Swift的入门基础,但是仅仅了解这些对于使用S ...

  2. IOS开发之SWIFT进阶部分

    概述 上一篇文章<iOS开发系列--Swift语言> 中对Swift的语法特点以及它和C.ObjC等其他语言的用法区别进行了介绍.当然,这只是Swift的入门基础,但是仅仅了解这些对于使用 ...

  3. Swift进阶

    概述 访问控制 Swift命名空间 Swift和ObjC互相调用 Swift和ObjC映射关系 Swift调用ObjC ObjC调用Swift 扩展—Swift调用C 反射 扩展—KVO 内存管理 循 ...

  4. Swift运行时简介

    因为Swift的操作在高层并且也得与Objc联合起来干活,用Swift写的程序一般会被Objc和Swift运行时处理.因为Swift的本性--换句话说,它是一门静态语言--Swift运行时在一些关键地 ...

  5. Swift 进阶

    iOS开发系列--Swift进阶 2015-09-21 00:01 by KenshinCui, 3072 阅读, 12 评论, 收藏, 编辑 概述 上一篇文章<iOS开发系列--Swift语言 ...

  6. iOS代码规范(OC和Swift)

    下面说下iOS的代码规范问题,如果大家觉得还不错,可以直接用到项目中,有不同意见 可以在下面讨论下. 相信很多人工作中最烦的就是代码不规范,命名不规范,曾经见过一个VC里有3个按钮被命名为button ...

  7. Swift与C#的基础语法比较

    背景: 这两天不小心看了一下Swift的基础语法,感觉既然看了,还是写一下笔记,留个痕迹~ 总体而言,感觉Swift是一种前后端多种语言混合的产物~~~ 做为一名.NET阵营人士,少少多多总喜欢通过对 ...

  8. iOS开发系列--Swift语言

    概述 Swift是苹果2014年推出的全新的编程语言,它继承了C语言.ObjC的特性,且克服了C语言的兼容性问题.Swift发展过程中不仅保留了ObjC很多语法特性,它也借鉴了多种现代化语言的特点,在 ...

  9. 算法与数据结构(十七) 基数排序(Swift 3.0版)

    前面几篇博客我们已经陆陆续续的为大家介绍了7种排序方式,今天博客的主题依然与排序算法相关.今天这篇博客就来聊聊基数排序,基数排序算法是不稳定的排序算法,在排序数字较小的情况下,基数排序算法的效率还是比 ...

随机推荐

  1. editplus如何设置不自动备份

    依次选择:工具,参数设置,文件(默认展开的,要缩回),然后看右边“保存文件时创建备份”,前面的框不要打勾,应用,确定

  2. 密钥,密钥对,公钥,pfx,jks和https的几个概念

    密钥: 我理解是公钥+私钥的统称. 密钥对: 公钥(证书)和私钥成对存在. 通信双方各持有自己的私钥和对方的公钥.自己的私钥需密切保护,而公钥是公开给对方的.在windows下,单独存在的公钥一般是后 ...

  3. BeagleBone Black Linux驱动程序开发入门(0): 开发环境

    搭建arm-linux交叉编译环境的教程有很多,这里只作简要说明.Host宿主机是Ubuntu10.04,我把它装在Windows XP的VirtualBox虚拟机中,这样相当于一台主机有两个操作系统 ...

  4. 3s自动跳转到登陆界面

    cdn资源 Bootstrap是Twitter推出的一个用于前端开发的开源工具包.它由Twitter的设计师Mark Otto和Jacob Thornton合作开发,是一个CSS/HTML框架.Boo ...

  5. js内置函数的使用

    arguments对象是一个参数对象,可以访问有操作和无操作的参数,能够获得每个参数的内容,参数的个数,例如:arguments[0];获第一个参数,arguments.length;获得参数的个数, ...

  6. Optimal Logging

    by Anthony Vallone How long does it take to find the root cause of a failure in your system? Five mi ...

  7. c#常用的一些命名空间

    using System.Collections; 有ArrayList;Hashtable;Stack;Queue;DictionaryEntry;等集合 using System.Data; 访问 ...

  8. 20160417javaweb之servlet监听器

    监听器:监听器就是一个java程序,功能是监听另一个java对象变化(方法调用.属性变更) 8个监听器,分为了3种 写一个类实现响应的接口 注册监听器 -- 在web.xml中注册监听器 1.用来监听 ...

  9. python 脚本查看微信把你删除的好友--win系统版

    PS:目测由于微信改动,该脚本目前不起作用 下面截图来自原作者0x5e 相信大家在微信上一定被上面的这段话刷过屏,群发消息应该算是微信上流传最广的找到删除好友的方法了.但群发消息不仅仅会把通讯录里面所 ...

  10. 学习笔记_第一个strut程序_之中文乱码,过滤器解决方案及过程总结

    1.  第一次碰到加过滤器的过程,就是在学习struct1的时候,中文乱码 几个需要注意的关键字 2.什么叫package 所谓package就是打包的意思,就是说以下程序都是处于这个包内,所以一开始 ...