KVO(Key-Value Observing)光看用法很簡單。透過 addObserver 註冊後,每次屬性變更都會收到通知。
相關文章 Objective-C 方法 Swizzling 完整整理 會一併介紹背景概念與延伸的實際應用案例。
但這不是很奇怪嗎?明明沒有在屬性的 setter 中加入通知程式碼,執行階段究竟怎麼知道值變了?
答案很直接。加入觀察者的瞬間,執行階段會悄悄替換該物件的類別。
這項技術稱為 isa-swizzling。如果上一篇文章的 method 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 token(Apple KVO 文件)。
結束觀察時,使 token 失效或釋放它,註冊也會一併清理。如果繼續使用字串式 API,就必須自行對齊 addObserver 與 removeObserver 的生命週期(Apple KVO 文件)。
在同一個類別混用 KVO 與方法 swizzling,會產生順序問題。 若先 swizzling 再加入 KVO,動態子類別會包住已 swizzling 的 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 token 釋放時,也會自動解除觀察註冊。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 是標準做法,而釋放 token 就等同於解除註冊
把方法 swizzling 與 isa-swizzling 一起納入,就能掌握 Objective-C 執行階段的全貌。方法連結表與物件所屬類別,都是可以在執行階段替換的資料;這份彈性正是 Cocoa 框架大多數魔法的來源。
延伸閱讀
- Objective-C 方法 Swizzling 完整整理
- +load vs +initialize:呼叫時機與繼承陷阱
- Objective-C Category vs Swift extension 的差異(概念與注意事項總整理)
來源與驗證
- Using Key-Value Observing in SwiftApple · 官方文件 · 查核 2026年8月17日依據: Swift KVO 註冊、變更通知與 observation token 生命週期

