KVO(Key-Value Observing)は使い方だけ見れば単純です。addObserverで登録すると、プロパティが変わるたびに通知が届きます。
関連記事Objective-Cメソッドスウィズリング完全解説では、背景となる概念と関連する適用例を確認できます。
でも不思議ではありませんか?プロパティのsetterに通知コードを入れていないのに、ランタイムは値が変わったことをどうやって知るのでしょうか?
答えは大胆です。オブザーバーを登録した瞬間、ランタイムはそのオブジェクトのクラスを密かに差し替えます。
この手法をisa-swizzlingと呼びます。前回の記事のメソッドスウィズリングがディスパッチテーブルの「行」を変えるものなら、今回はオブジェクトが属する「クラス」そのものを入れ替える話です。
addObserverの瞬間に起きていること
Person オブジェクトにオブザーバーを登録すると、ランタイムは次の処理を行います。
NSKVONotifying_Personというサブクラスを動的に生成⟧します- 観察対象プロパティのsetterを、変更前後に通知コードを挿入したバージョンでオーバーライドします
- オブジェクトのisaポインタを新しいサブクラスに差し替えます
以降、person.name = @"Kim"を実行すると、呼び出しフローは次のようになります。
// NSKVONotifying_Personがオーバーライドした setterの疑似コード
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
[super setName:name]; // 元の setter を実行
[self didChangeValueForKey:@"name"]; // ここでオブザーバーに通知
}
オブジェクトのメモリ内容は変わらず、所属クラスだけが変わるため、既存コードは何も気づきません。最後のオブザーバーを削除すると、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を使い続けるなら、addObserverとremoveObserverのライフサイクルを自分で合わせる必要があります(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はNSKVONotifying_サブクラスを動的に生成し、isaを差し替えます
- 通知の発生源はオーバーライドされたsetter—willChange/didChangeが組み込まれたバージョンです
-classは元のクラスを返すよう細工され、本当のクラスはobject_getClass()として見えます- ivarの直接変更はsetterを迂回するため通知されません—必要ならwillChange/didChangeで手動通知します
- Swiftでは
@objc dynamic+ブロックベースのobserve APIが標準で、トークンの解放がそのまま観察解除になります
メソッドスウィズリングとisa-swizzlingまで見ると、Objective-Cランタイムの全体像がつかめます。メソッドの接続表もオブジェクトの所属クラスも、すべてランタイムで変更できるデータです。この柔軟性が、Cocoaフレームワークの多くの魔法の源になっています。
あわせて読みたい
- Objective-Cメソッドスウィズリング完全解説
- +load vs +initialize:呼び出し時点と継承の落とし穴
- Objective-Cカテゴリ vs Swift extensionの違い(概念・注意点まとめ)
出典と確認基準
- Using Key-Value Observing in SwiftApple · 公式ドキュメント · 確認日 2026年8月17日根拠: Swift KVOの登録、変更通知、observation tokenのライフサイクル

