Method Swizzlingは、実行時に2つのクラスメソッドの実装を入れ替えるランタイムテクニックです。サブクラスを作成したりソースコードを変更したりせずに、システムメソッドの動作をオーバーライドまたは補完できます。このテクニックはObjective-Cを使用したiOS開発で最も広く応用されていますが、Kotlin/Androidでもリフレクションを通じて類似の仕組みが存在します。NSHipster Guide by Mattt, 2024によると、swizzlingはObjective-C Runtimeの最も強力でありながら最も危険なメカニズムの1つです。
重要なポイント
Method Swizzlingは、2つのObjective-Cメソッドの実装を入れ替えるランタイムテクニックです。Swizzling後、originalSelectorを呼び出すとswizzledSelectorのコードが実行され、その逆も同様です。これは、各セレクタ(SEL)がディスパッチテーブルを介して実装(IMP)に関連付けられているObjective-C Runtimeのアーキテクチャのおかげで可能です。このテーブルは実行時に変更できます。
“swizzling”という用語は、2000年代初頭にCocoa開発者コミュニティで導入されました。このテクニックは、AFNetworking(読み込み追跡のためのUIWebViewのswizzling)、Aspects(swizzlingベースのAOPフレームワーク)、FLEX(システムメソッドをswizzleして検査するデバッグツール)などのライブラリによって広く認知されるようになりました。今日では、swizzlingはほとんどのiOSアプリケーションで暗黙的に使用されています。監視および分析ライブラリを通じてです。
Swizzlingの重要な特性はグローバル性です。実装の置換はインスタンスレベルではなくクラスレベルで発生します。ライブラリがUIViewController.viewDidLoadメソッドをswizzleすると、アプリケーション内のすべてのUIViewControllerインスタンス(システムのものも含む)に影響します。これはswizzlingの強み(1行のコードでアプリケーション全体の動作を変えられる)であると同時に、バグの主な原因でもあります。
Objective-C Runtimeは、各クラスにディスパッチテーブルを格納します。これは、キーがSEL(メソッド識別子)、値がIMP(実装関数へのポインタ)の辞書です。アプリケーションがオブジェクトにメッセージを送信すると、objc_msgSendがこのテーブルを線形検索します。Method Swizzlingは、あるSELのIMPを別のSELのIMPで置き換え、呼び出しをリダイレクトします。
// 安全なmethod swizzlingの実装
@implementation NSObject (SafeSwizzle)
+ (void)swizzleClassMethod:(SEL)original
with:(SEL)swizzled {
Class cls = [self class];
SEL originalSel = original;
SEL swizzledSel = swizzled;
Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end
キーとなる関数はmethod_exchangeImplementations(Method, Method)です。これは2つのMethodオブジェクトのIMPをアトミックに交換します。呼び出し後、クラスのディスパッチテーブルが変更されます。originalを呼び出すとswizzledコードが実行され、swizzledを呼び出すとoriginalコードが実行されます。SafeSwizzleカテゴリはこのメソッドをすべてのNSObjectに追加し、任意のクラスがswizzlingを実行できるようにします。
安全なswizzlingの実装には、swizzledバージョン内で元の実装を呼び出す必要があります。そうしなければ、メソッドの元の動作は永久に失われます。正しいパターンは、交換前に元のIMPを保存し、swizzledメソッド内でそれを呼び出すことです:
// 元の実装呼び出しを含むSwizzling
- (void)swizzled_viewDidLoad {
// 1. 元の実装の呼び出し
[self swizzled_viewDidLoad];
// 2. 元の呼び出し後の追加ロジック
NSLog("viewDidLoadが実行されました、swizzlingがアクティブです");
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleClassMethod:@selector(viewDidLoad)
with:@selector(swizzled_viewDidLoad)];
});
}
dispatch_onceは、swizzlingがアプリケーションのライフタイム中に正確に1回だけ実行されることを保証します。同じメソッドを再度swizzlingすると無限再帰が発生します。swizzledメソッドが自分自身を呼び出すことになります。+loadはクラスがランタイムにロードされるときに呼び出されます。これはメインのアプリケーションコードより前に実行されるswizzlingの安全なポイントです。
Objective-Cクラスのディスパッチテーブルは、SEL、IMP、戻り値の型を含むmethod_t構造体の配列です。method_exchangeImplementationsは単にこのテーブル内の2つのIMPポインタを交換します。重要なのは、swizzlingはクラスレベルでのみ機能し、プロトコルレベルでは機能しないことです。メソッドがプロトコルで定義されていても実装されていない場合、ディスパッチテーブルにはswizzlingのエントリがありません。
Swizzlingのオーバーヘッドは最小限です。ディスパッチテーブル内の2つのIMPポインタの交換には数ナノ秒かかります。Swizzling後もメソッドディスパッチは遅くなりません。objc_msgSendはswizzling前と同じO(1)時間でIMPを見つけます。唯一の追加操作は、交換後の最初の呼び出しでのメソッドキャッシュのチェックです。Apple Performance Teamのデータによると、swizzlingはアプリケーションのパフォーマンスに影響しません。
Method Swizzlingは主に3つのシナリオで使用されます。監視と分析(自動イベント送信のためのviewDidLoad、viewDidAppearの追跡)、AOPインターセプション(すべてのメソッド呼び出しのパラメータログ記録)、およびホットフィックス(JSPatchなどのライブラリを使用したApp Store Reviewなしでの本番バグ修正)です。
これらの各シナリオは、swizzlingが集中的に適用されるために機能します。分析ライブラリが+loadで一度swizzlingを実行すると、アプリケーション内のすべてのUIViewControllerインスタンスがイベントを送信し始めます。開発者は各コントローラにコードを追加する必要がありません。これにより重複とエラーのリスクが軽減されます。
Androidでは、クラシックなObjective-Cの意味でのmethod swizzlingは不可能です。Java/Kotlinはvtableを介した静的ディスパッチを使用します。しかし、同様の効果を達成するメカニズムは存在します。実行時の実装置換のためのJava Reflectionと、ビルド時のバイトコード変更のためのGradle Transform API / ASMです。
// Androidでのreflection + companion objectによるSwizzling
class Logger {
companion object {
var originalImpl: (() -> Unit)? = null
}
fun log() {
println("元のログ")
}
}
// リフレクションによる実行時の実装置換
fun swizzleLog() {
val originalMethod = Logger::class.java
.getDeclaredMethod("log")
originalMethod.isAccessible = true
Logger.originalImpl = {
originalMethod.invoke(Logger())
}
// インライン関数による置換
println("swizzled:ログがインターセプトされました")
}
このコードはJava Reflectionを介してlog()メソッドの動作を置き換えます。getDeclaredMethodはプライベート実装にアクセスし、isAccessibleはアクセスチェックを無効にします。log()を直接呼び出す代わりに、追加のロジックを実行するラッパーが呼び出されます。ただし、AndroidはJITを介してホットメソッドを最適化するため、リフレクションは既にコンパイルされたAOTセグメントでは機能しない可能性があります。
より信頼性の高いアプローチは、ASMライブラリを使用したGradle Transform APIまたはAGP(Android Gradle Plugin)を介したバイトコード操作です。バイトコードの変更はコンパイル時に行われます。ASMはクラスの各メソッドに呼び出しを追加します。コードカバレッジツール(JaCoCo)やパフォーマンス監視ツール(Firebase Performance Monitoring)はこのように動作します。
Method Swizzlingは高リスクのテクニックです。ライブラリ間の競合:2つのライブラリが同じメソッドをswizzleする場合、実行順序は保証されません。iOSアップデートとの非互換性:Appleが新しいiOSバージョンでシグネチャを変更したりメソッドを削除したりすると、swizzlingはクラッシュを引き起こします。コード内での可視性の欠如:swizzlingはクラス実装に表示されないため、デバッグが複雑になります。
| リスク | 説明 | 軽減策 |
|---|---|---|
| ライブラリ競合 | 2つのライブラリがviewDidAppearをswizzleすると、一方が他方を壊す | class_getInstanceMethodでメソッドが既にswizzledかどうか確認する |
| 再帰 | 同じメソッドを再swizzlingすると無限ループが発生する | 常にdispatch_onceを使用する |
| シグネチャ変更 | Appleが新しいiOSでメソッドシグネチャを変更する — IMP不一致 | サポートするすべてのiOSバージョンでテストする |
| 不可視性 | SwizzlingがXcodeのコールスタックに表示されない | コード内のすべてのswizzling操作を文書化する |
| App Review | Appleは文書化されていないswizzlingを含むアプリを拒否する | パブリックAPIのみを使用し、目的を文書化する |
安全なswizzlingのためのベストプラクティスには以下が含まれます:常に元の実装を呼び出す、dispatch_onceを使用して+loadで厳密にswizzlingを実行する、swizzledメソッドにプレフィックス(例:s_originalMethodName)を付ける、各swizzling操作の目的を文書化する。Aspectsライブラリは、元のメソッドの前/後にブロックを連鎖実行することで競合問題を解決します。
Method swizzlingの代替案は、予測可能性と安全性の点から本番コードに推奨されます。デリゲートとプロトコル(UIApplicationDelegate、UITableViewDelegate)は、ランタイムを変更せずに明示的な拡張ポイントを提供します。サブクラス化 — viewDidAppearをオーバーライドするUIViewControllerのサブクラスを作成する — は予測可能に動作し、競合がありません。
SwiftUIとCombineはswizzlingの必要性を排除します。モディファイア(onAppear、onChange)はメソッドをオーバーライドせずに宣言的に動作を追加します。Android Jetpack Composeでは、エフェクト(LaunchedEffect、SideEffect)とモディファイアを通じて同じことを実現します。AOPフレームワーク(Android用AspectJ、iOS用InterposeKit)はコンパイルタイムウィービングによる安全な代替手段を提供します。
Apple WWDC 2024のデータによると、Swiftランタイムは言語レベルでmethod swizzlingをサポートしていません。@objc dynamicメソッドはObjective-C Runtimeを介してのみswizzleできます。@objcを使用しないSwiftアプリケーションは、サードパーティライブラリによる偶発的なswizzlingから完全に保護されています。これによりSwiftはより安全になりますが、ランタイム計装機能は制限されます。
SwiftUIモディファイア(onAppear、onChange、onReceive)とJetpack Composeエフェクト(LaunchedEffect、SideEffect、DisposableEffect)は、UIタスクにおいてswizzlingを完全に置き換えます。これらはディスパッチテーブルを変更せずに、横断的な動作を追加する宣言的で予測可能かつテスト可能な方法を提供します。新しいプロジェクトでは、AppleとGoogleはランタイムインターセプションではなくこのアプローチを推奨しています。
よくある質問
Method Swizzlingは、ルールを守れば本番環境で許容されます。dispatch_onceによる1回限りの実行、元の実装の呼び出し、すべてのiOSバージョンでのテスト、および文書化が必要です。単純なタスクでは、デリゲートまたはサブクラス化を使用する方が良いでしょう。本番環境でのSwizzlingは、監視および分析ライブラリに適しています。
Method Swizzlingはディスパッチテーブル内のIMPを置き換える特定のテクニックです。AOP(アスペクト指向プログラミング)はパラダイムであり、swizzlingはそのメカニズムの1つとして使用されます。AOPにはコンパイルタイムウィービング(AspectJ)、プロキシベースのインターセプション(Spring AOP)、コード生成も含まれます。
すべてのメッセージを追跡するにはobjc_msgSendにブレークポイントを設定します。クラス名を条件としてmethod_exchangeImplementationsにシンボリックブレークポイントを追加します。FLEXツールはクラスのどのメソッドがswizzledされているかを表示します。体系的なチェックには、クラスのディスパッチテーブルを出力するlldbスクリプトを使用します。
Swiftは言語レベルでswizzlingをサポートしていません。Method Swizzlingは@objc dynamicとマークされたメソッドでのみ機能し、これらはObjective-C Runtimeを介してコンパイルされます。純粋なSwiftメソッド(@objcなし)は静的ディスパッチを使用するためswizzleできません。それらのディスパッチテーブルは変更のためにアクセスできません。
Firebase Analytics(自動画面追跡のためのviewDidAppearのswizzling)、Amplitude、Mixpanel、FLEX(UI検査)、OHHTTPStubs(ネットワークリクエストモッキング)、Aspects(AOPフレームワーク)。これらはすべて元の実装を呼び出しながら、dispatch_onceを使用して+loadでswizzlingを実行します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。