swift swizzle
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的更多相关文章
- iOS开发系列--Swift进阶
概述 上一篇文章<iOS开发系列--Swift语言>中对Swift的语法特点以及它和C.ObjC等其他语言的用法区别进行了介绍.当然,这只是Swift的入门基础,但是仅仅了解这些对于使用S ...
- IOS开发之SWIFT进阶部分
概述 上一篇文章<iOS开发系列--Swift语言> 中对Swift的语法特点以及它和C.ObjC等其他语言的用法区别进行了介绍.当然,这只是Swift的入门基础,但是仅仅了解这些对于使用 ...
- Swift进阶
概述 访问控制 Swift命名空间 Swift和ObjC互相调用 Swift和ObjC映射关系 Swift调用ObjC ObjC调用Swift 扩展—Swift调用C 反射 扩展—KVO 内存管理 循 ...
- Swift运行时简介
因为Swift的操作在高层并且也得与Objc联合起来干活,用Swift写的程序一般会被Objc和Swift运行时处理.因为Swift的本性--换句话说,它是一门静态语言--Swift运行时在一些关键地 ...
- Swift 进阶
iOS开发系列--Swift进阶 2015-09-21 00:01 by KenshinCui, 3072 阅读, 12 评论, 收藏, 编辑 概述 上一篇文章<iOS开发系列--Swift语言 ...
- iOS代码规范(OC和Swift)
下面说下iOS的代码规范问题,如果大家觉得还不错,可以直接用到项目中,有不同意见 可以在下面讨论下. 相信很多人工作中最烦的就是代码不规范,命名不规范,曾经见过一个VC里有3个按钮被命名为button ...
- Swift与C#的基础语法比较
背景: 这两天不小心看了一下Swift的基础语法,感觉既然看了,还是写一下笔记,留个痕迹~ 总体而言,感觉Swift是一种前后端多种语言混合的产物~~~ 做为一名.NET阵营人士,少少多多总喜欢通过对 ...
- iOS开发系列--Swift语言
概述 Swift是苹果2014年推出的全新的编程语言,它继承了C语言.ObjC的特性,且克服了C语言的兼容性问题.Swift发展过程中不仅保留了ObjC很多语法特性,它也借鉴了多种现代化语言的特点,在 ...
- 算法与数据结构(十七) 基数排序(Swift 3.0版)
前面几篇博客我们已经陆陆续续的为大家介绍了7种排序方式,今天博客的主题依然与排序算法相关.今天这篇博客就来聊聊基数排序,基数排序算法是不稳定的排序算法,在排序数字较小的情况下,基数排序算法的效率还是比 ...
随机推荐
- solr使用方法 完全匹配
最近一直被solr的搜索困扰,搜索汉字时不能搜索出自己想要的内容,经过研究和查询发现,问题出在没有完全匹配上,主要还是对solr使用不太熟练. 解决方法:以前UserRealname:某某家长,这样搜 ...
- 【转】VS2010中使用AnkhSvn
今天想到要在自己的开发环境IDE(Visual Studio 2010)中安装一个代码管理器的插件,本人在使用VS2005的时候一直都是使用AnkhSvn-2.1.7444.278这版本,使用过程中也 ...
- Poco C++库网络模块例子解析2-------HttpServer
//下面程序取自 Poco 库的Net模块例子----HTTPServer 下面开始解析代码 #include "Poco/Net/HTTPServer.h" //继承自TCPSe ...
- 判断app是否在后台
1.通过RunningTaskInfo类判断(需要额外权限):(测试通过5.1可用,权限名称修改 <uses-permission android:name="android.perm ...
- Android 开发第二天
开发入门HelloWorld 首先打开开发工具 第一步 第二步 效果图 以后可以点击一直下去 第三步骤介绍一下里面项目的作用 SRC是用来保存源代码的东西MainAcrivity.java主视图res ...
- hdu 1563 Find your present!
Find your present! Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Other ...
- Asp.NET获取文件及其路径
[相对路径] Request.ApplicationPath /src Path.GetDirectoryName(HttpContext.Current.Request.RawUrl ) //s ...
- 在Linux下开始C语言的学习
为什么要在linux下学习C语言? linux下可以体验到最纯粹的C语言编程,可以抛出其他IDE的影响 环境配置简单,一条命令就足够.甚至对于大多数linux发行版本,都已经不需要配置C语言的环境 查 ...
- postgresql 行转列,列转行后加入到一个整体数据
这里行转列的基本思想就是使用max,因为其他列下面都是NULL,所以可以Max最后就只能得到有值的这行 普通的查询: SELECT icd , case when (ROW_NUMBER() OVER ...
- import com.sun.image.codec.jpeg.JPEGCodec不通过 Eclipse找不到包
Eclipse默认把这些受访问限制的API设成了ERROR.只要把Windows-Preferences-Java-Complicer-Errors/Warnings里面的Deprecated and ...