Runtime 系列 3-- 給 category 新增屬性

西木柚子發表於2016-08-11

Associated Objects的作用

Associated Objects的作用一般有如下三種:

  1. 為系統類新增私有變數以幫助實現細節;
  2. 為系統類新增公有屬性;
  3. 為 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);複製程式碼

這三個函式的命名對程式設計師非常友好,可以讓我們一眼就看出函式的作用:

  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。


場景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的實現細節,大家可以參考這篇文章:

tech.meituan.com/DiveIntoCat…

更多文章歡迎大家關注我的個人blog:

blog.ximu.site

相關文章