Swift4 [SE-160 Limiting @objc inference] 概要

  • 38
    いいね
  • 0
    コメント

SE-160 Limiting @objc inferenceの概要

TL;DR

  • Objective-Cランタイムから呼ぶ必要があるものは明示的に@objcの付与が必要になった(今まではコンパイラが勝手につけていた)
  • @objcをクラス全体に適用する@objcMembersが追加になった
  • いらない内部コードが減ってバイナリサイズが6-8%減る
  • マイグレーションとかはXcodeのマイグレータでほぼOK

前提

Swift3では@objc推論が多く行われていた
(@objcとはSwiftのコードをObjective-Cランタイムから呼べるようにするためのもの)
以下の場合に暗黙的に@objc推論が行われている

  • Objective-Cのクラスを継承している
  • dynamicキーワードを付与している変数やメソッド
  • etc.

提案された理由

  • @objc推論される規則がわかりにくい
  • Objective-Cのセレクターの衝突が起こりがち
class MyNumber : NSObject {
  init(_ int: Int) { }
  init(_ double: Double) { } // error: initializer 'init' with Objective-C selector 'init:'
      // conflicts with previous declaration with the same Objective-C selector
}
class MyNumber : NSObject {
  @objc(initWithInteger:) init(_ int: Int) { }
  @objc(initWithDouble:) init(_ double: Double) { }
}
  • SwiftコンパイラはObjective-C Calling ConventionからSwift Calling Conventionを関連づけるthunkメソッドをObjective-Cメタデータ内に記録するためバイナリサイズが大きくなってしまう
    • Objective-C entry pointごとにコストがかかる
    • thunkメソッドにより6-8%のバイナリサイズが大きくなる(未使用のものも含まれている) Apple's Music Appでは5.7%サイズが減った

提案

プログラムの整合性上、必要な部分を除き@objc推論をしないようにする

その上で広範囲で@objcをon/offする時に定型記述を減らすためにclassレベル、extensionレベルのアノテーションを追加する

引き続き推論されるパターン

  • @objc属性がついているものをオーバーライドする場合
    • Super.foo()に対するObjective-C callersがオーバーライドしたSub.foo()を適切に呼び出すため
class Super {
    @objc func foo() { }
}

class Sub : Super {
    /* inferred @objc */
    override func foo() { }
}
  • @objc属性がついているprotocol要件を満たす時
    • MyDelegate.bar()がObjective-C message sendを介して呼ばれるため
@objc protocol MyDelegate {
    func bar()
}

class MyClass : MyDelegate {
    /* inferred @objc */
    func bar() { }
}
  • @IBAction,@IBOutlet属性がついている時
    • Interface BuilderとのインタラクションがObjective-Cランタイムで行われるため
  • @NSManaged属性がついている時
    • Core DataとのインタラクションがObjective-Cランタイムで行われるため

追加で推論されるパターン

  • @GKInspectable属性がついている時
    • GameplayKitとのインタラクションがObjective-Cランタイムで行われるため
  • @IBInspectable属性がついている時
    • Interface BuilderとのインタラクションがObjective-Cランタイムで行われるため

推論されなくなるパターン

  • dynamicキーワードが付与されているもの
    • dynamicは動的ディスパッチをするためのもの(Objective-Cランタイムからよびだされる)なのでSwift3の時は@objcが推論されていた
class MyClass {
  dynamic func foo() { }       // error: 'dynamic' method must be '@objc'
  @objc dynamic func bar() { } // okay
}
  • NSObject由来のもの
    • これがこの提案で唯一の大きな変更
    • これにより多くのメソッドをObjective-Cから呼べなくなる(これによりバイナリサイズが小さくなる)
class MyClass : NSObject {
  func foo() { } // not exposed to Objective-C in Swift 4
}

Swift3時代はObjective-Cで表現できるもののみ@objc推論されていた

extension MyClass {
  func bar(param: ObjCClass) { } // Swift3では推論された。この提案では推論されない
  func baz(param: SwiftStruct) { } // StructはObjective-Cでは表現できないので推論されない
}

クラス全体で@objc推論を有効にさせる(@objcMembers)

XCTest,Realmのようないくつかのライブラリは依然としてObjective-Cランタイムに依存する
そのためクラス、そのExtension、サブクラス、およびそれらすべてのExtensionに対して今まで通りの@objc推論を一括で行うため
@objcMembers属性を新しく追加する。

@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
}

また以下のようにswift_objc_membersを付与することで
インポートされたObjective-Cクラスを@objcMembersとしてSwiftにインポートすることができる

__attribute__((swift_objc_members))
@interface XCTestCase : XCTest
/* ... */
@end

@objcMembers
class XCTestCase : XCTest { /* ... */ }

Extensionでの@objcのon/off

Extensionに@objc or @nonobjc属性をつけることで
そのExtension内の@objcor@nonobjc属性を個別で持ってないメンバーに対して一律適用できる

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
}

Swift4へのマイグレーション

Step1:
Swift4モードで、@objc推論されている警告の対処をする

Step2:
環境変数SWIFT_DEBUG_IMPLICIT_OBJC_ENTRYPOINTの値を1〜3にしてアプリをテストする

  1. ObjectiveCのエントリポイントへの呼び出しを記録
    ex.***Swift runtime: entrypoint -[MyApp.MyClass foo] generated by implicit @objc inference is deprecated and will be removed in Swift 4
  2. 1 + バックトレースを発行
  3. 2 + クラッシュ

Step3:
この時点でSwift4へマイグレーション可能になる
Swift4でビルドすると、廃止予定のルールに基づき@objc推論されたObjective-Cエントリポイントが削除される

※実際はXcodeのマイグレーターがよしなにやってくれるので、自分のプロジェクトではここら辺は何も考える必要がなかった