iOS開發·runtime原理與實踐: 關聯對象篇(Associated Object)(爲分類添加「屬性」,爲UI控件關聯事件Block體,爲了避免重複執行)

本文Demo傳送門:AssociatedObjectDemogit

摘要:編程,只瞭解原理不行,必須實戰才能知道應用場景。本系列嘗試闡述runtime相關理論的同時介紹一些實戰場景,而本文則是本系列的關聯對象篇。本文中,第一節將介紹關聯對象及如何關聯對象,第二節將介紹關聯對象最經常使用的一個實戰場景:爲分類添加屬性,第三節將介紹關聯對象另外一個很重要的實戰場景:爲UI控件(好比,UIAlertView以及UIButton等等)關聯事件Block體。github

1. 什麼是關聯對象

1.1 關聯對象

分類(category)與關聯對象(Associated Object)做爲objective-c的擴展機制的兩個特性:分類,能夠經過它來擴展方法;Associated Object,能夠經過它來擴展屬性;objective-c

在iOS開發中,可能category比較常見,相對的Associated Object,就用的比較少,要用它以前,必須導入<objc/runtime.h>的頭文件。算法

1.2 如何關聯對象

runtime提供了給咱們3個API以管理關聯對象(存儲、獲取、移除):編程

//關聯對象
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)
複製代碼

其中的參數數組

  • id object:被關聯的對象
  • const void *key:關聯的key,要求惟一
  • id value:關聯的對象
  • objc_AssociationPolicy policy:內存管理的策略

2. 關聯對象:爲分類添加「屬性」

2.1 分類的限制

先來看@property 的一個例子bash

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

@end
複製代碼

在使用上述@property 時會作三件事:網絡

  • 生成實例變量 _property
  • 生成 getter 方法 - property
  • 生成 setter 方法 - setProperty:
@implementation DKObject {
    NSString *_property;
}

- (NSString *)property {
    return _property;
}

- (void)setProperty:(NSString *)property {
    _property = property;
}

@end
複製代碼

這些代碼都是編譯器爲咱們生成的,雖然你看不到它,可是它確實在這裏。可是,若是咱們在分類中寫一個屬性,則會給一個警告,分類中的 @property 並無爲咱們生成實例變量以及存取方法,而須要咱們手動實現。框架

由於在分類中 @property 並不會自動生成實例變量以及存取方法,因此通常使用關聯對象爲已經存在的類添加 「屬性」。解決方案:可使用兩個方法 objc_getAssociatedObject 以及 objc_setAssociatedObject 來模擬屬性 的存取方法,而使用關聯對象模擬實例變量。ide

2.2 用法解析

  • NSObject+AssociatedObject.m
#import "NSObject+AssociatedObject.h"
#import <objc/runtime.h>

@implementation NSObject (AssociatedObject)

- (void)setAssociatedObject:(id)associatedObject
{
    objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)associatedObject
{
    return objc_getAssociatedObject(self, _cmd);
}

@end
複製代碼
  • ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSObject *objc = [[NSObject alloc] init];
    objc.associatedObject = @"Extend Category";
    
    NSLog(@"associatedObject is = %@", objc.associatedObject);
}
複製代碼

其中, _cmd 代指當前方法的選擇子,也就是 @selector(categoryProperty)_cmd在Objective-C的方法中表示當前方法的selector,正如同self表示當前方法調用的對象實例。這裏強調當前,_cmd的做用域只在當前方法裏,直指當前方法名@selector。

於是,亦能夠寫成下面的樣子:

- (id)associatedObject
{
    return objc_getAssociatedObject(self, @selector(associatedObject));
}
複製代碼

另外,查看OBJC_ASSOCIATION_RETAIN_NONATOMIC,能夠發現它是一個枚舉類型,完整枚舉項以下所示:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};
複製代碼

從這裏的註釋咱們能看到不少東西,也就是說不一樣的 objc_AssociationPolicy 對應了不通的屬性修飾符,整理成表格以下:

objc_AssociationPolicy modifier
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, strong
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
OBJC_ASSOCIATION_RETAIN atomic, strong
OBJC_ASSOCIATION_COPY atomic, copy

而咱們在代碼中實現的屬性 associatedObject 就至關於使用了 nonatomicstrong 修飾符。

2.3 實戰場景

