iOS開發(fā)·runtime原理與實(shí)踐: 方法交換篇(Method Swizzling)(iOS“黑魔法”,埋點(diǎn)統(tǒng)計(jì),禁止UI控件連續(xù)點(diǎn)擊,防奔潰處理)

本文Demo傳送門:MethodSwizzlingDemo

摘要:編程,只了解原理不行,必須實(shí)戰(zhàn)才能知道應(yīng)用場景。本系列嘗試闡述runtime相關(guān)理論的同時(shí)介紹一些實(shí)戰(zhàn)場景,而本文則是本系列的方法交換篇。本文中,第一節(jié)將介紹方法交換及注意點(diǎn),第二節(jié)將總結(jié)一下方法交換相關(guān)的API,第三節(jié)將介紹方法交換幾種的實(shí)戰(zhàn)場景:統(tǒng)計(jì)VC加載次數(shù)并打印,防止UI控件短時(shí)間多次激活事件,防奔潰處理(數(shù)組越界問題)。

1. 原理與注意

原理

Method Swizzing是發(fā)生在運(yùn)行時(shí)的,主要用于在運(yùn)行時(shí)將兩個(gè)Method進(jìn)行交換,我們可以將Method Swizzling代碼寫到任何地方,但是只有在這段Method Swilzzling代碼執(zhí)行完畢之后互換才起作用。

用法

先給要替換的方法的類添加一個(gè)Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我們用來替換的方法也寫在這個(gè)Category中。

由于load類方法是程序運(yùn)行時(shí)這個(gè)類被加載到內(nèi)存中就調(diào)用的一個(gè)方法,執(zhí)行比較早,并且不需要我們手動調(diào)用。

注意要點(diǎn)

  • Swizzling應(yīng)該總在+load中執(zhí)行
  • Swizzling應(yīng)該總是在dispatch_once中執(zhí)行
  • Swizzling在+load中執(zhí)行時(shí),不要調(diào)用[super load]。如果多次調(diào)用了[super load],可能會出現(xiàn)“Swizzle無效”的假象。
  • 為了避免Swizzling的代碼被重復(fù)執(zhí)行,我們可以通過GCD的dispatch_once函數(shù)來解決,利用dispatch_once函數(shù)內(nèi)代碼只會執(zhí)行一次的特性。

2. Method Swizzling相關(guān)的API

  • 方案1
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) 
  • 方案2
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 

3. 應(yīng)用場景與實(shí)踐

3.1 統(tǒng)計(jì)VC加載次數(shù)并打印

  • UIViewController+Logging.m
#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);
    }
    
}

3.2 防止UI控件短時(shí)間多次激活事件

需求

當(dāng)前項(xiàng)目寫好的按鈕,還沒有全局地控制他們短時(shí)間內(nèi)不可連續(xù)點(diǎn)擊(也許有過零星地在某些網(wǎng)絡(luò)請求接口之前做過一些控制)。現(xiàn)在來了新需求:本APP所有的按鈕1秒內(nèi)不可連續(xù)點(diǎn)擊。你怎么做?一個(gè)個(gè)改?這種低效率低維護(hù)度肯定是不妥的。

方案

給按鈕添加分類,并添加一個(gè)點(diǎn)擊事件間隔的屬性,執(zhí)行點(diǎn)擊事件的時(shí)候判斷一下是否時(shí)間到了,如果時(shí)間不到,那么攔截點(diǎn)擊事件。

怎么攔截點(diǎn)擊事件呢?其實(shí)點(diǎn)擊事件在runtime里面是發(fā)送消息,我們可以把要發(fā)送的消息的SEL 和自己寫的SEL交換一下,然后在自己寫的SEL里面判斷是否執(zhí)行點(diǎn)擊事件。

實(shí)踐

UIButton是UIControl的子類,因而根據(jù)UIControl新建一個(gè)分類即可

  • UIControl+Limit.m
#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
  • ViewController.m
-(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");
}

3.3 防奔潰處理:數(shù)組越界問題

需求

