Associated Objects的作用
Associated Objects的作用一般有如下三種:
- 為系統類新增私有變數以幫助實現細節;
- 為系統類新增公有屬性;
- 為 KVO 建立一個關聯的觀察者。
我們用的最多的是第二種情況。
那麼給系統類新增公共屬性是如何做到的呢?我們可以通過category來實現,下面具體講講實現過程。
###category新增屬性的實現原理 我們知道,在 Objective-C 中可以通過 Category 給一個系統類新增屬性,但是卻不能新增例項變數,這似乎成為了 Objective-C 的一個明顯短板。看下面category的結構就知道
Objective-C
struct category_t {
// 類名
const char *name;
// 類
classref_t cls;
// 例項方法
struct method_list_t *instanceMethods;
// 類方法
struct method_list_t *classMethods;
// 協議
struct protocol_list_t *protocols;
// 屬性
struct property_list_t *instanceProperties;
};複製程式碼
從以上的分類結構,可以看出,分類中是不能新增成員變數的,也就是Ivar型別。所以,如果想在分類中儲存某些資料時,關聯物件就是在這種情況下的常用選擇。
需要注意的是,關聯物件並不是成員變數,關聯物件是由一個全域性雜湊表儲存的鍵值對中的值。
全域性雜湊表
class AssociationsManager {
static spinlock_t _lock;
static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap.
public:
AssociationsManager() { spinlock_lock(&_lock); }
~AssociationsManager() { spinlock_unlock(&_lock); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};複製程式碼
其中的AssociationsHashMap就是那個全域性雜湊表,而註釋中也說明的很清楚了:雜湊表中儲存的鍵值對是(源物件指標 : 另一個雜湊表)。而這個value,即ObjectAssociationMap對應的雜湊表如下:
// hash_map和unordered_map是模版類
// 檢視原始碼後可以看出AssociationsHashMap的key是disguised_ptr_t型別,value是ObjectAssociationMap *型別
// ObjectAssociationMap的key是void *型別,value是ObjcAssociation型別
#if TARGET_OS_WIN32
typedef hash_map ObjectAssociationMap;
typedef hash_map AssociationsHashMap;
#else
typedef ObjcAllocator > ObjectAssociationMapAllocator;
class ObjectAssociationMap : public std::map {
public:
void *operator new(size_t n) { return ::_malloc_internal(n); }
void operator delete(void *ptr) { ::_free_internal(ptr); }
};
typedef ObjcAllocator > AssociationsHashMapAllocator;
class AssociationsHashMap : public unordered_map {
public:
void *operator new(size_t n) { return ::_malloc_internal(n); }
void operator delete(void *ptr) { ::_free_internal(ptr); }
};
#endif複製程式碼
AssociationsManager裡面是由一個靜態AssociationsHashMap來儲存所有的關聯物件的。這相當於把所有物件的關聯物件都存在一個全域性map裡面。而map的的key是這個物件的指標地址(任意兩個不同物件的指標地址一定是不同的),而這個map的value又是另外一個AssociationsHashMap,裡面儲存了關聯物件的key-value對。
####實現程式碼範例 給系統類UIViewController新增一個name屬性
#import
@interface UIViewController (Utilities)
@property(nonatomic,strong)NSString *name;
@end
======================================
#import "UIViewController+Utilities.h"
#import
@implementation UIViewController (Utilities)
-(void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY);
}
-(NSString *)name
{
return objc_getAssociatedObject(self, @selector(name));
}
@end 複製程式碼
Associated Objects的使用
相關函式
與 Associated Objects 相關的函式主要有三個,我們可以在 runtime 原始碼的 runtime.h 檔案中找到它們的宣告:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);複製程式碼
這三個函式的命名對程式設計師非常友好,可以讓我們一眼就看出函式的作用:
- objc_setAssociatedObject 用於給物件新增關聯物件,傳入 nil 則可以移除已有的關聯物件;
- objc_getAssociatedObject 用於獲取關聯物件;
- objc_removeAssociatedObjects 用於移除一個物件的所有關聯物件。
注:objc_removeAssociatedObjects 函式我們一般是用不上的,因為這個函式會移除一個物件的所有關聯物件,將該物件恢復成“原始”狀態。這樣做就很有可能把別人新增的關聯物件也一併移除,這並不是我們所希望的。所以一般的做法是通過給 objc_setAssociatedObject 函式傳入 nil 來移除某個已有的關聯物件。
key 值
關於前兩個函式中的 key 值是我們需要重點關注的一個點,這個 key 值必須保證是一個物件級別(為什麼是物件級別?看完下面的章節你就會明白了)的唯一常量。一般來說,有以下三種推薦的 key 值:
- 宣告 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 作為 key 值;
- 宣告 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 作為 key 值;
- 用 selector ,使用 getter 方法的名稱作為 key 值。
最推薦第3種方式,因為你不需要去定義一個變數名。
關聯策略
在給一個物件新增關聯物件時有五種關聯策略可供選擇:
大多數場景我們選擇OBJC_ASSOCIATION_RETAIN_NONATOMIC。
場景1、給button關聯一個block
一般我們初始化一個UIbutton,然後給他加上點選事件,這個時候點選事件的程式碼需要去另外一個方法實現,導致程式碼太分散。我們可以給button關聯一個block,把點選事件的程式碼都移到block裡面執行。
實現程式碼
@interface UIButton ()
@property (nonatomic, copy) void (^callbackBlock)(UIButton * button);
@end
@implementation UIButton (Callback)
- (void (^)(UIButton *))callbackBlock {
return objc_getAssociatedObject(self, @selector(callbackBlock));
}
- (void)setCallbackBlock:(void (^)(UIButton *))callbackBlock {
objc_setAssociatedObject(self, @selector(callbackBlock), callbackBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (instancetype)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame]) {
[self addTarget:self action:@selector(didClickAction:) forControlEvents:UIControlEventTouchUpInside];
}
return self;
}
- (void)didClickAction:(UIButton *)button {
self.callbackBlock(button);
}
@end複製程式碼
呼叫
UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(100,100 , 100, 100)];
[btn setTitle:@"dsd" forState:UIControlStateNormal];
btn.backgroundColor = [UIColor redColor];
[self.view addSubview:btn];
btn.callbackBlock = ^(UIButton *btn){
NSLog(@"%@",btn.titleLabel.text);
};複製程式碼
場景2、給UIAlertView關聯一個block
同上面的場景一樣,UIAlertView的代理方法裡面程式碼和初始化方法是分開的,我們也可以用block把他們集中到一起實現。
實現程式碼
#import
static void *alertViewBlockKey = &alertViewBlockKey;
- (void)function {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Question"
message:@"What do you want to do?"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Continue", nil];
void (^block)(NSInteger) = ^(NSInteger buttonIndex) {
if (buttonIndex == 0) {
//你的程式碼;
} else {
//你的程式碼;
}
};
objc_setAssociatedObject(alertView, alertViewBlockKey, block, OBJC_ASSOCIATION_COPY);
[alertView show];
}
// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, alertViewBlockKey);
block(buttonIndex);
} 複製程式碼
場景3、AFNetworking給UIImageView新增私有屬性
當擴充套件一個內建類的行為時,保持附加屬性的狀態可能非常必要。注意以下說的是一種非常教科書式的關聯物件的用例:AFNetworking在 UIImageView 的category上用了關聯物件來保持一個operation物件,用於從網路上某URL非同步地獲取一張圖片。
@interface UIImageView (_AFNetworking)
@property (readwrite, nonatomic, strong, setter = af_setImageRequestOperation:) AFHTTPRequestOperation *af_imageRequestOperation;
@end
@implementation UIImageView (_AFNetworking)
- (AFHTTPRequestOperation *)af_imageRequestOperation {
return (AFHTTPRequestOperation *)objc_getAssociatedObject(self, @selector(af_imageRequestOperation));
}
- (void)af_setImageRequestOperation:(AFHTTPRequestOperation *)imageRequestOperation {
objc_setAssociatedObject(self, @selector(af_imageRequestOperation), imageRequestOperation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end複製程式碼
@implementation UIImageView (AFNetworking)
- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
placeholderImage:(UIImage *)placeholderImage
success:(void (^)(NSURLRequest *request, NSHTTPURLResponse * __nullable response, UIImage *image))success
failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse * __nullable response, NSError *error))failure
{
[self cancelImageRequestOperation];
UIImage *cachedImage = [[[self class] sharedImageCache] cachedImageForRequest:urlRequest];
if (cachedImage) {
if (success) {
success(urlRequest, nil, cachedImage);
} else {
self.image = cachedImage;
}
self.af_imageRequestOperation = nil;
} else {
if (placeholderImage) {
self.image = placeholderImage;
}
__weak __typeof(self)weakSelf = self;
self.af_imageRequestOperation = [[AFHTTPRequestOperation alloc] initWithRequest:urlRequest];
self.af_imageRequestOperation.responseSerializer = self.imageResponseSerializer;
[self.af_imageRequestOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
if ([[urlRequest URL] isEqual:[strongSelf.af_imageRequestOperation.request URL]]) {
if (success) {
success(urlRequest, operation.response, responseObject);
} else if (responseObject) {
strongSelf.image = responseObject;
}
if (operation == strongSelf.af_imageRequestOperation){
strongSelf.af_imageRequestOperation = nil;
}
}
[[[strongSelf class] sharedImageCache] cacheImage:responseObject forRequest:urlRequest];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
if ([[urlRequest URL] isEqual:[strongSelf.af_imageRequestOperation.request URL]]) {
if (failure) {
failure(urlRequest, operation.response, error);
}
if (operation == strongSelf.af_imageRequestOperation){
strongSelf.af_imageRequestOperation = nil;
}
}
}];
[[[self class] af_sharedImageRequestOperationQueue] addOperation:self.af_imageRequestOperation];
}
}複製程式碼
不恰當的使用場景
1、 當值不需要的時候建立一個關聯物件。
一個常見的例子就是在view上建立一個方便的方法去儲存來自model的屬性、值或者其他混合的資料。如果那個資料在之後根本用不到,那麼這種方法雖然是沒什麼問題的,但用關聯到物件的做法並不可取。
2、當一個值可以被其他值推算出時建立一個關聯物件。
例如:在呼叫cellForRowAtIndexPath: 時儲存一個指向view的 UITableViewCell 中accessory view的引用,用於在 tableView:accessoryButtonTappedForRowWithIndexPath: 中使用。
3、 用關聯物件替代X,這裡的X可以代表下列含義:
- 當繼承比擴充套件原有的類更方便時用子類化。
- 為事件的響應者新增響應動作。
- 當響應動作不方便使用時使用的手勢動作捕捉。
- 行為可以在其他物件中被代理實現時要用代理(delegate)。
- 用NSNotification 和 NSNotificationCenter進行鬆耦合化的跨系統的事件通知。
PS:
比起其他解決問題的方法,關聯物件應該被視為最後的選擇(事實上category也不應該作為首選方法)。
總結:
關於category的實現細節,大家可以參考這篇文章:
更多文章歡迎大家關注我的個人blog: