最近用到了sunnyxx的forkingdog系列《UIView-FDCollapsibleConstraints》,紀錄下關聯(lián)對象和MethodSwizzling在實際場景中的應用。
基本概念
關聯(lián)對象
-
關聯(lián)對象操作函數(shù)
- 設置關聯(lián)對象:
/** * 設置關聯(lián)對象 * * @param object 源對象 * @param key 關聯(lián)對象的key * @param value 關聯(lián)的對象 * @param policy 關聯(lián)策略 */ void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
- 獲取關聯(lián)對象:
```objc
/**
* 獲取關聯(lián)對象
*
* @param object 源對象
* @param key 關聯(lián)對象的key
*
* @return 關聯(lián)的對象
*/
id objc_getAssociatedObject(id object, const void *key)
其中設置關聯(lián)對象的策略有以下5種:
- 和MRC的內存操作retain懂衩、assign方法效果差不多
- 比如設置的關聯(lián)對象是一個UIView,并且這個UIView已經有父控件時实辑,可以使用OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_ASSIGN // 對關聯(lián)對象進行弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC // 對關聯(lián)對象進行強引用(非原子)
OBJC_ASSOCIATION_COPY_NONATOMIC // 對關聯(lián)對象進行拷貝引用(非原子)
OBJC_ASSOCIATION_RETAIN // 對關聯(lián)對象進行強引用
OBJC_ASSOCIATION_COPY // 對關聯(lián)對象進行拷貝引用
關聯(lián)對象在一些第三方框架的分類中常常見到,這里在分析前先看下分類的結構:
struct category_t {
// 類名
const char *name;
// 類
classref_t cls;
// 實例方法
struct method_list_t *instanceMethods;
// 類方法
struct method_list_t *classMethods;
// 協(xié)議
struct protocol_list_t *protocols;
// 屬性
struct property_list_t *instanceProperties;
};
從以上的分類結構藻丢,可以看出剪撬,分類中是不能添加成員變量的,也就是Ivar類型郁岩。所以婿奔,如果想在分類中存儲某些數(shù)據(jù)
時,關聯(lián)對象就是在這種情況下的常用選擇问慎。
需要注意的是萍摊,關聯(liá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對應的哈希表如下:
// 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的鍵值對類型為(關聯(lián)對象對應的key : 關聯(lián)對象)
,也就是函數(shù)objc_setAssociatedObject的對應的key:value參數(shù)追葡。
大部分情況下腺律,關聯(lián)對像會使用getter方法的SEL當作key
(getter方法中可以這樣表示:_cmd)。
更多和關聯(lián)對象有關的底層信息宜肉,可以查看Dive into Category
MethodSwizzling
MethodSwizzling主要原理就是利用runtime的動態(tài)特性匀钧,交換方法對應的實現(xiàn)
,也就是IMP
谬返。
通常之斯,MethodSwizzling的封裝為:
+ (void)load
{
// 源方法--原始的方法
// 目的方法--我們自己實現(xiàn)的,用來替換源方法
static dispatch_once_t onceToken;
// MethodSwizzling代碼只需要在類加載時調用一次遣铝,并且需要線程安全環(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獲取對應的Method
Method origionMethod = class_getInstanceMethod(class, origionSel);
Method swizzlingMethod = class_getInstanceMethod(class, swizzlingSel);
// 向類中添加目的方法對應的Method
BOOL hasAdded = class_addMethod(class, origionSel, method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod));
// 交換源方法和目的方法的Method方法實現(xiàn)
if (hasAdded) {
class_replaceMethod(class, swizzlingSel, method_getImplementation(origionMethod), method_getTypeEncoding(origionMethod));
} else {
method_exchangeImplementations(origionMethod, swizzlingMethod);
}
});
}
為了便于區(qū)別佑刷,這里列出Method的結構:
typedef struct method_t *Method;
// method_t
struct method_t {
SEL name;
const char *types;
IMP imp;
...
}
實現(xiàn)MethodSwizzling需要了解的有以下幾個常用函數(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 )
// 替代方法的實現(xiàn)
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types )
// 返回方法的實現(xiàn)
IMP method_getImplementation ( Method m );
// 獲取描述方法參數(shù)和返回值類型的字符串
const char * method_getTypeEncoding ( Method m );
// 交換兩個方法的實現(xiàn)
void method_exchangeImplementations ( Method m1, Method m2 );
介紹MethodSwizzling的文章很多莉擒,更多和MethodSwizzling有關的信息,可以查看Objective-C的hook方案(一): Method Swizzling
針對UIView-FDCollapsibleConstraints的應用
UIView-FDCollapsibleConstraints是sunnyxx陽神寫的一個UIView分類项乒,可以實現(xiàn)僅在IB中對UIView上的約束進行設置啰劲,就達到以下效果,而不需要編寫改變約束的代碼:(圖片來源UIView-FDCollapsibleConstraints)
這里介紹下自己對這個分類的理解:
- 實現(xiàn)思路
將需要和UIView關聯(lián)且需要動態(tài)修改的約束添加進一個和UIView綁定的特定的數(shù)組里面
根據(jù)UIView的內容是否為nil檀何,對這個特定數(shù)組中的約束值進行統(tǒng)一設置
而在分類不能增加成員變量的情況下,和UIView綁定的特定的數(shù)組就是用關聯(lián)對象實現(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;
分析幾點:
-
IBOutletCollection
频鉴,詳情參考IBAction / IBOutlet / IBOutlet?Collection- 表示將SB中相同的控件連接到一個數(shù)組中;這里使用這個方式恋拍,將在SB中的
NSLayoutConstraint
添加到fd_collapsibleConstraints數(shù)組中垛孔,以便后續(xù)對約束進行統(tǒng)一操作
- IBOutletCollectionh和IBOutlet操作方式一樣,需要
在IB中進行相應的拖拽
才能把對應的控件加到數(shù)組中(UIView->NSLayoutConstraint
) - 設置了IBOutletCollection之后施敢,當從storybooard或者xib中加載進行解檔時周荐,最終會調用fd_collapsibleConstraints的
setter
方法,然后就可以在其setter方法中做相應的操作了
- 表示將SB中相同的控件連接到一個數(shù)組中;這里使用這個方式恋拍,將在SB中的
-
IBInspectable
表示這個屬性可以在IB中更改僵娃,如下圖
- 還有一個這里沒用概作,
IB_DESIGNABLE
,這個表示可以在IB中實時顯示修改的效果默怨,詳情參考@IBDesignable和@IBInspectable
主文件
NSLayoutConstraint (_FDOriginalConstantStorage)
- 因為在
修改約束值后讯榕,需要還原操作
,但是分類中無法添加成員變量
匙睹,所以在這個分類中愚屁,給NSLayoutConstraint約束關聯(lián)一個存儲約束初始值
的浮點數(shù),以便在修改約束值后痕檬,可以還原
/// A stored property extension for NSLayoutConstraint's original constant.
@implementation NSLayoutConstraint (_FDOriginalConstantStorage)
// 給NSLayoutConstraint關聯(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)
同樣霎槐,因為需要
對UIView上綁定的約束進行改動
,所以需要在分類中添加一個可以記錄所有約束的對象
梦谜,需要用到關聯(lián)對象-
實現(xiàn)fd_collapsibleConstraints屬性的setter和getter方法 (
關聯(lián)一個存儲約束的對象
)- 在
getter方法中創(chuàng)建關聯(lián)對象constraints
(和懶加載
的方式類似丘跌,不過不是創(chuàng)建成員變量) - 在
setter方法中設置約束的初始值
,并添加進關聯(lián)對象constraints
中改淑,方便統(tǒng)一操作
- 在
-
從IB中關聯(lián)的約束碍岔,最終會調用setFd_collapsibleConstraints:方法,也就是這一步不需要手動調用朵夏,系統(tǒng)自己完成(在awakeFromNib之前完成IB這些值的映射)
- (NSMutableArray *)fd_collapsibleConstraints { // 獲取對象的所有約束關聯(lián)值 NSMutableArray *constraints = objc_getAssociatedObject(self, _cmd); if (!constraints) { constraints = @[].mutableCopy; // 設置對象的所有約束關聯(lián)值 objc_setAssociatedObject(self, _cmd, constraints, OBJC_ASSOCIATION_RETAIN); } return constraints; } // IBOutletCollection表示xib中的相同的控件連接到一個數(shù)組中 // 因為設置了IBOutletCollection蔼啦,所以從xib進行解檔時,最終會調用set方法 // 然后就來到了這個方法 - (void)setFd_collapsibleConstraints:(NSArray *)fd_collapsibleConstraints { // Hook assignments to our custom `fd_collapsibleConstraints` property. // 返回保存原始約束的數(shù)組仰猖,使用關聯(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:方
- 實現(xiàn)自己的KVC的-setValue:forKey:方法
// load先從原類,再調用分類的開始調用
// 也就是調用的順序是
// 原類
// 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捏肢,再加載進行decode時奈籽,會調用setValue:forKey:,把他的方法替換成自身的鸵赫,然后獲取添加的約束
// 作者說明不使用重寫這個KVC方法的方式衣屏,是因為這樣會覆蓋view本身在這個方法中進行的操作
- (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ā)對應屬性的setter方法辩棒,所以這里執(zhí)行手動調用
self.fd_collapsibleConstraints = value;
} else {
// Forward the rest of KVC's to original implementation.
[self fd_setValue:value forKey:key];
}
}
- 上面使用Method Swizzling的原因
作者認為是這種類型的IBOutlet不會觸發(fā)其setter方法
狼忱,但是經過測試,注釋掉這段代碼后一睁,系統(tǒng)還是自己觸發(fā)了setter方法
钻弄,說明這種IBOutlet還是可以觸發(fā)setter方法的。所以者吁,即使沒有這一段代碼窘俺,應該也是可行的
-
設置對應的約束值
- 這里給UIView對象提供一個關聯(lián)對象,來判斷是否將約束值清零
- 注意复凳,這里只要傳入的是YES瘤泪,那么,這個UIView對應存入
constraints關聯(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的內容為nil对途,則將view關聯(lián)的constraints對象所有值設置為0 constraint.constant = 0; } else { // 如果view的內容不為nil,則將view關聯(lián)的constraints對象所有值返回成原值 constraint.constant = constraint.fd_originalConstant; } }]; // 設置fd_collapsed關聯(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交換自己實現(xiàn)的-fd_updateConstraints和系統(tǒng)的updateConstraints方法
- [self fd_updateConstraints]調用的是self的updateConstraints方法掀宋,fd_updateConstraints和updateConstraints方法的IMP,即方法實現(xiàn)已經調換了
- 可以看到仲锄,加入這里不使用Method Swizzling劲妙,那么要實現(xiàn)在更新約束時就需要`重寫updateConstraints`方法,而這只能在`繼承UIView`的情況下才能完成的儒喊;而實用了Method Swizzling镣奋,就可以直接在`分類`中實現(xiàn)在`調用系統(tǒng)updateConstraints的前提下`,又`添加自己想要執(zhí)行的附加代碼`
- `intrinsicContentSize(控件的內置大小)`默認為UIViewNoIntrinsicMetric怀愧,當`控件中沒有內容時`侨颈,調用intrinsicContentSize返回的即為`默認值`,詳情參考([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);
// 當設置控件顯示內容為nil時芯义,計算出來的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)) {
// 當控件沒有內容時哈垢,則設置控件關聯(lián)對象constraints的所有約束值為0
self.fd_collapsed = YES;
} else {
// 當控件有內容時,則設置控件關聯(lián)對象constraints的所有約束值返回為原值
self.fd_collapsed = NO;
}
}
}
-
設置一些動態(tài)屬性(關聯(lián)對象)
- 給UIView關聯(liá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;
}
##總結
總體來說耘分,在分類中要想實現(xiàn)相對復雜的邏輯,卻`不能添加成員變量`,也`不想對需要操作的類進行繼承`求泰,這時就需要runtime中的`關聯(lián)對象和MethodSwizzling`技術了央渣。
forkingdog系列分類都用到了runtime的一些知識,代碼簡潔注釋齊全風格也不錯渴频,比較適合需要學習runtime應用知識的我芽丹。