本文Demo傳送門:MethodSwizzlingDemogit
摘要:編程,只瞭解原理不行,必須實戰才能知道應用場景。本系列嘗試闡述runtime相關理論的同時介紹一些實戰場景,而本文則是本系列的方法交換篇。本文中,第一節將介紹方法交換及注意點,第二節將總結一下方法交換相關的API,第三節將介紹方法交換幾種的實戰場景:統計VC加載次數並打印,防止UI控件短期屢次激活事件,防奔潰處理(數組越界問題)。github
Method Swizzing是發生在運行時的,主要用於在運行時將兩個Method進行交換,咱們能夠將Method Swizzling代碼寫到任何地方,可是隻有在這段Method Swilzzling代碼執行完畢以後互換才起做用。編程
先給要替換的方法的類添加一個Category,而後在Category中的+(void)load
方法中添加Method Swizzling方法,咱們用來替換的方法也寫在這個Category中。設計模式
因爲load類方法是程序運行時這個類被加載到內存中就調用的一個方法,執行比較早,而且不須要咱們手動調用。數組
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
複製代碼
method_getImplementation(Method _Nonnull m)
複製代碼
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
複製代碼
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
複製代碼
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
複製代碼
#import "UIViewController+Logging.h"
#import <objc/runtime.h>
@implementation UIViewController (Logging)
+ (void)load
{
swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}
- (void)swizzled_viewDidAppear:(BOOL)animated
{
// call original implementation
[self swizzled_viewDidAppear:animated];
// Logging
NSLog(@"%@", NSStringFromClass([self class]));
}
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
// the method might not exist in the class, but in its superclass
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// class_addMethod will fail if original method already exists
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// the method doesn’t exist and we just added one
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
複製代碼
需求bash
當前項目寫好的按鈕,尚未全局地控制他們短期內不可連續點擊(也許有過零星地在某些網絡請求接口以前作過一些控制)。如今來了新需求:本APP全部的按鈕1秒內不可連續點擊。你怎麼作?一個個改?這種低效率低維護度確定是不妥的。服務器
方案網絡
給按鈕添加分類,並添加一個點擊事件間隔的屬性,執行點擊事件的時候判斷一下是否時間到了,若是時間不到,那麼攔截點擊事件。ide
怎麼攔截點擊事件呢?其實點擊事件在runtime裏面是發送消息,咱們能夠把要發送的消息的SEL 和本身寫的SEL交換一下,而後在本身寫的SEL裏面判斷是否執行點擊事件。函數
實踐
UIButton是UIControl的子類,於是根據UIControl新建一個分類便可
#import "UIControl+Limit.h"
#import <objc/runtime.h>
static const char *UIControl_acceptEventInterval="UIControl_acceptEventInterval";
static const char *UIControl_ignoreEvent="UIControl_ignoreEvent";
@implementation UIControl (Limit)
#pragma mark - acceptEventInterval
- (void)setAcceptEventInterval:(NSTimeInterval)acceptEventInterval
{
objc_setAssociatedObject(self,UIControl_acceptEventInterval, @(acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSTimeInterval)acceptEventInterval {
return [objc_getAssociatedObject(self,UIControl_acceptEventInterval) doubleValue];
}
#pragma mark - ignoreEvent
-(void)setIgnoreEvent:(BOOL)ignoreEvent{
objc_setAssociatedObject(self,UIControl_ignoreEvent, @(ignoreEvent), OBJC_ASSOCIATION_ASSIGN);
}
-(BOOL)ignoreEvent{
return [objc_getAssociatedObject(self,UIControl_ignoreEvent) boolValue];
}
#pragma mark - Swizzling
+(void)load {
Method a = class_getInstanceMethod(self,@selector(sendAction:to:forEvent:));
Method b = class_getInstanceMethod(self,@selector(swizzled_sendAction:to:forEvent:));
method_exchangeImplementations(a, b);//交換方法
}
- (void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event
{
if(self.ignoreEvent){
NSLog(@"btnAction is intercepted");
return;}
if(self.acceptEventInterval>0){
self.ignoreEvent=YES;
[self performSelector:@selector(setIgnoreEventWithNo) withObject:nil afterDelay:self.acceptEventInterval];
}
[self swizzled_sendAction:action to:target forEvent:event];
}
-(void)setIgnoreEventWithNo{
self.ignoreEvent=NO;
}
@end
複製代碼
-(void)setupSubViews{
UIButton *btn = [UIButton new];
btn =[[UIButton alloc]initWithFrame:CGRectMake(100,100,100,40)];
[btn setTitle:@"btnTest"forState:UIControlStateNormal];
[btn setTitleColor:[UIColor redColor]forState:UIControlStateNormal];
btn.acceptEventInterval = 3;
[self.view addSubview:btn];
[btn addTarget:self action:@selector(btnAction)forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnAction{
NSLog(@"btnAction is executed");
}
複製代碼
需求
在實際工程中,可能在一些地方(好比取出網絡響應數據)進行了數組NSArray取數據的操做,並且之前的小哥們也沒有進行防越界處理。測試方一不當心也沒有測出數組越界狀況下奔潰(由於返回的數據是動態的),結果覺得沒有問題了,其實還隱藏的生產事故的風險。
這時APP負責人說了,即便APP即便不能工做也不能Crash,這是最低的底線。那麼這對數組越界的狀況下的奔潰,你有沒有辦法攔截?
思路:對NSArray的objectAtIndex:
方法進行Swizzling,替換一個有處理邏輯的方法。可是,這時候仍是有個問題,就是類簇的Swizzling沒有那麼簡單。
類簇
在iOS中NSNumber、NSArray、NSDictionary等這些類都是類簇(Class Clusters),一個NSArray的實現可能由多個類組成。因此若是想對NSArray進行Swizzling,必須獲取到其**「真身」**進行Swizzling,直接對NSArray進行操做是無效的。這是由於Method Swizzling對NSArray這些的類簇是不起做用的。
由於這些類簇類,實際上是一種抽象工廠的設計模式。抽象工廠內部有不少其它繼承自當前類的子類,抽象工廠類會根據不一樣狀況,建立不一樣的抽象對象來進行使用。例如咱們調用NSArray的objectAtIndex:
方法,這個類會在方法內部判斷,內部建立不一樣抽象類進行操做。
因此若是咱們對NSArray類進行Swizzling操做其實只是對父類進行了操做,在NSArray內部會建立其餘子類來執行操做,真正執行Swizzling操做的並非NSArray自身,因此咱們應該對其「真身」進行操做。
下面列舉了NSArray和NSDictionary本類的類名,能夠經過Runtime函數取出本類:
類名 | 真身 |
---|---|
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
實踐
好啦,新建一個分類,直接用代碼實現,看看怎麼取出真身的:
@implementation NSArray (CrashHandle)
// Swizzling核心代碼
// 須要注意的是,好多同窗反饋下面代碼不起做用,形成這個問題的緣由大多都是其調用了super load方法。在下面的load方法中,不該該調用父類的load方法。
+ (void)load {
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cm_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
// 爲了不和系統的方法衝突,我通常都會在swizzling方法前面加前綴
- (id)cm_objectAtIndex:(NSUInteger)index {
// 判斷下標是否越界,若是越界就進入異常攔截
if (self.count-1 < index) {
@try {
return [self cm_objectAtIndex:index];
}
@catch (NSException *exception) {
// 在崩潰後會打印崩潰信息。若是是線上,能夠在這裏將崩潰信息發送到服務器
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
}
@finally {}
} // 若是沒有問題,則正常進行方法調用
else {
return [self cm_objectAtIndex:index];
}
}
複製代碼
這裏面可能有個誤會,- (id)cm_objectAtIndex:(NSUInteger)index {
裏面調用了自身?這是遞歸嗎?其實不是。這個時候方法替換已經有效了,cm_objectAtIndex
這個SEL指向的實際上是原來系統的objectAtIndex:
的IMP。於是不是遞歸。
- (void)viewDidLoad {
[super viewDidLoad];
// 測試代碼
NSArray *array = @[@0, @1, @2, @3];
[array objectAtIndex:3];
//原本要奔潰的
[array objectAtIndex:4];
}
複製代碼
運行以後,發現沒有崩潰,並打印了相關信息,以下所示。