iOS
Swift
swift4

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

More than 1 year has passed since last update.

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のマイグレーターがよしなにやってくれるので、自分のプロジェクトではここら辺は何も考える必要がなかった