Swift & Objective-C

KVO内部動作完全解説:isa-swizzlingの正体

オブザーバーを登録した瞬間、ランタイムはオブジェクトのクラスを動的サブクラスに差し替えます。isa-swizzlingの動作、classが嘘をつく理由、setterを経由しないと通知されない条件を整理します。

読了 6 分
KVO内部動作完全解説:isa-swizzlingの正体のカバー画像

KVO(Key-Value Observing)は使い方だけ見れば単純です。addObserverで登録すると、プロパティが変わるたびに通知が届きます。

関連記事Objective-Cメソッドスウィズリング完全解説では、背景となる概念と関連する適用例を確認できます。

でも不思議ではありませんか?プロパティのsetterに通知コードを入れていないのに、ランタイムは値が変わったことをどうやって知るのでしょうか?

答えは大胆です。オブザーバーを登録した瞬間、ランタイムはそのオブジェクトのクラスを密かに差し替えます。

この手法をisa-swizzlingと呼びます。前回の記事のメソッドスウィズリングがディスパッチテーブルの「行」を変えるものなら、今回はオブジェクトが属する「クラス」そのものを入れ替える話です。


addObserverの瞬間に起きていること

Person オブジェクトにオブザーバーを登録すると、ランタイムは次の処理を行います。

  1. NSKVONotifying_Personというサブクラスを動的に生成⟧します
  2. 観察対象プロパティのsetterを、変更前後に通知コードを挿入したバージョンでオーバーライドします
  3. オブジェクトのisaポインタを新しいサブクラスに差し替えます

以降、person.name = @"Kim"を実行すると、呼び出しフローは次のようになります。

// NSKVONotifying_Personがオーバーライドした setterの疑似コード
- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    [super setName:name];             // 元の setter を実行
    [self didChangeValueForKey:@"name"];  // ここでオブザーバーに通知
}

オブジェクトのメモリ内容は変わらず、所属クラスだけが変わるため、既存コードは何も気づきません。最後のオブザーバーを削除すると、isaは元のクラスに戻ります。

KVOが動的なNSKVONotifying_Personサブクラスを作成し、Personオブジェクトのisaを差し替える流れ
KVOが動的サブクラスを作成してisaを差し替える順序

クラスが嘘をつく:class vs object_getClass

ここで興味深い細部が出てきます。オブザーバーが付いたオブジェクトにクラスを尋ねると、答えが二つに分かれます。

[person class];              // Person — 嘘
object_getClass(person);     // NSKVONotifying_Person — 真実

ランタイムは動的サブクラスで-classメソッドまでオーバーライドし、元のクラスを返すようにしています。開発者が「自分のオブジェクトのクラス名がおかしいのはなぜ?」と混乱しないための配慮であり、実装詳細を隠す設計です。

object_getClass()はisaポインタを直接読むため、本当のクラスが見えます。デバッグ中に二つの値が異なるなら、そのオブジェクトは現在誰かに観察されています。


setterを経由しなければ通知もない

isa-swizzlingの原理を知れば、KVOの有名な制約も当然だと分かります。

person.name = @"Kim";       // 通知発生 — オーバーライドされた setter 経由
person->_name = @"Kim";     // 通知なし — ivar 直接変更, setterを経由しない

KVO通知の発生源はオーバーライドされたsetterです。ivarを直接変更するとその経路を迂回するため、何も起きません。Objective-Cでプロパティアクセスにドット記法(内部的にはsetter呼び出し)を推奨する実質的な理由の一つです。

setterなしで値を変更する必要がある場合は、手動通知で囲みます。

[self willChangeValueForKey:@"name"];
_name = @"Kim";
[self didChangeValueForKey:@"name"];

逆に、特定プロパティの自動通知を無効にしたい場合は、automaticallyNotifiesObserversForKey:でNOを返します。


実務で注意すべきこと

観察のライフサイクル管理は依然として重要です。 ブロックベースAPIが返すNSKeyValueObservationトークンを、観察が必要な間は保持してください(Apple KVOドキュメント)。

観察を終えるときにトークンを無効化または解放すれば、登録も解除されます。文字列ベースAPIを使い続けるなら、addObserverremoveObserverのライフサイクルを自分で合わせる必要があります(Apple KVOドキュメント)。

同じクラスでKVOとメソッドスウィズリングを混ぜると、順序の問題が起きます。 スウィズリング後にKVOを付けると動的サブクラスはスウィズリング済みsetterを包みますが、逆順では互いの前提が食い違う可能性があります。

ランタイムを操作する二つの手法が一つのオブジェクトで出会うと、デバッグの難易度は倍増します。

Swiftでは@objc dynamicが必須です。

class Person: NSObject {
    @objc dynamic var name: String = ""
}

let observation = person.observe(\.name, options: [.new]) { _, change in
    print(change.newValue ?? "")
}

isa-swizzlingが動作するには、setter呼び出しがObjective-Cのメッセージ経路を通る必要があるためです。

Swift 4以降はブロックベースのobserve(_:options:changeHandler:) APIを使います。返されたNSKeyValueObservationトークンが解放されると、観察の解除まで自動的に処理されます。Using Key-Value Observing in Swift

そのため、文字列keyPath時代の問題の大半をこの構造だけで防げます。

KVOのisa-swizzling前のPersonオブジェクトとNSKVONotifying_Person動的サブクラスの比較図
外見はPersonですが、ランタイムクラスはNSKVONotifying_Personです

まとめ

  • オブザーバー登録時、KVOはNSKVONotifying_サブクラスを動的に生成し、isaを差し替えます
  • 通知の発生源はオーバーライドされたsetter—willChange/didChangeが組み込まれたバージョンです
  • -classは元のクラスを返すよう細工され、本当のクラスはobject_getClass()として見えます
  • ivarの直接変更はsetterを迂回するため通知されません—必要ならwillChange/didChangeで手動通知します
  • Swiftでは@objc dynamic+ブロックベースのobserve APIが標準で、トークンの解放がそのまま観察解除になります

メソッドスウィズリングとisa-swizzlingまで見ると、Objective-Cランタイムの全体像がつかめます。メソッドの接続表もオブジェクトの所属クラスも、すべてランタイムで変更できるデータです。この柔軟性が、Cocoaフレームワークの多くの魔法の源になっています。

あわせて読みたい

出典と確認基準

  • Using Key-Value Observing in SwiftApple · 公式ドキュメント · 確認日 2026年8月17日根拠: Swift KVOの登録、変更通知、observation tokenのライフサイクル