需求:好比你爲UIView添加事件,能夠在上面添加一個UITapGestureRecognizer,可是這個點擊事件沒法攜帶NSString信息(雖然能夠攜帶int類型的tag),這就沒法讓後續響應該事件的方法區分究竟是哪裏激活的事件。那麼,你是否能爲這種添加事件的方式攜帶另外的信息呢?

方案就是爲UITapGestureRecognizer追加一個「屬性」,利用runtime新建一個UITapGestureRecognizer的分類便可。

分類

  • UITapGestureRecognizer+NSString.h
#import <UIKit/UIKit.h>

@interface UITapGestureRecognizer (NSString)
//類拓展添加屬性
@property (nonatomic, strong) NSString *dataStr;

@end
複製代碼
  • UITapGestureRecognizer+NSString.m
#import "UITapGestureRecognizer+NSString.h"
#import <objc/runtime.h>
//定義常量 必須是C語言字符串
static char *PersonNameKey = "PersonNameKey";

@implementation UITapGestureRecognizer (NSString)

- (void)setDataStr:(NSString *)dataStr{
    objc_setAssociatedObject(self, PersonNameKey, dataStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(NSString *)dataStr{
    return objc_getAssociatedObject(self, PersonNameKey);
}

@end
複製代碼

調用處

  • VC的tableView:cellForRowAtIndexPath:代理方法中由cell激發事件
UITapGestureRecognizer *signViewSingle0 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
            //partnercode
signViewSingle0.dataStr = [cell.cellMdl.partnercode copy];
[cell.contractView addGestureRecognizer:signViewSingle0];
複製代碼
  • VC單獨寫一個響應方法
- (void)tapAction:(UITapGestureRecognizer *)sender
{
    UITapGestureRecognizer *tap = (UITapGestureRecognizer *)sender;
    //partnercode
    [self requestCallConSetWithPartnerCode:tap.dataStr];
}
複製代碼

如此一來,響應事件的方法就能夠根據事件激活方攜帶過來的信息進行下一步操做了,好比根據它攜帶過來的某個參數進行網絡請求等等。

2.4 應用到此知識點的第三方框架

2.5 這樣就能生成_變量?

儘管能夠模擬地爲分類添加「屬性」,但畢竟只是模擬。在分類中@property不會生成_變量,也不會實現getter和setter方法。咱們實現的只是getter和setter方法,並無自動生成下劃線開頭的變量!

3. 關聯對象:爲UI控件關聯事件Block體

3.1 UIAlertView

開發iOS時常常用到UIAlertView類,該類提供了一種標準視圖,可向用戶展現警告信息。當用戶按下按鈕關閉該視圖時,須要用委託協議(delegate protocol)來處理此動做,可是,要想設置好這個委託機制,就得把建立警告視圖和處理按鈕動做的代碼分開。因爲代碼分做兩塊,因此讀起來有點亂。

方案1 :傳統方案

比方說,咱們在使用UIAlertView時,通常都會這麼寫:

  • Test2ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view setBackgroundColor:[UIColor whiteColor]];
    self.title = @"Test2ViewController";
    
    [self popAlertViews1];
}

#pragma mark - way1
- (void)popAlertViews1{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
    [alert show];
}

// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 0) {
        [self doCancel];
    } else {
        [self doContinue];
    }
}
複製代碼

若是想在同一個類裏處理多個警告信息視圖,那麼代碼就會變得更爲複雜,咱們必須在delegate方法中檢查傳入的alertView參數,並據此選用相應的邏輯。

要是能在建立UIAlertView的時候直接把處理每一個按鈕的邏輯都寫好,那就簡單多了。這能夠經過關聯對象來作。建立完警告視圖以後,設定一個與之關聯的「塊」(block),等到執行delegate方法時再將其讀出來。下面對此方案進行改進。

方案2:關聯Block體

除了上一個方案中的傳統方法,咱們能夠利用關聯對象爲UIAlertView關聯一個Block:首先在建立UIAlertView的時候設置關聯一個回調(objc_setAssociatedObject),而後在UIAlertView的代理方法中取出關聯相應回調(objc_getAssociatedObject)。

  • Test2ViewController.m
#pragma mark - way2
- (void)popAlertViews2 {

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
    void (^clickBlock)(NSInteger) = ^(NSInteger buttonIndex){
        if (buttonIndex == 0) {
            [self doCancel];
        } else {
            [self doContinue];
        }
    };
    objc_setAssociatedObject(alert,CMAlertViewKey,clickBlock,OBJC_ASSOCIATION_COPY);
    [alert show];
}

// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{

    void (^clickBlock)(NSInteger) = objc_getAssociatedObject(alertView, CMAlertViewKey);
    clickBlock(buttonIndex);
}
複製代碼
方案3:繼續改進:封裝關聯的Block體,做爲屬性

上面方案,若是須要的位置比較多,相同的代碼會比較冗餘地出現,因此咱們能夠將設置Block的代碼封裝到一個UIAlertView的分類中去。

  • UIAlertView+Handle.h
#import <UIKit/UIKit.h>

// 聲明一個button點擊事件的回調block
typedef void (^ClickBlock)(NSInteger buttonIndex) ;

@interface UIAlertView (Handle)

@property (copy, nonatomic) ClickBlock callBlock;

@end
複製代碼
  • UIAlertView+Handle.m
#import "UIAlertView+Handle.h"
#import <objc/runtime.h>

@implementation UIAlertView (Handle)

- (void)setCallBlock:(ClickBlock)callBlock
{
    objc_setAssociatedObject(self, @selector(callBlock), callBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (ClickBlock )callBlock
{
    return objc_getAssociatedObject(self, _cmd);
    //    return objc_getAssociatedObject(self, @selector(callBlock));
}

@end
複製代碼
  • Test2ViewController.m
#pragma mark - way3
- (void)popAlertViews3 {

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];
    [alert setCallBlock:^(NSInteger buttonIndex) {
        if (buttonIndex == 0) {
            [self doCancel];
        } else {
            [self doContinue];
        }
    }];

    [alert show];
}

// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{

    void (^block)(NSInteger) = alertView.callBlock;
    block(buttonIndex);
}
複製代碼
方案4:繼續改進:封裝關聯的Block體,跟初始化方法綁在一塊兒

練習:能夠對這個分類進一步改進,將設置Block屬性的方法與初始化方法寫在一塊兒。

3.2 UIButton

除了上述的UIAlertView,這節以UIButton爲例,使用關聯對象完成一個功能函數:爲UIButton增長一個分類,定義一個方法,使用block去實現button的點擊回調。

  • UIButton+Handle.h
#import <UIKit/UIKit.h>
#import <objc/runtime.h> // 導入頭文件

// 聲明一個button點擊事件的回調block
typedef void(^ButtonClickCallBack)(UIButton *button);

@interface UIButton (Handle)

// 爲UIButton增長的回調方法
- (void)handleClickCallBack:(ButtonClickCallBack)callBack;

@end
複製代碼
  • UIButton+Handle.m
#import "UIButton+Handle.h"

// 聲明一個靜態的索引key,用於獲取被關聯對象的值
static char *buttonClickKey;

@implementation UIButton (Handle)

