Swift和Objective-C如何兼顾?且看@objc和Dynamic,dynamic关键字

来源:互联网 发布:python爬虫设置代理ip 编辑:程序博客网 时间:2024/05/24 06:31

虽然说 Swift 语言的初衷是希望能摆脱 Objective-C 的沉重的历史包袱和约束,但是不可否认的是经过了二十多年的洗礼,Cocoa 框架早就烙上了不可磨灭的 Objective-C 的印记。无数的第三方库是用 Objective-C 写成的,这些积累无论是谁都不能小觑。因此,在最初的版本中,Swift 不得不考虑与 Objective-C 的兼容。

Apple 采取的做法是允许我们在同一个项目中同时使用 Swift 和 Objective-C 来进行开发。其实一个项目中的 Objective-C 文件和 Swift 文件是处于两个不同世界中的,为了让它们能相互联通,我们需要添加一些桥梁。

首先通过添加 {product-module-name}-Bridging-Header.h 文件,并在其中填写想要使用的头文件名称,我们就可以很容易地在 Swift 中使用 Objective-C 代码了。Xcode 为了简化这个设定,甚至在 Swift 项目中第一次导入 Objective-C 文件时会主动弹框进行询问是否要自动创建这个文件,可以说是非常方便。

但是如果想要在 Objective-C 中使用 Swift 的类型的时候,事情就复杂一些。如果是来自外部的框架,那么这个框架与 Objective-C 项目肯定不是处在同一个 target 中的,我们需要对外部的 Swift module 进行导入。这个其实和使用 Objective-C 的原来的 Framework 是一样的,对于一个项目来说,外界框架是由 Swift 写的还是 Objective-C 写的,两者并没有太大区别。我们通过使用 2013 年新引入的 @import 来引入 module:

  1. @import MySwiftKit; 

之后就可以正常使用这个 Swift 写的框架了。

如果想要在 Objective-C 里使用的是同一个项目中的 Swift 的源文件的话,可以直接导入自动生成的头文件 {product-module-name}-Swift.h 来完成。比如项目的 target 叫做 MyApp 的话,我们就需要在 Objective-C 文件中写

  1. #import "MyApp-Swift.h" 

但这只是故事的开始。Objective-C 和 Swift 在底层使用的是两套完全不同的机制,Cocoa 中的 Objective-C 对象是基于运行时的,它从骨子里遵循了 KVC (Key-Value Coding,通过类似字典的方式存储对象信息) 以及动态派发 (Dynamic Dispatch,在运行调用时再决定实际调用的具体实现)。而 Swift 为了追求性能,如果没有特殊需要的话,是不会在运行时再来决定这些的。也就是说,Swift 类型的成员或者方法在编译时就已经决定,而运行时便不再需要经过一次查找,而可以直接使用。

显而易见,这带来的问题是如果我们要使用 Objective-C 的代码或者特性来调用纯 Swift 的类型时候,我们会因为找不到所需要的这些运行时信息,而导致失败。解决起来也很简单,在 Swift 类型文件中,我们可以将需要暴露给 Objective-C 使用的任何地方 (包括类,属性和方法等) 的声明前面加上 @objc 修饰符。注意这个步骤只需要对那些不是继承自 NSObject 的类型进行,如果你用 Swift 写的 class 是继承自 NSObject 的话,Swift 会默认自动为所有的非 private 的类和成员加上 @objc。这就是说,对一个 NSObject 的子类,你只需要导入相应的头文件就可以在 Objective-C 里使用这个类了。

