博客地址:張飛的技術(shù)博客
在對(duì)導(dǎo)航欄進(jìn)行深度學(xué)習(xí)的時(shí)候瞪慧,在網(wǎng)上發(fā)現(xiàn)@我就叫Sunny怎么了
開(kāi)源了一個(gè)導(dǎo)航欄返回手勢(shì)的庫(kù)FDFullscreenPopGesture,我看了看源代碼炒刁,作者使用Runtime的一些知識(shí)實(shí)現(xiàn)的陆爽,今天我就借這個(gè)庫(kù)的源代碼進(jìn)行Runtime的用法進(jìn)行學(xué)習(xí)嫂用。
如果看過(guò)我前面幾篇關(guān)于Runtime的文章怜跑,應(yīng)該知道Runtime的消息發(fā)送機(jī)制的原理是對(duì)象根據(jù)方法編號(hào)SEL去映射表查找對(duì)應(yīng)的方法實(shí)現(xiàn)。因此在運(yùn)行時(shí)階段我們利用Runtime的一些方法可以幫助我們實(shí)現(xiàn)用正常方法很難辦到的事情伪窖。
1.給分類(lèi)動(dòng)態(tài)添加屬性
在FDFullscreenPopGesture
中給UIViewController的分類(lèi)里有這么一個(gè)屬性:
@property (nonatomic, copy) _FDViewControllerWillAppearInjectBlock fd_willAppearInjectBlock;
這是一個(gè)block的屬性,block定義如下:
typedef void (^_FDViewControllerWillAppearInjectBlock)(UIViewController *viewController, BOOL animated);
看到這里也許你會(huì)提問(wèn)居兆,OC中不是不能給分類(lèi)添加屬性么覆山?正常情況下,OC是不允許給OC添加屬性的泥栖。但是利用Runtime的特性簇宽,這是可以辦到的。實(shí)現(xiàn)方法如下:
- (_FDViewControllerWillAppearInjectBlock)fd_willAppearInjectBlock
{
return objc_getAssociatedObject(self, _cmd);// 根據(jù)關(guān)聯(lián)的key吧享,獲取關(guān)聯(lián)的值魏割。這里的key等于_cmd,_cmd等于fd_willAppearInjectBlock
}
- (void)setFd_willAppearInjectBlock:(_FDViewControllerWillAppearInjectBlock)block
{
// 第一個(gè)參數(shù):給哪個(gè)對(duì)象添加關(guān)聯(lián)
// 第二個(gè)參數(shù):關(guān)聯(lián)的key,通過(guò)這個(gè)key獲取
// 第三個(gè)參數(shù):關(guān)聯(lián)的value
// 第四個(gè)參數(shù):關(guān)聯(lián)的策略
objc_setAssociatedObject(self, @selector(fd_willAppearInjectBlock), block, OBJC_ASSOCIATION_COPY_NONATOMIC);//關(guān)聯(lián)對(duì)象
}
動(dòng)態(tài)給分類(lèi)添加屬性的方法是:
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
獲取這個(gè)屬性的方法是:
objc_getAssociatedObject(id object, const void *key)
還有一個(gè)方法是移除屬性:
objc_removeAssociatedObjects(id object)
是的钢颂,這樣就動(dòng)態(tài)的給UIViewController
的分類(lèi)添加了fd_willAppearInjectBlock
這么一個(gè)屬性钞它。
NOTE:在使用Runtime的這些方法的時(shí)候不要忘了導(dǎo)入
objc/runtime.h
這個(gè)頭文件哦!
2.動(dòng)態(tài)添加方法
要想動(dòng)態(tài)添加方法我們必須了解方法是如何執(zhí)行的殊鞭,通常我們調(diào)用方法是通過(guò)[object message]
這種方法遭垛,除了這種方法還有一種是比較少用的,就是[object performSelector:@selector(message)]
這種方式操灿。通過(guò)下面這張圖我們可以了解一下他們對(duì)消息的處理的不同之處锯仪。
通過(guò)上圖,我們可以得知趾盐,要想動(dòng)態(tài)添加方法必須是通過(guò)[object performSelector:@selector(message)]
這種方式調(diào)用方法才能在運(yùn)行時(shí)階段通過(guò)Runtime的一些方法達(dá)到動(dòng)態(tài)的添加方法庶喜。如果現(xiàn)在有一個(gè)Person
類(lèi)小腊,在其它地方通過(guò)performSelector
的方式調(diào)用Person
的run
方法。但是Person
類(lèi)中并沒(méi)有實(shí)現(xiàn)這個(gè)方法溃卡。
Person p = [Person alloc] init];
// 這個(gè)時(shí)候即使Person類(lèi)沒(méi)有實(shí)現(xiàn)run方法編譯器也不會(huì)報(bào)錯(cuò)
[p performSelector:@selector(run)];
這時(shí)候只需要在Person
中實(shí)現(xiàn)resolveInstanceMethod:
方法就可以達(dá)到動(dòng)態(tài)添加方法的目的溢豆。
//首先我們要在Person類(lèi)里面實(shí)現(xiàn)我們要?jiǎng)討B(tài)添加的方法
// 要注意,默認(rèn)方法都有兩個(gè)隱式參數(shù)
void run(id self,SEL sel){
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 當(dāng)一個(gè)對(duì)象調(diào)用未實(shí)現(xiàn)的方法瘸羡,會(huì)調(diào)用這個(gè)方法處理,并且會(huì)把對(duì)應(yīng)的方法列表傳過(guò)來(lái).
// 剛好可以用來(lái)判斷未實(shí)現(xiàn)的方法是不是我們想要?jiǎng)討B(tài)添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
//先判斷一下傳過(guò)來(lái)的是不是run方法
if (sel == @selector(run)){
//如果是run方法就動(dòng)態(tài)添加run方法
class_addMethod(self.class, @selector(run),(IMP)run, "v@:");
// 第一個(gè)參數(shù):給哪個(gè)類(lèi)添加方法
// 第二個(gè)參數(shù):添加方法的方法編號(hào)
// 第三個(gè)參數(shù):添加方法的函數(shù)實(shí)現(xiàn)(函數(shù)地址),如果是OC方法
//可以用+(IMP)instanceMethodForSelector:(SEL)aSelector;獲得方法的實(shí)現(xiàn)漩仙。
// 第四個(gè)參數(shù):方法的簽名,(返回值+參數(shù)類(lèi)型) v:void @:對(duì)象->self :表示SEL->_cmd
}
}
這樣就達(dá)到了給一個(gè)類(lèi)動(dòng)態(tài)添加方法的效果了犹赖,如果想把方法轉(zhuǎn)發(fā)給其他的類(lèi)實(shí)現(xiàn)队他,需要處理消息轉(zhuǎn)發(fā)的第二或第三個(gè)函數(shù)了。
3.替換系統(tǒng)自帶的方法
當(dāng)一些時(shí)候峻村,系統(tǒng)自帶效果滿(mǎn)足不了我們的時(shí)候麸折,要么我們自定義,要么直接替換系統(tǒng)的方法粘昨。在公有的API是沒(méi)有方法辦到的垢啼。我們來(lái)看一段FDFullscreenPopGesture
的代碼(注釋是我加的):
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
//獲取系統(tǒng)方法的SEL
SEL originalSelector = @selector(viewWillAppear:);
//獲取替換方法的SEL
SEL swizzledSelector = @selector(fd_viewWillAppear:);
//為了獲取IMP指針,獲得方法的Method
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//為了安全起見(jiàn)张肾,先判斷是否已經(jīng)存在要交換的方法
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)fd_viewWillAppear:(BOOL)animated
{
//不要認(rèn)為這句代碼有錯(cuò)芭析,其實(shí)很好理解,在調(diào)用這句的時(shí)候方法已經(jīng)交換了
// Forward to primary implementation.
[self fd_viewWillAppear:animated];
if (self.fd_willAppearInjectBlock) {
self.fd_willAppearInjectBlock(self, animated);
}
}
通過(guò)上面的代碼我們可以看出來(lái)吞瞪,替換系統(tǒng)自帶的方式實(shí)現(xiàn)需要用到的重要方法是method_exchangeImplementations()
方法馁启,并且要注意替換方法里面對(duì)自己的調(diào)用芍秆。這個(gè)方法也就是人們常說(shuō)的Method Swizzling
黑魔法,用的時(shí)候要注意妖啥,這是一把雙刃劍!
結(jié)尾
Runtime在項(xiàng)目中很少用迹栓,但是要理解它掉分,理解了之后用起來(lái)也不危險(xiǎn)。如果你喜歡我的文章克伊,不妨掃一掃下面的二維碼請(qǐng)我喝杯茶。祝大家在iOS開(kāi)發(fā)的道路上玩得愉快愿吹!