- (void)handleClickCallBack:(ButtonClickCallBack)callBack {
    // 將button的實例與回調的block經過索引key進行關聯:
    objc_setAssociatedObject(self, &buttonClickKey, callBack, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    // 設置button執行的方法
    [self addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventTouchUpInside];
}

- (void)buttonClicked {
    // 經過靜態的索引key,獲取被關聯對象(這裏就是回調的block)
    ButtonClickCallBack callBack = objc_getAssociatedObject(self, &buttonClickKey);
    
    if (callBack) {
        callBack(self);
    }
}

@end
複製代碼

在Test3ViewController中,導入咱們寫好的UIButton分類頭文件,定義一個button對象,調用分類中的這個方法:

  • Test3ViewController.m
[self.testButton handleClickCallBack:^(UIButton *button) {
        NSLog(@"block --- click UIButton+Handle");
    }];
複製代碼

4. 關聯對象:關聯觀察者對象

有時候咱們在分類中使用NSNotificationCenter或者KVO,推薦使用關聯的對象做爲觀察者,儘可能避免對象觀察自身。

例如大名鼎鼎的AFNetworking爲菊花控件監聽NSURLSessionTask以獲取網絡進度的分類:

  • UIActivityIndicatorView+AFNetworking.m
@implementation UIActivityIndicatorView (AFNetworking)

- (AFActivityIndicatorViewNotificationObserver *)af_notificationObserver {
    
    AFActivityIndicatorViewNotificationObserver *notificationObserver = objc_getAssociatedObject(self, @selector(af_notificationObserver));
    if (notificationObserver == nil) {
        notificationObserver = [[AFActivityIndicatorViewNotificationObserver alloc] initWithActivityIndicatorView:self];
        objc_setAssociatedObject(self, @selector(af_notificationObserver), notificationObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return notificationObserver;
}

- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task {
    [[self af_notificationObserver] setAnimatingWithStateOfTask:task];
}

@end
複製代碼
@implementation AFActivityIndicatorViewNotificationObserver

- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];

    [notificationCenter removeObserver:self name:AFNetworkingTaskDidResumeNotification object:nil];
    [notificationCenter removeObserver:self name:AFNetworkingTaskDidSuspendNotification object:nil];
    [notificationCenter removeObserver:self name:AFNetworkingTaskDidCompleteNotification object:nil];
    
    if (task) {
        if (task.state != NSURLSessionTaskStateCompleted) {
            UIActivityIndicatorView *activityIndicatorView = self.activityIndicatorView;
            if (task.state == NSURLSessionTaskStateRunning) {
                [activityIndicatorView startAnimating];
            } else {
                [activityIndicatorView stopAnimating];
            }

            [notificationCenter addObserver:self selector:@selector(af_startAnimating) name:AFNetworkingTaskDidResumeNotification object:task];
            [notificationCenter addObserver:self selector:@selector(af_stopAnimating) name:AFNetworkingTaskDidCompleteNotification object:task];
            [notificationCenter addObserver:self selector:@selector(af_stopAnimating) name:AFNetworkingTaskDidSuspendNotification object:task];
        }
    }
}
複製代碼

5. 關聯對象:爲了避免重複執行

有時候OC中會有些方法是爲了獲取某個數據,但這個獲取的過程只須要執行一次便可,這個獲取的算法可能有必定的時間複雜度和空間複雜度。那麼每次調用的時候就必須得執行一次嗎?有沒有辦法讓方法只執行一次,每次調用方法的時候直接得到那一次的執行結果?有的,方案就是讓某個對象的方法得到的數據結果做爲「屬性」與這個對象進行關聯。

有這麼一個需求:須要將字典轉成模型對象

方案:咱們先獲取到對象全部的屬性名(只執行一次),而後加入到一個數組裏面,而後再遍歷,利用KVC進行鍵值賦值。在程序運行的時候,抓取對象的屬性,這時候,要利用到運行時的關聯對象了,詳情見下面的代碼。

  • 獲取對象全部的屬性名
+ (NSArray *)propertyList {
    
    // 0. 判斷是否存在關聯對象,若是存在,直接返回
    /**
     1> 關聯到的對象
     2> 關聯的屬性 key
     
     提示:在 OC 中,類本質上也是一個對象
     */
    NSArray *pList = objc_getAssociatedObject(self, propertiesKey);
    if (pList != nil) {
        return pList;
    }
    
    // 1. 獲取`類`的屬性
    /**
     參數
     1> 類
     2> 屬性的計數指針
     */
    unsigned int count = 0;
    // 返回值是全部屬性的數組 objc_property_t
    objc_property_t *list = class_copyPropertyList([self class], &count);
    
    NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:count];
    
    // 遍歷數組
    for (unsigned int i = 0; i < count; ++i) {
        // 獲取到屬性
        objc_property_t pty = list[i];
        
        // 獲取屬性的名稱
        const char *cname = property_getName(pty);
        
        [arrayM addObject:[NSString stringWithUTF8String:cname]];
    }
    NSLog(@"%@", arrayM);
    
    // 釋放屬性數組
    free(list);
    
    // 設置關聯對象
    /**
     1> 關聯的對象
     2> 關聯對象的 key
     3> 屬性數值
     4> 屬性的持有方式 reatin, copy, assign
     */
    objc_setAssociatedObject(self, propertiesKey, arrayM, OBJC_ASSOCIATION_COPY_NONATOMIC);
    
    return arrayM.copy;
}
複製代碼
  • KVC進行鍵值賦值
+ (instancetype)objectWithDict:(NSDictionary *)dict {
    id obj = [[self alloc] init];
    
    //    [obj setValuesForKeysWithDictionary:dict];
    NSArray *properties = [self propertyList];
    
    // 遍歷屬性數組
    for (NSString *key in properties) {
        // 判斷字典中是否包含這個key
        if (dict[key] != nil) {
            // 使用 KVC 設置數值
            [obj setValue:dict[key] forKeyPath:key];
        }
    }
   
    
    return obj;
}
複製代碼
相關文章
相關標籤/搜索