最近用到了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)


這里介紹下自己對這個分類的理解:
- 實(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)的操作了
- 表示將SB中相同的控件連接到一個數(shù)組中;這里使用這個方式,將在SB中的
-
IBInspectable表示這個屬性可以在IB中更改,如下圖

- 還有一個這里沒用,
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)該也是可行的

-
設(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)用知識的我。