問題背景
需為系統類別 (如 UIButton) 新增屬性儲存自訂資料 (如 cityId、typeId),無法繼承 (final 類別) 或不想建立子類別。
解決方案:Category + Associated Objects
.h 檔案:宣告屬性
#import <UIKit/UIKit.h>
@interface UIButton (TLButton)
@property (nonatomic, strong, readwrite) NSNumber *cityId;
@property (nonatomic, strong, readwrite) NSNumber *typeId;
@property (nonatomic, readwrite) NSNumber *progress;
@end
.m 檔案:實作 Getter/Setter 使用關聯物件
#import "UIButton+TLButton.h"
#import <objc/runtime.h>
@implementation UIButton (TLButton)
- (NSNumber *)cityId {
return objc_getAssociatedObject(self, @selector(cityId));
}
- (void)setCityId:(NSNumber *)cityId {
[self willChangeValueForKey:@"cityId"];
objc_setAssociatedObject(self, @selector(cityId), cityId, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self didChangeValueForKey:@"cityId"];
}
- (NSNumber *)typeId {
return objc_getAssociatedObject(self, @selector(typeId));
}
- (void)setTypeId:(NSNumber *)typeId {
[self willChangeValueForKey:@"typeId"];
objc_setAssociatedObject(self, @selector(typeId), typeId, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self didChangeValueForKey:@"typeId"];
}
- (NSNumber *)progress {
return objc_getAssociatedObject(self, @selector(progress));
}
- (void)setProgress:(NSNumber *)progress {
[self willChangeValueForKey:@"progress"];
objc_setAssociatedObject(self, @selector(progress), progress, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self didChangeValueForKey:@"progress"];
}
@end
關鍵 API 解析
| 函數 | 用途 | 參數 |
objc_getAssociatedObject | 讀取關聯值 | (id object, const void *key) |
objc_setAssociatedObject | 設定關聯值 | (id object, const void *key, id value, objc_AssociationPolicy policy) |
objc_removeAssociatedObjects | 移除所有關聯 | (id object) |
關聯策略對照
| 策略常數 | 記憶體語意 | 等同 @property | 適用場景 |
OBJC_ASSOCIATION_ASSIGN | 弱引用 | assign / weak | 委派、非擁有關係 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | 強引用、非原子 | strong, nonatomic | 一般物件 (效能較好) |
OBJC_ASSOCIATION_COPY_NONATOMIC | 複製、非原子 | copy, nonatomic | NSString、NSArray 等不可變類別 |
OBJC_ASSOCIATION_RETAIN | 強引用、原子 | strong | 多執行緒共享 |
OBJC_ASSOCIATION_COPY | 複製、原子 | copy | 多執行緒共享不可變物件 |
使用範例
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.cityId = @1001;
btn.typeId = @2;
btn.progress = @0.5;
NSLog(@"City: %@, Type: %@, Progress: %@", btn.cityId, btn.typeId, btn.progress);
Category vs 繼承 比較
| 維度 | Category (分類) | 繼承 (子類別) |
| 適用性 | 系統類別、無法修改源碼 | 自有類別、需擴展行為 |
| 屬性儲存 | 需用 Associated Objects | 直接新增實例變數 |
| 方法覆寫 | 可覆寫但危險 (方法交換) | 安全覆寫、呼叫 super |
| 編譯檢查 | 較弱 | 完整型別檢查 |
| 推薦場景 | 為現有類別「貼上」功能 | 建立專用型別、IS-A 關係 |
最佳實務建議
- Key 使用
@selector(propertyName) 確保唯一性且易除錯
- 實作
willChangeValueForKey/didChangeValueForKey 支援 KVO
- 避免在 Category 覆寫系統方法 (用 Method Swizzling 並謹慎)
- Swift 中用
objc_setAssociatedObject 同樣適用,或用 Extension + 儲存至外部字典