-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
151 additions
and
0 deletions.
There are no files selected for viewing
151 changes: 151 additions & 0 deletions
151
...onSet/如何在 Swift 中高效地使用 Method Swizzling(Effective Method Swizzling in Swift).md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
# 如何在 Swift 中高效地使用 Method Swizzling | ||
|
||
--- | ||
原文链接:[Effective Method Swizzling in Swift](https://www.uraimo.com/2015/10/23/effective-method-swizzling-with-swift/) | ||
|
||
示例程序可以从 [Github](https://github.com/uraimo/SwizzlingInSwift/) 上下载。 | ||
|
||
Method Swizzling 在 Objective-C 和其他语言中是很有名的一个技术,该技术支持动态方法派发。 | ||
|
||
Method Swizzling 通过改变特定 selector(方法)与实际实现的映射,可以将一个方法的实现在 *runtime* 时被替换成其它的方法实现。 | ||
|
||
 | ||
|
||
虽然这看起来非常方便,但是这个功能也有其缺点。在 runtime 执行这类更改时,你就不能在编译时利用那些可用的安全检查了。因此,Method Swizzling 应该小心使用。 | ||
|
||
[NSHipster](http://nshipster.com/method-swizzling/) 上有一篇关于如何在 Objective-C 中使用 Method Swizzling 的文章(译者注:[南峰子的技术博客](http://southpeak.github.io/blog/2014/11/06/objective-c-runtime-yun-xing-shi-zhi-si-:method-swizzling/)有这篇文章的译文)(其他详情也可以看[这里](https://www.mikeash.com/pyblog/friday-qa-2010-01-29-method-replacement-for-fun-and-profit.html))以及 [Stackoverflow](http://stackoverflow.com/q/5339276) 上有一些如何使用 Method Swizzling 的讨论。 | ||
|
||
Swift 关于方法派发是使用静态方法的,但有些情形可能需要用到 Method Swizzling。 | ||
|
||
在 Swift 中使用 Method Swizzling 之前,让我再次重申一下这种技术还是尽量少用,只有当你的问题不能用 swift 的方式解决,也不能用子类或协议或扩展解决时才使用。 | ||
|
||
正如 NSHipster 上[另一篇文章](http://nshipster.com/swift-objc-runtime/)描述的那样,在 Swift 中使用 Method Swizzling 的方式 swizzle 一个继承自基本框架(Foundation、UIKit 等)的类与 Objective-C 没什么区别。 | ||
|
||
```swift | ||
extension UIViewController { | ||
public override static func initialize() { | ||
struct Static { | ||
static var token: dispatch_once_t = 0 | ||
} | ||
|
||
// make sure this isn't a subclass | ||
if self !== UIViewController.self { | ||
return | ||
} | ||
|
||
dispatch_once(&Static.token) { | ||
let originalSelector = Selector("viewWillAppear:") | ||
let swizzledSelector = Selector("newViewWillAppear:") | ||
|
||
let originalMethod = class_getInstanceMethod(self, originalSelector) | ||
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) | ||
|
||
let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) | ||
|
||
if didAddMethod { | ||
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)) | ||
} else { | ||
method_exchangeImplementations(originalMethod, swizzledMethod); | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Method Swizzling | ||
|
||
func newViewWillAppear(animated: Bool) { | ||
self.newViewWillAppear(animated) | ||
if let name = self.descriptiveName { | ||
print("viewWillAppear: \(name)") | ||
} else { | ||
print("viewWillAppear: \(self)") | ||
} | ||
} | ||
} | ||
``` | ||
|
||
在这个例子中,应用中的每个 UIViewController 都会执行额外的操作,而原始的 `viewWillAppear` 方法会被保护起来不执行,这种情形只能通过 Method Swizzling 来实现。 | ||
|
||
`viewWillAppear` 方法的实现会被替换成 `initialize` 中 `newViewWillAppear` 方法的实现。值得注意的是,在 swizzle 后,会递归调用 `newViewWillAppear` 方法,以替换之前原始的 `viewWillAppear` 方法。 | ||
|
||
与 Objective-C 的第一个不同之处就是 swizzle 改在 `load` 方法里执行。 | ||
|
||
加载一个类的定义时,会调用 `load` 方法,这地方执行 swizzle 最好了。 | ||
|
||
但是 `load` 方法只在 Objective-C 里有,而且不能在 Swift 里重载,不敢怎么试都会报编译错误。接下来执行 swizzle 最好的地方就是 `initialize` 了,这是调用你的类中第一个方法前的地方。 | ||
|
||
所有对修改方法执行的操作放在 `dispatch_once` 中都是为了确保这个过程只执行一次。 | ||
|
||
这些都是你要知道的关于继承自基本功框架或 Objective-C 桥接类的。如果你要在纯 Swift 类中正确使用 Method Swizzling 的话,就需要将下面这些注意事项记在心上。 | ||
|
||
## Swift 类中使用 Method Swizzling | ||
|
||
要在 Swift 类中使用 Method Swizzling 有两个必要条件: | ||
|
||
* 包含 swizzle 方法的类需要继承自 NSObject | ||
|
||
* 需要 swizzle 的方法必须有动态属性(dynamic attribute) | ||
|
||
更多关于为什么必须需要这些可以在苹果的[文档](https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html#//apple_ref/doc/uid/TP40014216-CH4-XID_38)里查看: | ||
|
||
需要动态派发 | ||
|
||
当 @objc 属性将你的 Swift API 暴露给 Objective-C runtime 时,不能确保属性、方法、初始器的动态派发。Swift 编译器可能为了优化你的代码,而绕过 Objective-C runtime。当你指定一个成员变量 为 dynamic 时,访问该变量就总会动态派发了。因为在变量定义时指定 dynamic 后,会使用 Objective-C runtime 来派发。 | ||
|
||
动态派发是必须的。但是,你必须使用 dynamic 修饰才能知道在运行时 API 的实现被替换了。举个例子,你可以在 Objective-C runtime 使用 `method_exchangeImplementations` 方法来交换两个方法的实现。假如 Swift 编译器将方法的实现内联(inline)了或者访问去虚拟化(devirtualize)了,这个交换过来的新的实现就无效了。 | ||
|
||
说白了,如果你想要替换的方法没有声明为 dynamic 的话,就不能 swizzle。 | ||
|
||
翻译成代码就是这样: | ||
|
||
```swift | ||
class TestSwizzling : NSObject { | ||
dynamic func methodOne()->Int{ | ||
return 1 | ||
} | ||
} | ||
|
||
|
||
extension TestSwizzling { | ||
|
||
//In Objective-C you'd perform the swizzling in load() , but this method is not permitted in Swift | ||
override class func initialize() | ||
{ | ||
struct Static | ||
{ | ||
static var token: dispatch_once_t = 0; | ||
} | ||
|
||
// Perform this one time only | ||
dispatch_once(&Static.token) | ||
{ | ||
let originalSelector = Selector("methodOne"); | ||
let swizzledSelector = Selector("methodTwo"); | ||
|
||
let originalMethod = class_getInstanceMethod(self, originalSelector); | ||
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector); | ||
|
||
method_exchangeImplementations(originalMethod, swizzledMethod); | ||
} | ||
} | ||
|
||
func methodTwo()->Int{ | ||
// It will not be a recursive call anymore after the swizzling | ||
return methodTwo()+1 | ||
} | ||
} | ||
|
||
var c = TestSwizzling() | ||
print(c.methodOne()) //2 | ||
print(c.methodTwo()) //1 | ||
``` | ||
|
||
这个简单的例子中,在 `TestSwizzling` 对象第一个方法被调用前,`methodOne` 和 `methodTwo` 的实现被相互替换了。 | ||
|
||
## 去掉注释 | ||
|
||
正如你看到的,在 Swift 中还是可以使用 Method Swizzling 的,只是我的意见是,在实际的产品代码里大多数时候还是用不到的。如果你想通过 swizzle 快速解决问题的话,那更好的办法就是想出一个更好的架构,重构你的代码。 | ||
|
||
|
||
|
||
|
||
|