swift swizzle

6334 ワード

SWIZZLE
王巍(@ONEVCAT)より2015/09/30にリリース
SwizzleはObjective-C実行時の黒魔法の一つです.私たちはSwizzleの手段を通じて、実行時にいくつかの方法の実現を置き換えることができます.これはObjective-CがCocoa開発の中で最も華やかで、最も危険なテクニックの一つでもあります.
Objective-Cはメソッド呼び出し時にクラスのdispatch tableによってselectorでインプリメンテーションを検索するので,実行時にあるselectorに対応するインプリメンテーションを置き換えることができれば,実行時にこのメソッドの動作を「再定義」することができる.よく理解していない場合は、あるクラスが応答できる方法は辞書のような構造に格納されていることを想像することができます.キーは方法の名前(つまりselector)であり、値は方法が本当にしていることです.メソッドを実行するときにObjective-Cの実行時に実行したいメソッドの名前を伝え、この名前を使用してこの「辞書」から値を取り、実行します.ここの値を置き換えることで,元のコード構造を変えずに日替わりすることができる.
一般的にはこのような技術はあまり使われないかもしれませんが、特にシステムフレームワークに触れる必要がある場合に便利です.たとえば、膨大なプロジェクトがあり、UIButtonを多く使用してユーザーをインタラクティブにしています.ある日、製品汪は突然、app全体でユーザーがすべてのボタンをクリックした回数を統計する必要があると言った.技術を全く知らない選手にとって、カウンターを作ってボタンを押すたびに1つ追加すればいいという難しいことではないようです.しかし、コードで生きているすべての人にとって、直面している厳しい問題は、どうすればいいかということです.
もちろん、プロジェクトのすべてのボタンを探してクリックしたイベントコードを探して、グローバルカウンタを作ってカウントすることができますが、その後のメンテナンスはどうすればいいのか、探している間に漏れが発生したらどうすればいいのか、新しく加入した人はどうすればいいのか分かりません.明らかにこれは最悪の道だ.もう1つの方法は、UIButtonのサブクラスを作成し、クリックイベントを書き換える方法です.この戦略は良いですが、プロジェクトのボタンを探して継承関係を変更する必要があります.上記の問題も依然として存在しています.また、プロジェクトで他のUIButtonのサブクラスを使用している場合は、それらのサブクラスのために新しいサブクラスを作成する必要があります.時間と労力がかかります.
こういう時はSwizzzleが活躍する番だ.私たちはグローバル範囲ですべてのUIButtonの送信イベントの方法を交換すれば、この問題を永遠に解決することができます.コードの置換検索がなく、ボタンが漏れず、その後の開発でもこのカウントの機能に特に注意する必要はありません.
Swiftでは、Objective-Cランタイムを利用してSwizzleを行うこともできます.たとえば、上記の例では、次のような拡張機能を使用して実行できます.
extension UIButton { class func xxx_swizzleSendAction() { struct xxx_swizzleToken { static var onceToken : dispatch_once_t = 0 } dispatch_once(&xxx_swizzleToken.onceToken) { let cls: AnyClass! = UIButton.self let originalSelector = Selector("sendAction:to:forEvent:") let swizzledSelector = Selector("xxx_sendAction:to:forEvent:") let originalMethod = class_getInstanceMethod(cls, originalSelector) let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector) method_exchangeImplementations(originalMethod, swizzledMethod) } } public func xxx_sendAction(action: Selector, to: AnyObject!, forEvent: UIEvent!) { struct xxx_buttonTapCounter { static var count: Int = 0 } xxx_buttonTapCounter.count += 1 print(xxx_buttonTapCounter.count) xxx_sendAction(action, to: to, forEvent: forEvent) } } 
xxx_swizzleSendActionメソッド(一般的なクラスにメソッドを追加するため、または万一に備えて接頭辞を付けることが望ましい)では、置換されるメソッド(sendAction:to:forEvent:)とそれを置換するためのメソッド(xxx_sendAction:to:forEvent:)のselectorを取得し、実行時にこれら2つのメソッドの具体的な実装を交換した.xxx_sendAction:to:forEvent:の実装では、まずカウンタを1つ追加し、出力します.最後に私たちはこの方法で自分を呼び出したように見え、死の循環を形成するようです.しかし,我々は実際にsendAction:to:forEvent:xxx_sendAction:to:forEvent:の実装を交換したので,この呼び出しを行う際にちょうど呼び出されたのは元の方法の実装である.同様に,外部でsendAction:to:forEvent:を使用する場合(すなわちボタンをクリックする場合),実際に呼び出される実装は,ここで定義したカウンタ加算付き実装となる.
最後に、app起動時にこのxxx_swizzleSendActionメソッドを呼び出す必要があります.Objective-Cではcategoryの+loadで一般的に完了しているが、SwiftのextensionとObjective-Cのcategoryはやや異なり、extensionは実行時にロードされるものではないため、ロード時に呼び出されるloadのような方法もない.また、extensionでもloadを上書きする方法を書き換えるべきではありません(実際には書き換えも無効です).実際、Swiftが実装したloadは、appの実行開始時に呼び出されたわけではない.これらの理由に基づいて、別のクラスの初期化時に呼び出されるメソッドを使用して交換します.
extension UIButton { override public class func initialize() { if self != UIButton.self { return } UIButton.xxx_swizzleSendAction() } } 
+loadとは異なり、+initializeは、現在のクラスおよびそのサブクラスが初期化されたときに呼び出されます.ここでは,現在のクラスのタイプを判断し,セキュリティを保証した.また、xxx_swizzleSendActionにおいても、1つのonce_を使用するtokenは、交換コードが一度だけ実行されることを保証します.
今、私たちのすべてのボタンイベントは私たちが置き換える方法を歩いて、毎回実際にイベントのボタンを送信して、あなたはコンソールで現在のクリック数の出力を見ることができます.
この方式のSwizzleは、Objective-Cのダイナミックディスパッチを使用しており、NSObjectのサブクラスに対しては直接使用可能であるが、Swiftのクラスについては、Objective-C実行時はデフォルトでは使用されていないため、ダイナミックディスパッチのメソッドリストもないので、Swizzleの方がSwiftタイプのメソッドであれば、動的配布メカニズムを使用する必要があることを示すために、従来の方法および置換方法にdynamicタグを付ける必要がある.この方面の知識については,@objcとdynamicの内容を参照することができる.