@objc 修饰符的另一个作用是为 Objective-C 侧重新声明方法或者变量的名字。虽然绝大部分时候自动转换的方法名已经足够好用 (比如会将 Swift 中类似 init(name: String) 的方法转换成 -initWithName:(NSString *)name 这样),但是有时候我们还是期望 Objective-C 里使用和 Swift 中不一样的方法名或者类的名字,比如 Swift 里这样的一个类:

  1. class 我的类 {  
  2.     func 打招呼(名字: String) {  
  3.         println("哈喽,\(名字)")  
  4.     }  
  5. }  
  6.  
  7. 我的类().打招呼("小明"

Objective-C 的话是无法使用中文来进行调用的,因此我们必须使用 @objc 将其转为 ASCII 才能在 Objective-C 里访问:

  1. @objc(MyClass)  
  2. class 我的类 {  
  3.     @objc(greeting:)  
  4.     func 打招呼(名字: String) {  
  5.         println("哈喽,\(名字)")  
  6.     }  

我们在 Objective-C 里就能调用 [[MyClass new] greeting:@"XiaoMing"] 这样的代码了 (虽然比起原来一点都不好玩了)。另外,正如上面所说的以及在 Selector 一节中所提到的,即使是 NSObject 的子类,Swift 也不会在被标记为 private 的方法或成员上自动加 @objc。如果我们需要使用这些内容的动态特性的话,我们需要手动给它们加上 @objc 修饰。

添加 @objc 修饰符并不意味着这个方法或者属性会变成动态派发,Swift 依然可能会将其优化为静态调用。如果你需要和 Objective-C 里动态调用时相同的运行时特性的话,你需要使用的修饰符是 dynamic。一般情况下在做 app 开发时应该用不上,但是在施展一些像动态替换方法或者运行时再决定实现这样的 "黑魔法" 的时候,我们就需要用到 dynamic 修饰符了。在之后的 KVO 一节中,我们还会提到一个关于使用 dynamic 的实例。

KVO (Key-Value Observing) 是 Cocoa 中公认的最强大的特性之一,但是同时它也以烂到家的 API 和极其难用著称。和属性观察不同,KVO 的目的并不是为当前类的属性提供一个钩子方法,而是为了其他不同实例对当前的某个属性 (严格来说是 keypath) 进行监听时使用的。其他实例可以充当一个订阅者的角色,当被监听的属性发生变化时,订阅者将得到通知。

这是一个很强大的属性,通过 KVO 我们可以实现很多松耦合的结构,使代码更加灵活和强大:像通过监听 model 的值来自动更新 UI 的绑定这样的工作,基本都是基于 KVO 来完成的。

在 Swift 中我们也是可以使用 KVO 的,但是仅限于在 NSObject 的子类中。这是可以理解的,因为 KVO 是基于 KVC (Key-Value Coding) 以及动态派发技术实现的,而这些东西都是 Objective-C 运行时的概念。另外由于 Swift 为了效率,默认禁用了动态派发,因此想用 Swift 来实现 KVO,我们还需要做额外的工作,那就是将想要观测的对象标记为 dynamic

在 Swift 中,为一个 NSObject 的子类实现 KVO 的最简单的例子看起来是这样的:

class MyClass: NSObject {    dynamic var date = NSDate()}private var myContext = 0class Class: NSObject {    var myObject: MyClass!    override init() {        super.init()        myObject = MyClass()        print("初始化 MyClass,当前日期: \(myObject.date)")        myObject.addObserver(self,            forKeyPath: "date",            options: .New,            context: &myContext)        delay(3) {            self.myObject.date = NSDate()        }    }    override func observeValueForKeyPath(keyPath: String?,            ofObject object: AnyObject?,            change: [String : AnyObject]?,            context: UnsafeMutablePointer<Void>)    {        if let change = change where context == &myContext {            let a = change[NSKeyValueChangeNewKey]            print("日期发生变化 \(a)")        }    }}let obj = Class()

这段代码中用到了一个叫做 delay 的方法,这不是 Swift 的方法,而是本书在延时调用一节中实现的一个方法。这里您只需要理解我们是过了三秒以后在主线程将 myObject 中的时间更新到了当前时间即可。

我们标明了 MyClass 的 date 为 dynamic,然后在一个 Class 的 init 中将自己添加为该实例的观察者。接下来等待了三秒钟之后改变了这个对象的被观察属性,这时我们的观察方法就将被调用。运行这段代码,输出应该类似于:

初始化 MyClass,当前日期: 2014-08-23 16:37:20 +0000日期发生变化 Optional(2014-08-23 16:37:23 +0000)

别忘了,新的值是从字典中取出的。虽然我们能够确定 (其实是 Cocoa 向我们保证) 这个字典中会有相应的键值,但是在实际使用的时候我们最好还是进行一下判断或者 Optional Binding 后再加以使用,毕竟世事难料。

在 Swift 中使用 KVO 有两个显而易见的问题。

首先是 Swift 的 KVO 需要依赖的东西比原来多。在 Objective-C 中我们几乎可以没有限制地对所有满足 KVC 的属性进行监听,而现在我们需要属性有 dynamic 进行修饰。大多数情况下,我们想要观察的类不一定是 dynamic 修饰的 (除非这个类的开发者有意为之,否则一般也不会有人愿意多花功夫在属性前加上 dynamic,因为这毕竟要损失一部分性能),并且有时候我们很可能也无法修改想要观察的类的源码。遇到这样的情况的话,一个可能可行的方案是继承这个类并且将需要观察的属性使用 dynamic 进行重写。比如刚才我们的 MyClass 中如果 date 没有 dynamic 的话,我们可能就需要一个新的 MyChildClass 了:

class MyClass: NSObject {    var date = NSDate()}class MyChildClass: MyClass {    dynamic override var date: NSDate {        get { return super.date }        set { super.date = newValue }    }}

对于这种重载,我们没有必要改变什么逻辑,所以在子类中简单地用 super 去调用父类里相关的属性就可以了。

另一个大问题是对于那些非 NSObject 的 Swift 类型怎么办。因为 Swift 类型并没有通过 KVC 进行实现,所以更不用谈什么对属性进行 KVO 了。对于 Swift 类型,语言中现在暂时还没有原生的类似 KVO 的观察机制。我们可能只能通过属性观察来实现一套自己的类似替代了。结合泛型和闭包这些 Swift 的先进特性 (当然是相对于 Objective-C 来说的先进特性),把 API 做得比原来的 KVO 更优雅其实不是一件难事。Observable-Swift 就利用了这个思路实现了一套对 Swift 类型进行观察的机制,如果您也有类似的需求,不妨可以参考看看。

dynamic关键字

如果您有过OC的开发经验,那一定会对OC中@dynamic关键字比较熟悉,它告诉编译器不要为属性合成getter和setter方法。

Swift中也有dynamic关键字,它可以用于修饰变量或函数,它的意思也与OC完全不同。它告诉编译器使用动态分发而不是静态分发。OC区别于其他语言的一个特点在于它的动态性,任何方法调用实际上都是消息分发,而Swift则尽可能做到静态分发。

因此,标记为dynamic的变量/函数会隐式的加上@objc关键字,它会使用OC的runtime机制。

虽然静态分发在效率上可能更好,不过一些app分析统计的库需要依赖动态分发的特性,动态的添加一些统计代码,这一点在Swift的静态分发机制下很难完成。这种情况下,虽然使用dynamic关键字会牺牲因为使用静态分发而获得的一些性能优化,但也依然是值得的。

class Kraken {    dynamic var imADynamicallyDispatchedString: String    dynamic func imADynamicallyDispatchedFunction() {        //Hooray for dynamic dispatch!    }}

使用动态分发,您可以更好的与OC中runtime的一些特性(如CoreData,KVC/KVO)进行交互,不过如果您不能确定变量或函数会被动态的修改、添加或使用了Method-Swizzle,那么就不应该使用dynamic关键字,否则有可能程序崩溃。


0 0
原创粉丝点击