iOS开发Swift

Swift 中@Objc以及Dynamic的使用

@Objc


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 里使用这个类了

Objective-C 中所有类都继承自NSObject,Swift 中的类如果要供 Objective-C 调用,必须也继承自NSObject。

@objc修饰符的根本目的是用来暴露接口给 Objective-C 的运行时(类、协议、属性和方法等)

添加@objc修饰符并不意味着这个方法或者属性会采用 Objective-C 的方式变成动态派发,Swift 依然可能会将其优化为静态调用

@objc 修饰符的隐式添加:

Swift 3 中继承自NSObject的类,不需要手动添加@objc,编译器会给所有的非private的类和成员加上@objc,private接口想要暴露给 Objective-C 需要@objc的修饰

button.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside)
@objc private func backButtonTapped() { }
func backButtonTapped() { }

Swift 4 中继承自NSObject的类的隐式@objc自动添加,只会发生在以下四种情况:

  • 1.重写了父类的 Objective-C 方法
  • 2.实现了一个 Objective-C 的协议
  • 3.@IBAction或@IBOutlet关键字的修饰
  • 4.@NSManaged关键字的修饰

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

class我的类:NSObject{
         func 打招呼(名字:String){
          print("哈喽,\(名字)")
}}

我的类().打招呼("小明")

另外,正如上面所说的以及在Selector一节中所提到的,即使是NSObject的子类,Swift 也不会在被标记为private的方法或成员上自动加@objc,以保证尽量不使用动态派发来提高代码执行效率。

如果我们确定使用这些内容的动态特性的话,我们需要手动给它们加上@objc修饰.
但是需要注意的是,添加@objc修饰符并不意味着这个方法或者属性会变成动态派发,Swift 依然可能会将其优化为静态调用。
如果你需要和 Objective-C 里动态调用时相同的运行时特性的话,你需要使用的修饰符是dynamic。

一般情况下在做 app 开发时应该用不上,但是在施展一些像动态替换方法或者运行时再决定实现这样的 “黑魔法” 的时候,我们就需要用到dynamic修饰符了。
KVO一节中,我们提到了一个关于使用dynamic的实例。
关于 Swift 和 Objective-C 混用的一个好消息是,随着 Swift 的发展,Apple 正在努力改善 SDK。
在 Objective-C 中添加的nonnull和nullable,以及泛型的数组和字典等,其实上都是为了使 SDK 更加适合用 Swift 来使用所做的努力,我们还是很有希望在不久的未来能够摆脱掉这些妥协和束缚的。

@NSManaged的定义


Core Data 提供了基本存储和实现NSManagedObject子类的一组属性。在与Core Data 模型中管理对象子类相关的特性或者关系的每个属性定义之前,将@NSmanaged特性加入。与 Objective-C 里面的 @dynamic特性类似,@NSManaged特性告知 Swift 编译器,这个属性的存储和实现将在运行时完成。但是,与@dynamic不同的是,@NSManaged特性仅在 Core Data 支持中可用。

使用@objc可以修改 Swift 接口暴露到 Objective-C 后的名字

@objc(Squirrel)
class Белка: NSObject {
    @objc(color)
    var цвет: Цвет = .Красный
 
    @objc(hideNuts:inTree:)
    func прячьОрехи(количество: Int, вДереве дерево: Дерево) { }
}

@objcMembers


使用@objcMembers关键字,将类中的所有方法暴露给Objc (效果等同于为所有方法加上@objc)。

为什么要用这个关键字呢?

@objcMembers 在Swift 4中继承 NSObject 的 swift class 不再默认全部 bridge 到 OC,如果我们想要使用的话我们就需要在class前面加上@objcMembers 这么一个关键字。
引用: 在 swift 3 中除了手动添加 @objc 声明函数支持 OC 调用还有另外一种方式:继承 NSObject。
class 继承了 NSObject 后,编译器就会默认给这个类中的所有函数都标记为 @objc ,支持 OC 调用。
苹果在Swift 4 中苹果修改了自动添加 @objc 的逻辑: 一个继承 NSObject 的 swift 类不再默认给所有函数添加 @objc。
只在实现 OC 接口和重写 OC 方法时才自动给函数添加 @objc 标识。
Swift4 后继承自NSObject的类不再隐式添加@objc关键字,但在某些情况下非常依赖 Objective-C 的运行时(如 XCTest),所以在 Swift4 中提供了@objcMembers关键字,对类和子类、扩展和子类扩展重新启用@objc推断。

@objcMembers
class MyClass : NSObject {
  func foo() { }             // implicitly @objc
 
  func bar() -> (Int, Int)   // not @objc, because tuple returns
      // aren't representable in Objective-C
}
 
extension MyClass {
  func baz() { }   // implicitly @objc
}
 
class MySubClass : MyClass {
  func wibble() { }   // implicitly @objc
}
 
extension MySubClass {
  func wobble() { }   // implicitly @objc
}

使用@objc和@nonobjc可以指定开启或关闭某一extension中的所有方法的@objc推断。

class SwiftClass { }
 
@objc extension SwiftClass {
  func foo() { }            // implicitly @objc
  func bar() -> (Int, Int)  // error: tuple type (Int, Int) not
      // expressible in @objc. add @nonobjc or move this method to fix the issue
}
 
@objcMembers
class MyClass : NSObject {
  func wibble() { }    // implicitly @objc
}
 
@nonobjc extension MyClass {
  func wobble() { }    // not @objc, despite @objcMembers
}

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关键字,否则有可能程序崩溃。

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

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

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

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

补充的一些例子


在 Swift 中很多地方都是用到了一个修饰符@objc,尤其是在混编项目中,出于安全的考虑,可以将需要暴露给 Objective-C 使用的如类,属性和方法的声明前面加上 @objc。那么在 Swift 中哪些地方用到了这个关键字呢?

  • Selector 中调用的方法需要在方法前声明 @objc,目的是允许这个函数在“运行时”通过 Objective-C 的消息机制调用
override func viewDidLoad() {
    super.viewDidLoad()
    let btn = UIButton(type: .contactAdd)     
    btn.addTarget(self, action: #selector(click), for: .touchUpInside)
}
@objc func click()  {      
    print("Button clicked")
}
  • 协议的方法可选时,协议和可选方法前要用 @objc声明
@objc protocol OptionalProtocol {
    @objc optional func optionalMethold1()
    @objc optional func optionalMethold2()
}
  • 用weak修饰协议时,协议前面要用@objc声明
@objc protocol MyDelegate{   
    func methold1()  
}

class MyClass{
    weak var delegate: MyDelegate?  
}
  • 类前加上 @objcMembers,那么它及其子类、扩展里的方法都会隐式的加上 @objc
@objcMembers
class Person {

}
  • 如果此时在扩展里面不想加@objc,可以用@nonobjc修饰
@objcMembers
class Person {  
    func work(){}
}

@nonobjc extension Person{
    func eat() { } //包含隐式的 @objc
    func sleep() { } //包含隐式的 @objc
}
  • 扩展前加上 @objc,那么里面的方法都会隐式加上 @objc
class Person {  
    func work(){}
}
@objc extension Person{
    func eat() { } //包含隐式的 @objc
    func sleep() { } //包含隐式的 @objc
}