博客地址:張飛的技術博客
在對導航欄進行深度學習的時候,在網上發現@我就叫Sunny怎么了
開源了一個導航欄返回手勢的庫FDFullscreenPopGesture,我看了看源代碼,作者使用Runtime的一些知識實現的,今天我就借這個庫的源代碼進行Runtime的用法進行學習。
如果看過我前面幾篇關于Runtime的文章,應該知道Runtime的消息發送機制的原理是對象根據方法編號SEL去映射表查找對應的方法實現。因此在運行時階段我們利用Runtime的一些方法可以幫助我們實現用正常方法很難辦到的事情。
1.給分類動態添加屬性
在FDFullscreenPopGesture
中給UIViewController的分類里有這么一個屬性:
@property (nonatomic, copy) _FDViewControllerWillAppearInjectBlock fd_willAppearInjectBlock;
這是一個block的屬性,block定義如下:
typedef void (^_FDViewControllerWillAppearInjectBlock)(UIViewController *viewController, BOOL animated);
看到這里也許你會提問,OC中不是不能給分類添加屬性么?正常情況下,OC是不允許給OC添加屬性的。但是利用Runtime的特性,這是可以辦到的。實現方法如下:
- (_FDViewControllerWillAppearInjectBlock)fd_willAppearInjectBlock
{
return objc_getAssociatedObject(self, _cmd);// 根據關聯的key,獲取關聯的值。這里的key等于_cmd,_cmd等于fd_willAppearInjectBlock
}
- (void)setFd_willAppearInjectBlock:(_FDViewControllerWillAppearInjectBlock)block
{
// 第一個參數:給哪個對象添加關聯
// 第二個參數:關聯的key,通過這個key獲取
// 第三個參數:關聯的value
// 第四個參數:關聯的策略
objc_setAssociatedObject(self, @selector(fd_willAppearInjectBlock), block, OBJC_ASSOCIATION_COPY_NONATOMIC);//關聯對象
}
動態給分類添加屬性的方法是:
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
獲取這個屬性的方法是:
objc_getAssociatedObject(id object, const void *key)
還有一個方法是移除屬性:
objc_removeAssociatedObjects(id object)
是的,這樣就動態的給UIViewController
的分類添加了fd_willAppearInjectBlock
這么一個屬性。
NOTE:在使用Runtime的這些方法的時候不要忘了導入
objc/runtime.h
這個頭文件哦!
2.動態添加方法
要想動態添加方法我們必須了解方法是如何執行的,通常我們調用方法是通過[object message]
這種方法,除了這種方法還有一種是比較少用的,就是[object performSelector:@selector(message)]
這種方式。通過下面這張圖我們可以了解一下他們對消息的處理的不同之處。
通過上圖,我們可以得知,要想動態添加方法必須是通過[object performSelector:@selector(message)]
這種方式調用方法才能在運行時階段通過Runtime的一些方法達到動態的添加方法。如果現在有一個Person
類,在其它地方通過performSelector
的方式調用Person
的run
方法。但是Person
類中并沒有實現這個方法。
Person p = [Person alloc] init];
// 這個時候即使Person類沒有實現run方法編譯器也不會報錯
[p performSelector:@selector(run)];
這時候只需要在Person
中實現resolveInstanceMethod:
方法就可以達到動態添加方法的目的。
//首先我們要在Person類里面實現我們要動態添加的方法
// 要注意,默認方法都有兩個隱式參數
void run(id self,SEL sel){
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 當一個對象調用未實現的方法,會調用這個方法處理,并且會把對應的方法列表傳過來.
// 剛好可以用來判斷未實現的方法是不是我們想要動態添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
//先判斷一下傳過來的是不是run方法
if (sel == @selector(run)){
//如果是run方法就動態添加run方法
class_addMethod(self.class, @selector(run),(IMP)run, "v@:");
// 第一個參數:給哪個類添加方法
// 第二個參數:添加方法的方法編號
// 第三個參數:添加方法的函數實現(函數地址),如果是OC方法
//可以用+(IMP)instanceMethodForSelector:(SEL)aSelector;獲得方法的實現。
// 第四個參數:方法的簽名,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd
}
}
這樣就達到了給一個類動態添加方法的效果了,如果想把方法轉發給其他的類實現,需要處理消息轉發的第二或第三個函數了。
3.替換系統自帶的方法
當一些時候,系統自帶效果滿足不了我們的時候,要么我們自定義,要么直接替換系統的方法。在公有的API是沒有方法辦到的。我們來看一段FDFullscreenPopGesture
的代碼(注釋是我加的):
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
//獲取系統方法的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);
//為了安全起見,先判斷是否已經存在要交換的方法
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
{
//不要認為這句代碼有錯,其實很好理解,在調用這句的時候方法已經交換了
// Forward to primary implementation.
[self fd_viewWillAppear:animated];
if (self.fd_willAppearInjectBlock) {
self.fd_willAppearInjectBlock(self, animated);
}
}
通過上面的代碼我們可以看出來,替換系統自帶的方式實現需要用到的重要方法是method_exchangeImplementations()
方法,并且要注意替換方法里面對自己的調用。這個方法也就是人們常說的Method Swizzling
黑魔法,用的時候要注意,這是一把雙刃劍!
結尾
Runtime在項目中很少用,但是要理解它,理解了之后用起來也不危險。如果你喜歡我的文章,不妨掃一掃下面的二維碼請我喝杯茶。祝大家在iOS開發的道路上玩得愉快!