Method Swizzling and Swift:しかし、ニュアンスがあります

便宜上、フレームワークのバグを回避するために、そして絶望のために、誰かが作成したクラスメソッドの動作を再定義する必要がある場合があります。 メソッドスウィズリングを使用すると、元の実装を利用可能なまま、メソッドをランタイムで直接メソッドに置き換えることができます。

Objective-Cランタイムの記事 このプロセスの理論と実際の応用はよく説明されています。 しかし、Swiftへの移行に伴い、いくつかのニュアンスがあります。

例から始めましょう。 サードパーティのChartsフレームワークにChartRendererクラスがあり、その中にdrawDotsメソッドがあり、チャート上の節点に円を描くとします。 そして、円ではなく、アスタリスクが必要です。 そして、フレームワークの内部に登りたくはありません。更新が時々出て、それらを失うのは残念だからです。

そのため、 ChartRendererクラスの拡張機能を作成します。これにより、ランタイムでdrawDotsメソッドがdrawStarsメソッドに直接置き換えられます。 drawStarsメソッドがdrawDotsメソッドの呼び出しに応答するたびにChartsの他のオブジェクトがChartRendererにアクセスする方法を心配する必要はありません。

extension ChartRenderer { public override class func initialize() { struct Static { static var token: dispatch_once_t = 0 } // ,     if self !== UIViewController.self { return } dispatch_once(&Static.token) { let originalSelector = Selector("drawDots:") let swizzledSelector = Selector("drawStars:") 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) } } } //  func drawStars(color: CGColor) { //  // ,     drawStars(...), //    drawDots(...),    "drawStars:" } } 

Objective-Cのすべての資料では、メソッドは初期化()ではなくload()で置き換える必要があると書いています。これは、最初のメソッドはクラスに対して1回だけ呼び出され、2番目はすべてのサブクラスに対して呼び出されるためです ただし、Swiftを使用する場合のランタイムはload()をまったく呼び出さないため、このinitialize()が子孫ではなくクラスによって呼び出され、 dispatch_onceで 1回だけ置換が行われるようにする必要があります。

シンプルではあるが視覚的ではない代替オプションがあることに注意してください:拡張の代わりに、 アプリケーションの AppDelegateでこれらのすべての操作を実行ます(_:didFinishLaunchingWithOptions :)

ChartRendererが Objective-Cで記述されている場合、すべてがすでにそのように動作するはずです。 良い記事はNSHipsterにあります。

次に、ニュアンスについて説明します。 プロジェクトと、置き換え可能なメソッドを含むクラスの両方がSwiftで記述されている場合にメソッドスウィズリングを使用する場合、これは2つの要件を満たす必要があります。


このようなもの:

 class ChartRenderer: NSObject { dynamic func drawDots() { //  } } 

このニュアンスの性質については、記事「 CocoaおよびObjective-CでSwiftを使用する」で説明されています。
動的ディスパッチが必要

objc属性はSwift APIをObjective-Cランタイムに公開しますが、プロパティ、メソッド、サブスクリプト、または初期化子の動的なディスパッチを保証するものではありません。 Swiftコンパイラは、Objective-Cランタイムをバイパスして、コードのパフォーマンスを最適化するために、メンバーアクセスを仮想化またはインライン化する場合があります。 dynamic修飾子でメンバー宣言をマークすると、そのメンバーへのアクセスは常に動的にディスパッチされます。 dynamic修飾子でマークされた宣言は、Objective-Cランタイムを使用してディスパッチされるため、暗黙的にobjc属性でマークされます。
動的なディスパッチを要求する必要はほとんどありません。 ただし、APIの実装が実行時に置き換えられることがわかっている場合は、動的修飾子を使用する必要があります。 たとえば、Objective-Cランタイムでmethod_exchangeImplementations関数を使用して、アプリの実行中にメソッドの実装を交換できます。 Swiftコンパイラーがメソッドの実装をインライン化するか、メソッドへの仮想化アクセスを行った場合、新しい実装は使用されません。


上記の記事に加えて、英語で問題が特定のBartek Hugoによってここで説明されています

Source: https://habr.com/ru/post/J274545/


All Articles