在實(shí)際工程中,可能在一些地方(比如取出網(wǎng)絡(luò)響應(yīng)數(shù)據(jù))進(jìn)行了數(shù)組NSArray取數(shù)據(jù)的操作,而且以前的小哥們也沒有進(jìn)行防越界處理。測試方一不小心也沒有測出數(shù)組越界情況下奔潰(因?yàn)榉祷氐臄?shù)據(jù)是動態(tài)的),結(jié)果以為沒有問題了,其實(shí)還隱藏的生產(chǎn)事故的風(fēng)險(xiǎn)。

這時(shí)APP負(fù)責(zé)人說了,即使APP即使不能工作也不能Crash,這是最低的底線。那么這對數(shù)組越界的情況下的奔潰,你有沒有辦法攔截?

思路:對NSArray的objectAtIndex:方法進(jìn)行Swizzling,替換一個(gè)有處理邏輯的方法。但是,這時(shí)候還是有個(gè)問題,就是類簇的Swizzling沒有那么簡單。

類簇

在iOS中NSNumber、NSArray、NSDictionary等這些類都是類簇(Class Clusters),一個(gè)NSArray的實(shí)現(xiàn)可能由多個(gè)類組成。所以如果想對NSArray進(jìn)行Swizzling,必須獲取到其“真身”進(jìn)行Swizzling,直接對NSArray進(jìn)行操作是無效的。這是因?yàn)镸ethod Swizzling對NSArray這些的類簇是不起作用的。

因?yàn)檫@些類簇類,其實(shí)是一種抽象工廠的設(shè)計(jì)模式。抽象工廠內(nèi)部有很多其它繼承自當(dāng)前類的子類,抽象工廠類會根據(jù)不同情況,創(chuàng)建不同的抽象對象來進(jìn)行使用。例如我們調(diào)用NSArray的objectAtIndex:方法,這個(gè)類會在方法內(nèi)部判斷,內(nèi)部創(chuàng)建不同抽象類進(jìn)行操作。

所以如果我們對NSArray類進(jìn)行Swizzling操作其實(shí)只是對父類進(jìn)行了操作,在NSArray內(nèi)部會創(chuàng)建其他子類來執(zhí)行操作,真正執(zhí)行Swizzling操作的并不是NSArray自身,所以我們應(yīng)該對其“真身”進(jìn)行操作。

下面列舉了NSArray和NSDictionary本類的類名,可以通過Runtime函數(shù)取出本類:

類名 真身
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM

實(shí)踐

好啦,新建一個(gè)分類,直接用代碼實(shí)現(xiàn),看看怎么取出真身的:

  • NSArray+CrashHandle.m
@implementation NSArray (CrashHandle)

// Swizzling核心代碼
// 需要注意的是,好多同學(xué)反饋下面代碼不起作用,造成這個(gè)問題的原因大多都是其調(diào)用了super load方法。在下面的load方法中,不應(yīng)該調(diào)用父類的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);
}

// 為了避免和系統(tǒng)的方法沖突,我一般都會在swizzling方法前面加前綴
- (id)cm_objectAtIndex:(NSUInteger)index {
    // 判斷下標(biāo)是否越界,如果越界就進(jìn)入異常攔截
    if (self.count-1 < index) {
        @try {
            return [self cm_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 在崩潰后會打印崩潰信息。如果是線上,可以在這里將崩潰信息發(fā)送到服務(wù)器
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        }
        @finally {}
    } // 如果沒有問題,則正常進(jìn)行方法調(diào)用
    else {
        return [self cm_objectAtIndex:index];
    }
}

這里面可能有個(gè)誤會,- (id)cm_objectAtIndex:(NSUInteger)index {里面調(diào)用了自身?這是遞歸嗎?其實(shí)不是。這個(gè)時(shí)候方法替換已經(jīng)有效了,cm_objectAtIndex這個(gè)SEL指向的其實(shí)是原來系統(tǒng)的objectAtIndex:的IMP。因而不是遞歸。

  • ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 測試代碼
    NSArray *array = @[@0, @1, @2, @3];
    [array objectAtIndex:3];
    //本來要奔潰的
    [array objectAtIndex:4];
}

運(yùn)行之后,發(fā)現(xiàn)沒有崩潰,并打印了相關(guān)信息,如下所示。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內(nèi)容