Runtime 系列 3-- 給 category 添加屬性

Associated Objects的做用

Associated Objects的做用通常有以下三種:html

  1. 爲系統類添加私有變量以幫助實現細節;
  2. 爲系統類添加公有屬性;
  3. 爲 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的使用

相關函數

與 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);複製代碼

這三個函數的命名對程序員很是友好,可讓咱們一眼就看出函數的做用:

  1. objc_setAssociatedObject 用於給對象添加關聯對象,傳入 nil 則能夠移除已有的關聯對象;
  2. objc_getAssociatedObject 用於獲取關聯對象;
  3. objc_removeAssociatedObjects 用於移除一個對象的全部關聯對象。

注:objc_removeAssociatedObjects 函數咱們通常是用不上的,由於這個函數會移除一個對象的全部關聯對象,將該對象恢復成「原始」狀態。這樣作就頗有可能把別人添加的關聯對象也一併移除,這並非咱們所但願的。因此通常的作法是經過給 objc_setAssociatedObject 函數傳入 nil 來移除某個已有的關聯對象。

key 值

關於前兩個函數中的 key 值是咱們須要重點關注的一個點,這個 key 值必須保證是一個對象級別(爲何是對象級別?看完下面的章節你就會明白了)的惟一常量。通常來講,有如下三種推薦的 key 值:

  1. 聲明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 做爲 key 值;
  2. 聲明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 做爲 key 值;
  3. 用 selector ,使用 getter 方法的名稱做爲 key 值。

最推薦第3種方式,由於你不須要去定義一個變量名。

關聯策略

在給一個對象添加關聯對象時有五種關聯策略可供選擇:

image

大多數場景咱們選擇OBJC_ASSOCIATION_RETAIN_NONATOMIC。


場景一、給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);
    };複製代碼

場景二、給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); } 

 複製代碼

場景三、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];
    }
}複製代碼

不恰當的使用場景

一、 當值不須要的時候創建一個關聯對象。

一個常見的例子就是在view上建立一個方便的方法去保存來自model的屬性、值或者其餘混合的數據。若是那個數據在以後根本用不到,那麼這種方法雖然是沒什麼問題的,但用關聯到對象的作法並不可取。

二、當一個值能夠被其餘值推算出時創建一個關聯對象。

例如:在調用cellForRowAtIndexPath: 時存儲一個指向view的 UITableViewCell 中accessory view的引用,用於在 tableView:accessoryButtonTappedForRowWithIndexPath: 中使用。

三、 用關聯對象替代X,這裏的X能夠表明下列含義:
  • 當繼承比擴展原有的類更方便時用子類化。
  • 爲事件的響應者添加響應動做。
  • 當響應動做不方便使用時使用的手勢動做捕捉。
  • 行爲能夠在其餘對象中被代理實現時要用代理(delegate)。
  • 用NSNotification 和 NSNotificationCenter進行鬆耦合化的跨系統的事件通知。

PS:

比起其餘解決問題的方法,關聯對象應該被視爲最後的選擇(事實上category也不該該做爲首選方法)。

總結:

關於category的實現細節,你們能夠參考這篇文章:

tech.meituan.com/DiveIntoCat…

更多文章歡迎你們關注個人我的blog:

blog.ximu.site

相關文章
相關標籤/搜索