Runtime應(yīng)用之關(guān)聯(lián)對象和MethodSwizzling

最近用到了sunnyxx的forkingdog系列《UIView-FDCollapsibleConstraints》,紀(jì)錄下關(guān)聯(lián)對象和MethodSwizzling在實(shí)際場景中的應(yīng)用。

基本概念

關(guān)聯(lián)對象

  • 關(guān)聯(lián)對象操作函數(shù)

    • 設(shè)置關(guān)聯(lián)對象:
    /**
     *  設(shè)置關(guān)聯(lián)對象
     *
     *  @param object 源對象
     *  @param key    關(guān)聯(lián)對象的key
     *  @param value  關(guān)聯(lián)的對象
     *  @param policy 關(guān)聯(lián)策略
     */
    void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    
  - 獲取關(guān)聯(lián)對象:

  ```objc
  /**
   *  獲取關(guān)聯(lián)對象
   *
   *  @param object 源對象
   *  @param key    關(guān)聯(lián)對象的key
   *
   *  @return 關(guān)聯(lián)的對象
   */
  id objc_getAssociatedObject(id object, const void *key)

其中設(shè)置關(guān)聯(lián)對象的策略有以下5種:

  • 和MRC的內(nèi)存操作retain、assign方法效果差不多
    • 比如設(shè)置的關(guān)聯(lián)對象是一個UIView,并且這個UIView已經(jīng)有父控件時,可以使用OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_ASSIGN             // 對關(guān)聯(lián)對象進(jìn)行弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC   // 對關(guān)聯(lián)對象進(jìn)行強(qiáng)引用(非原子)
OBJC_ASSOCIATION_COPY_NONATOMIC     // 對關(guān)聯(lián)對象進(jìn)行拷貝引用(非原子)
OBJC_ASSOCIATION_RETAIN             // 對關(guān)聯(lián)對象進(jìn)行強(qiáng)引用
OBJC_ASSOCIATION_COPY               // 對關(guān)聯(lián)對象進(jìn)行拷貝引用

關(guān)聯(lián)對象在一些第三方框架的分類中常常見到,這里在分析前先看下分類的結(jié)構(gòu):

struct category_t {
    // 類名
    const char *name;
    // 類
    classref_t cls;
    // 實(shí)例方法
    struct method_list_t *instanceMethods;
    // 類方法
    struct method_list_t *classMethods;
    // 協(xié)議
    struct protocol_list_t *protocols;
    // 屬性
    struct property_list_t *instanceProperties;
};

從以上的分類結(jié)構(gòu),可以看出,分類中是不能添加成員變量的,也就是Ivar類型。所以,如果想在分類中存儲某些數(shù)據(jù)時,關(guān)聯(lián)對象就是在這種情況下的常用選擇。

需要注意的是,關(guān)聯(lián)對象并不是成員變量,關(guān)聯(lián)對象是由一個全局哈希表存儲的鍵值對中的值。

全局哈希表的定義如下:

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對應(yīng)的哈希表如下:

// 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

其中的ObjectAssociationMap就是value的類型。同時,也可以知道ObjectAssociationMap的鍵值對類型為(關(guān)聯(lián)對象對應(yīng)的key : 關(guān)聯(lián)對象),也就是函數(shù)objc_setAssociatedObject的對應(yīng)的key:value參數(shù)。

大部分情況下,關(guān)聯(lián)對像會使用getter方法的SEL當(dāng)作key(getter方法中可以這樣表示:_cmd)。

更多和關(guān)聯(lián)對象有關(guān)的底層信息,可以查看Dive into Category

MethodSwizzling

MethodSwizzling主要原理就是利用runtime的動態(tài)特性,交換方法對應(yīng)的實(shí)現(xiàn),也就是IMP
通常,MethodSwizzling的封裝為:

+ (void)load
{
// 源方法--原始的方法
// 目的方法--我們自己實(shí)現(xiàn)的,用來替換源方法

    static dispatch_once_t onceToken;
    // MethodSwizzling代碼只需要在類加載時調(diào)用一次,并且需要線程安全環(huán)境
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        // 獲取方法的SEL
        SEL origionSel = @selector(viewDidLoad);
        SEL swizzlingSel = @selector(tpc_viewDidLoad);
        //    IMP origionMethod = class_getMethodImplementation(class, origionSel);
        //    IMP swizzlingMethod = class_getMethodImplementation(class, swizzlingSel);
        // 根據(jù)SEL獲取對應(yīng)的Method
        Method origionMethod = class_getInstanceMethod(class, origionSel);
        Method swizzlingMethod = class_getInstanceMethod(class, swizzlingSel);

        // 向類中添加目的方法對應(yīng)的Method
        BOOL hasAdded = class_addMethod(class, origionSel, method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod));

        // 交換源方法和目的方法的Method方法實(shí)現(xiàn)
        if (hasAdded) {
            class_replaceMethod(class, swizzlingSel, method_getImplementation(origionMethod), method_getTypeEncoding(origionMethod));
        } else {
            method_exchangeImplementations(origionMethod, swizzlingMethod);
        }
    });
}

為了便于區(qū)別,這里列出Method的結(jié)構(gòu):

typedef struct method_t *Method;

// method_t
struct method_t {
    SEL name;
    const char *types;
    IMP imp;
    ...
}

實(shí)現(xiàn)MethodSwizzling需要了解的有以下幾個常用函數(shù):

// 返回方法的具體實(shí)現(xiàn)
IMP class_getMethodImplementation ( Class cls, SEL name )

// 返回方法描述
Method class_getInstanceMethod ( Class cls, SEL name )

// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types )

// 替代方法的實(shí)現(xiàn)
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types )

// 返回方法的實(shí)現(xiàn)
IMP method_getImplementation ( Method m );

// 獲取描述方法參數(shù)和返回值類型的字符串
const char * method_getTypeEncoding ( Method m );

// 交換兩個方法的實(shí)現(xiàn)
void method_exchangeImplementations ( Method m1, Method m2 );

介紹MethodSwizzling的文章很多,更多和MethodSwizzling有關(guān)的信息,可以查看Objective-C的hook方案(一): Method Swizzling

針對UIView-FDCollapsibleConstraints的應(yīng)用

UIView-FDCollapsibleConstraints是sunnyxx陽神寫的一個UIView分類,可以實(shí)現(xiàn)僅在IB中對UIView上的約束進(jìn)行設(shè)置,就達(dá)到以下效果,而不需要編寫改變約束的代碼:(圖片來源UIView-FDCollapsibleConstraints

UIView下
UITableView下

這里介紹下自己對這個分類的理解:

  • 實(shí)現(xiàn)思路
    • 將需要和UIView關(guān)聯(lián)且需要動態(tài)修改的約束添加進(jìn)一個和UIView綁定的特定的數(shù)組里面
    • 根據(jù)UIView的內(nèi)容是否為nil,對這個特定數(shù)組中的約束值進(jìn)行統(tǒng)一設(shè)置

而在分類不能增加成員變量的情況下,和UIView綁定的特定的數(shù)組就是用關(guān)聯(lián)對象實(shí)現(xiàn)的。

先從分類的頭文件開始:

頭文件

@interface UIView (FDCollapsibleConstraints)

/// Assigning this property immediately disables the view's collapsible constraints'
/// by setting their constants to zero.
@property (nonatomic, assign) BOOL fd_collapsed;

/// Specify constraints to be affected by "fd_collapsed" property by connecting in
/// Interface Builder.
@property (nonatomic, copy) IBOutletCollection(NSLayoutConstraint) NSArray *fd_collapsibleConstraints;

@end

@interface UIView (FDAutomaticallyCollapseByIntrinsicContentSize)

/// Enable to automatically collapse constraints in "fd_collapsibleConstraints" when
/// you set or indirectly set this view's "intrinsicContentSize" to {0, 0} or absent.
///
/// For example:
///  imageView.image = nil;
///  label.text = nil, label.text = @"";
///
/// "NO" by default, you may enable it by codes.
@property (nonatomic, assign) BOOL fd_autoCollapse;

/// "IBInspectable" property, more friendly to Interface Builder.
/// You gonna find this attribute in "Attribute Inspector", toggle "On" to enable.
/// Why not a "fd_" prefix? Xcode Attribute Inspector will clip it like a shit.
/// You should not assgin this property directly by code, use "fd_autoCollapse" instead.
@property (nonatomic, assign, getter=fd_autoCollapse) IBInspectable BOOL autoCollapse;

分析幾點(diǎn):

  • IBOutletCollection,詳情參考IBAction / IBOutlet / IBOutlet?Collection
    • 表示將SB中相同的控件連接到一個數(shù)組中;這里使用這個方式,將在SB中的NSLayoutConstraint添加到fd_collapsibleConstraints數(shù)組中,以便后續(xù)對約束進(jìn)行統(tǒng)一操作
    • IBOutletCollectionh和IBOutlet操作方式一樣,需要在IB中進(jìn)行相應(yīng)的拖拽才能把對應(yīng)的控件加到數(shù)組中(UIView->NSLayoutConstraint
    • 設(shè)置了IBOutletCollection之后,當(dāng)從storybooard或者xib中加載進(jìn)行解檔時,最終會調(diào)用fd_collapsibleConstraints的setter方法,然后就可以在其setter方法中做相應(yīng)的操作了
  • IBInspectable 表示這個屬性可以在IB中更改,如下圖

Snip20150704_1.png

- 還有一個這里沒用,IB_DESIGNABLE,這個表示可以在IB中實(shí)時顯示修改的效果,詳情參考@IBDesignable和@IBInspectable

主文件

NSLayoutConstraint (_FDOriginalConstantStorage)
  • 因?yàn)樵?code>修改約束值后,需要還原操作,但是分類中無法添加成員變量,所以在這個分類中,給NSLayoutConstraint約束關(guān)聯(lián)一個存儲約束初始值的浮點(diǎn)數(shù),以便在修改約束值后,可以還原
/// A stored property extension for NSLayoutConstraint's original constant.
@implementation NSLayoutConstraint (_FDOriginalConstantStorage)

// 給NSLayoutConstraint關(guān)聯(lián)一個初始約束值
- (void)setFd_originalConstant:(CGFloat)originalConstant
{
    objc_setAssociatedObject(self, @selector(fd_originalConstant), @(originalConstant), OBJC_ASSOCIATION_RETAIN);
}

- (CGFloat)fd_originalConstant
{
#if CGFLOAT_IS_DOUBLE
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
#else
    return [objc_getAssociatedObject(self, _cmd) floatValue];
#endif
}

@end
UIView (FDCollapsibleConstraints)
  • 同樣,因?yàn)樾枰?code>對UIView上綁定的約束進(jìn)行改動,所以需要在分類中添加一個可以記錄所有約束的對象,需要用到關(guān)聯(lián)對象

  • 實(shí)現(xiàn)fd_collapsibleConstraints屬性的setter和getter方法 (關(guān)聯(lián)一個存儲約束的對象)

    • getter方法中創(chuàng)建關(guān)聯(lián)對象constraints(和懶加載的方式類似,不過不是創(chuàng)建成員變量)
    • setter方法中設(shè)置約束的初始值,并添加進(jìn)關(guān)聯(lián)對象constraints中,方便統(tǒng)一操作
  • 從IB中關(guān)聯(lián)的約束,最終會調(diào)用setFd_collapsibleConstraints:方法,也就是這一步不需要手動調(diào)用,系統(tǒng)自己完成(在awakeFromNib之前完成IB這些值的映射)

    - (NSMutableArray *)fd_collapsibleConstraints
    {
      // 獲取對象的所有約束關(guān)聯(lián)值
      NSMutableArray *constraints = objc_getAssociatedObject(self, _cmd);
      if (!constraints) {
          constraints = @[].mutableCopy;
          // 設(shè)置對象的所有約束關(guān)聯(lián)值
          objc_setAssociatedObject(self, _cmd, constraints, OBJC_ASSOCIATION_RETAIN);
      }
    
      return constraints;
    }
    
    // IBOutletCollection表示xib中的相同的控件連接到一個數(shù)組中
    // 因?yàn)樵O(shè)置了IBOutletCollection,所以從xib進(jìn)行解檔時,最終會調(diào)用set方法
    // 然后就來到了這個方法
    - (void)setFd_collapsibleConstraints:(NSArray *)fd_collapsibleConstraints
    {
      // Hook assignments to our custom `fd_collapsibleConstraints` property.
      // 返回保存原始約束的數(shù)組,使用關(guān)聯(lián)對象
      NSMutableArray *constraints = (NSMutableArray *)self.fd_collapsibleConstraints;
    
      [fd_collapsibleConstraints enumerateObjectsUsingBlock:^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {
          // Store original constant value
          // 保存原始的約束
          constraint.fd_originalConstant = constraint.constant;
          [constraints addObject:constraint];
      }];
    }
    
    
  • 使用Method Swizzling交換自己的和系統(tǒng)的-setValue:forKey:方

    • 實(shí)現(xiàn)自己的KVC的-setValue:forKey:方法
 // load先從原類,再調(diào)用分類的開始調(diào)用
  // 也就是調(diào)用的順序是
  // 原類
  // FDCollapsibleConstraints
  // FDAutomaticallyCollapseByIntrinsicContentSize
  // 所以并不沖突

  + (void)load
  {
      // Swizzle setValue:forKey: to intercept assignments to `fd_collapsibleConstraints`
      // from Interface Builder. We should not do so by overriding setvalue:forKey:
      // as the primary class implementation would be bypassed.
      SEL originalSelector = @selector(setValue:forKey:);
      SEL swizzledSelector = @selector(fd_setValue:forKey:);

      Class class = UIView.class;
      Method originalMethod = class_getInstanceMethod(class, originalSelector);
      Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

      method_exchangeImplementations(originalMethod, swizzledMethod);
  }

  // xib也就是xml,再加載進(jìn)行decode時,會調(diào)用setValue:forKey:,把他的方法替換成自身的,然后獲取添加的約束
  // 作者說明不使用重寫這個KVC方法的方式,是因?yàn)檫@樣會覆蓋view本身在這個方法中進(jìn)行的操作

  - (void)fd_setValue:(id)value forKey:(NSString *)key
  {
      NSString *injectedKey = [NSString stringWithUTF8String:sel_getName(@selector(fd_collapsibleConstraints))];

      if ([key isEqualToString:injectedKey]) {
          // This kind of IBOutlet won't trigger property's setter, so we forward it.
          // 作者的意思是,IBOutletCollection不會觸發(fā)對應(yīng)屬性的setter方法,所以這里執(zhí)行手動調(diào)用
          self.fd_collapsibleConstraints = value;
      } else {
          // Forward the rest of KVC's to original implementation.
          [self fd_setValue:value forKey:key];
      }
  }
  • 上面使用Method Swizzling的原因作者認(rèn)為是這種類型的IBOutlet不會觸發(fā)其setter方法,但是經(jīng)過測試,注釋掉這段代碼后,系統(tǒng)還是自己觸發(fā)了setter方法,說明這種IBOutlet還是可以觸發(fā)setter方法的。所以,即使沒有這一段代碼,應(yīng)該也是可行的
操作結(jié)果
  • 設(shè)置對應(yīng)的約束值

    • 這里給UIView對象提供一個關(guān)聯(lián)對象,來判斷是否將約束值清零
    • 注意,這里只要傳入的是YES,那么,這個UIView對應(yīng)存入constraints關(guān)聯(lián)對象的所有約束,都會置為0
    #pragma mark - Dynamic Properties
    
    - (void)setFd_collapsed:(BOOL)collapsed
    {
        [self.fd_collapsibleConstraints enumerateObjectsUsingBlock:
     ^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {
         if (collapsed) {
             // 如果view的內(nèi)容為nil,則將view關(guān)聯(lián)的constraints對象所有值設(shè)置為0
             constraint.constant = 0;
         } else {
            // 如果view的內(nèi)容不為nil,則將view關(guān)聯(lián)的constraints對象所有值返回成原值
             constraint.constant = constraint.fd_originalConstant;
         }
     }];
        // 設(shè)置fd_collapsed關(guān)聯(lián)對象,供自動collapsed使用
        objc_setAssociatedObject(self, @selector(fd_collapsed), @(collapsed), OBJC_ASSOCIATION_RETAIN);
    }
    
    - (BOOL)fd_collapsedFDAutomaticallyCollapseByIntrinsicContentSize{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
    }
    @end
    

######UIView (FDAutomaticallyCollapseByIntrinsicContentSize)
- 使用Method Swizzling交換自己實(shí)現(xiàn)的-fd_updateConstraints和系統(tǒng)的updateConstraints方法
  - [self fd_updateConstraints]調(diào)用的是self的updateConstraints方法,fd_updateConstraints和updateConstraints方法的IMP,即方法實(shí)現(xiàn)已經(jīng)調(diào)換了
  - 可以看到,加入這里不使用Method Swizzling,那么要實(shí)現(xiàn)在更新約束時就需要`重寫updateConstraints`方法,而這只能在`繼承UIView`的情況下才能完成的;而實(shí)用了Method Swizzling,就可以直接在`分類`中實(shí)現(xiàn)在`調(diào)用系統(tǒng)updateConstraints的前提下`,又`添加自己想要執(zhí)行的附加代碼`
  - `intrinsicContentSize(控件的內(nèi)置大小)`默認(rèn)為UIViewNoIntrinsicMetric,當(dāng)`控件中沒有內(nèi)容時`,調(diào)用intrinsicContentSize返回的即為`默認(rèn)值`,詳情參考([intrinsicContentSize和Content Hugging Priority](http://www.mgenware.com/blog/?p=491))

  ```objc
  #pragma mark - Hacking "-updateConstraints"

    + (void)load
    {
    // Swizzle to hack "-updateConstraints" method
    SEL originalSelector = @selector(updateConstraints);
    SEL swizzledSelector = @selector(fd_updateConstraints);

    Class class = UIView.class;
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    method_exchangeImplementations(originalMethod, swizzledMethod);
    }

    - (void)fd_updateConstraints
    {
    // Call primary method's implementation
    [self fd_updateConstraints];

    if (self.fd_autoCollapse && self.fd_collapsibleConstraints.count > 0) {

        // "Absent" means this view doesn't have an intrinsic content size, {-1, -1} actually.
        const CGSize absentIntrinsicContentSize = CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);

        // 當(dāng)設(shè)置控件顯示內(nèi)容為nil時,計(jì)算出來的contentSize和上面的相等
        // Calculated intrinsic content size
        const CGSize contentSize = [self intrinsicContentSize];

        // When this view doesn't have one, or has no intrinsic content size after calculating,
        // it going to be collapsed.
        if (CGSizeEqualToSize(contentSize, absentIntrinsicContentSize) ||
            CGSizeEqualToSize(contentSize, CGSizeZero)) {
            // 當(dāng)控件沒有內(nèi)容時,則設(shè)置控件關(guān)聯(lián)對象constraints的所有約束值為0
            self.fd_collapsed = YES;
        } else {
            // 當(dāng)控件有內(nèi)容時,則設(shè)置控件關(guān)聯(lián)對象constraints的所有約束值返回為原值
            self.fd_collapsed = NO;
        }
    }
    }

  • 設(shè)置一些動態(tài)屬性(關(guān)聯(lián)對象)

    • 給UIView關(guān)聯(lián)一個對象,來判斷是否需要自動對約束值進(jìn)行清零
    #pragma mark - Dynamic Properties
    
      - (BOOL)fd_autoCollapse
    

{
return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setFd_autoCollapse:(BOOL)autoCollapse

{
objc_setAssociatedObject(self, @selector(fd_autoCollapse), @(autoCollapse), OBJC_ASSOCIATION_RETAIN);
}

- (void)setAutoCollapse:(BOOL)collapse

{
// Just forwarding
self.fd_autoCollapse = collapse;
}


##總結(jié)

總體來說,在分類中要想實(shí)現(xiàn)相對復(fù)雜的邏輯,卻`不能添加成員變量`,也`不想對需要操作的類進(jìn)行繼承`,這時就需要runtime中的`關(guān)聯(lián)對象和MethodSwizzling`技術(shù)了。

forkingdog系列分類都用到了runtime的一些知識,代碼簡潔注釋齊全風(fēng)格也不錯,比較適合需要學(xué)習(xí)runtime應(yīng)用知識的我。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容