Swift 與 Objective-C

Objective-C Associated Objects:在類別中加入屬性

類別無法新增 ivar,但可以透過 Associated Objects 綁定值。本文整理值儲存在物件旁邊的側表中的原理、類別屬性的完整實作,以及只需小心 assign 的記憶體策略。

閱讀 4 分鐘
Objective-C Associated Objects:在類別中加入屬性 封面圖

在「類別 vs extension」一文中,我們整理過「類別無法加入儲存屬性」。原因是類別的記憶體配置會在編譯時固定,之後載入的類別沒有位置插入 ivar。

但在實務程式碼中,常會看到類別確實擁有屬性。祕密就是 Associated Objects。這是不用 ivar 就能將值綁定到物件上的執行階段功能。

這是解決類別儲存屬性問題的官方替代方案,也是 Objective-C 執行階段藏在物件外部的輔助儲存區。


原理:附加在物件旁的側表

Associated Objects 的核心函式有兩個。

objc_setAssociatedObject(目標物件、鍵、值、記憶體策略);
objc_getAssociatedObject(目標物件、鍵);

值不會儲存在物件的 ivar 區域。執行階段會在全域管理的 **關聯表(側表)**中,以「物件位址 → {鍵: 值}」的形式記錄。

物件本體的記憶體配置完全不變,因此不會與編譯時固定的配置限制衝突。

目標物件釋放後,執行階段會在關聯表中找到對應項目,並依策略一併清理。由系統自動管理生命週期,因此適合實務使用。

objc_setAssociatedObject 將值寫入執行階段全域關聯表的結構圖
本體配置不變,值儲存在旁邊的表中

類別屬性的完整實作

在類別中宣告屬性時,編譯器只會產生 getter/setter 宣告,不會建立儲存空間。用 Associated Objects 填補即可。

#import <objc/runtime.h>

@interface UIView (BadgeCount)
@property (nonatomic, strong) NSNumber *badgeCount;
@end

@implementation UIView (BadgeCount)

- (NSNumber *)badgeCount {
    return objc_getAssociatedObject(self, @selector(badgeCount));
}

- (void)setBadgeCount:(NSNumber *)badgeCount {
    objc_setAssociatedObject(self, @selector(badgeCount),
                             badgeCount,
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

慣例是使用 @selector(badgeCount)作為鍵。鍵不是依值,而是依 指標位址比較,因此只要是整個 App 唯一的位址即可。

選取器由執行階段保證唯一,也不必另外宣告 static 變數,因此最簡潔。像 static void *kBadgeKey = &kBadgeKey;這種經典方法仍然有效。

用字串常值當鍵是陷阱。即使內容相同,只要編譯單元不同,位址就可能不同。

「明明儲存了卻得到 nil」的謎團就由此產生。


記憶體策略:只需小心 assign

第四個參數對應屬性特性。

策略 對應特性
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN / COPY 與上者相同,但為 atomic
OBJC_ASSOCIATION_ASSIGN assign — 不是 weak

唯一需要注意的是 ASSIGN。名稱看似 weak,但它是不會自動變成 nil 的 unsafe_unretained

如果關聯物件先被釋放,就會留下懸空指標。一存取就會當機。

若需要 weak 語意,必須建立以 weak 屬性包裝值的包裝物件,再以 RETAIN 關聯它。

要刪除特定關聯值,將其 set 為 nil 即可。objc_removeAssociatedObjects會清除該物件的所有關聯值,威力過強,基本上不應有使用時機。


實務用途與界線

實務上常見的組合如下。

  • UIKit 類別擴充:為 UIView 加入徽章數量、為 UIButton 加入以閉包為基礎的動作處理常式、為 UIViewController 加入分析用畫面名稱
  • 代理程式 → 區塊轉換包裝器:將代理程式物件關聯至原始物件,綁定兩者生命週期的模式(Alamofire 前代的網路類別很常使用)
  • 與 swizzling 搭配:當透過 swizzling 插入的邏輯需要儲存狀態時使用。這就是執行階段系列的兩種技術常在實務中一起出現的原因。

不過仍須守住界線。Associated Objects 終究只是存放輔助資料的管道。

若物件的核心狀態散落在關聯表中,讀者就很難掌握完整設計。若能建立子類別,使用 ivar 才是正解。

只有在必須為不擁有的類別附加額外資訊時,才適合拿出這張牌。

繞過 Swift extension 的 stored property 限制時,也能使用相同函式(僅限 NSObject 系列)。但在 Swift 世界中,使用協定加專用儲存型別,或以組合方式解決,更加自然。

用登山扣將收納包掛在旅行袋上的 Associated Objects 比喻插圖
這就是把收納包掛在袋子外側的方式

總結

  • Associated Objects 將值儲存在執行階段的側表,而非物件本體,因此也能在類別中運作。
  • 鍵會以指標位址比較。慣例是重複使用 @selector,而字串常值是陷阱。
  • 記憶體策略中的 ASSIGN不是 weak,而是 unsafe_unretained。請注意懸空指標造成的當機。
  • 目標物件釋放後,關聯值也會自動清理。
  • 用途僅限於為不擁有的類別附加輔助資料。若把核心狀態放在這裡,就是設計亮紅燈的訊號。

執行階段系列下一篇是 NSProxy。我們將深入探討訊息轉送一文預告的另一個非 NSObject 根類別,如何作為「純粹的代理人」運作。

延伸閱讀