Associated Objects的做用通常有以下三種:html
- 爲系統類添加私有變量以幫助實現細節;
- 爲系統類添加公有屬性;
- 爲 KVO 建立一個關聯的觀察者。
咱們用的最多的是第二種狀況。程序員
那麼給系統類添加公共屬性是如何作到的呢?咱們能夠經過category來實現,下面具體講講實現過程。網絡
###category添加屬性的實現原理 咱們知道,在 Objective-C 中能夠經過 Category 給一個系統類添加屬性,可是卻不能添加實例變量,這彷佛成爲了 Objective-C 的一個明顯短板。看下面category的結構就知道app
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類型。因此,若是想在分類中存儲某些數據時,關聯對象就是在這種狀況下的經常使用選擇。異步
須要注意的是,關聯對象並非成員變量,關聯對象是由一個全局哈希表存儲的鍵值對中的值。ide
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對。ui
####實現代碼範例 給系統類UIViewController添加一個name屬性atom
#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 相關的函數主要有三個,咱們能夠在 runtime 源碼的 runtime.h 文件中找到它們的聲明:url
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_removeAssociatedObjects 函數咱們通常是用不上的,由於這個函數會移除一個對象的全部關聯對象,將該對象恢復成「原始」狀態。這樣作就頗有可能把別人添加的關聯對象也一併移除,這並非咱們所但願的。因此通常的作法是經過給 objc_setAssociatedObject 函數傳入 nil 來移除某個已有的關聯對象。
關於前兩個函數中的 key 值是咱們須要重點關注的一個點,這個 key 值必須保證是一個對象級別(爲何是對象級別?看完下面的章節你就會明白了)的惟一常量。通常來講,有如下三種推薦的 key 值:
- 聲明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 做爲 key 值;
- 聲明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 做爲 key 值;
- 用 selector ,使用 getter 方法的名稱做爲 key 值。
最推薦第3種方式,由於你不須要去定義一個變量名。
在給一個對象添加關聯對象時有五種關聯策略可供選擇:
大多數場景咱們選擇OBJC_ASSOCIATION_RETAIN_NONATOMIC。
通常咱們初始化一個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);
};複製代碼
同上面的場景同樣,UIAlertView的代理方法裏面代碼和初始化方法是分開的,咱們也能夠用block把他們集中到一塊兒實現。
#importstatic 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); } 複製代碼
當擴展一個內建類的行爲時,保持附加屬性的狀態可能很是必要。注意如下說的是一種很是教科書式的關聯對象的用例: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];
}
}複製代碼
一個常見的例子就是在view上建立一個方便的方法去保存來自model的屬性、值或者其餘混合的數據。若是那個數據在以後根本用不到,那麼這種方法雖然是沒什麼問題的,但用關聯到對象的作法並不可取。
例如:在調用cellForRowAtIndexPath: 時存儲一個指向view的 UITableViewCell 中accessory view的引用,用於在 tableView:accessoryButtonTappedForRowWithIndexPath: 中使用。
PS:
比起其餘解決問題的方法,關聯對象應該被視爲最後的選擇(事實上category也不該該做爲首選方法)。
關於category的實現細節,你們能夠參考這篇文章:
更多文章歡迎你們關注個人我的blog: