在「類別 vs extension」一文中,我們整理過「類別無法加入儲存屬性」。原因是類別的記憶體配置會在編譯時固定,之後載入的類別沒有位置插入 ivar。
但在實務程式碼中,常會看到類別確實擁有屬性。祕密就是 Associated Objects。這是不用 ivar 就能將值綁定到物件上的執行階段功能。
這是解決類別儲存屬性問題的官方替代方案,也是 Objective-C 執行階段藏在物件外部的輔助儲存區。
原理:附加在物件旁的側表
Associated Objects 的核心函式有兩個。
objc_setAssociatedObject(目標物件、鍵、值、記憶體策略);
objc_getAssociatedObject(目標物件、鍵);
值不會儲存在物件的 ivar 區域。執行階段會在全域管理的 **關聯表(側表)**中,以「物件位址 → {鍵: 值}」的形式記錄。
物件本體的記憶體配置完全不變,因此不會與編譯時固定的配置限制衝突。
目標物件釋放後,執行階段會在關聯表中找到對應項目,並依策略一併清理。由系統自動管理生命週期,因此適合實務使用。
類別屬性的完整實作
在類別中宣告屬性時,編譯器只會產生 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 將值儲存在執行階段的側表,而非物件本體,因此也能在類別中運作。
- 鍵會以指標位址比較。慣例是重複使用
@selector,而字串常值是陷阱。 - 記憶體策略中的
ASSIGN不是 weak,而是 unsafe_unretained。請注意懸空指標造成的當機。 - 目標物件釋放後,關聯值也會自動清理。
- 用途僅限於為不擁有的類別附加輔助資料。若把核心狀態放在這裡,就是設計亮紅燈的訊號。
執行階段系列下一篇是 NSProxy。我們將深入探討訊息轉送一文預告的另一個非 NSObject 根類別,如何作為「純粹的代理人」運作。

