iOS 開發:『Runtime』詳解(二)Method Swizzling

  • 本文首發於個人我的博客:『不羈閣』
  • 文章連接:傳送門
  • 本文更新時間:2019年07月12日13:21:26

本文用來介紹 iOS 開發中『Runtime』中的黑魔法 Method Swizzling。經過本文,您將瞭解到:html

  1. Method Swizzling(動態方法交換)簡介
  2. Method Swizzling 使用方法(四種方案)
  3. Method Swizzling 使用注意
  4. Method Swizzling 應用場景 4.1 全局頁面統計功能 4.2 字體根據屏幕尺寸適配 4.3 處理按鈕重複點擊 4.4 TableView、CollectionView 異常加載佔位圖 4.5 APM(應用性能管理)、防止崩潰

文中示例代碼在: bujige / YSC-Runtime-MethodSwizzlinggit


咱們在上一篇 iOS 開發:『Runtime』詳解(一)基礎知識 中,講解了 iOS 運行時機制(Runtime 系統)的工做原理。包括消息發送以及轉發機制的原理和流程。github

從這一篇文章開始,咱們來了解一下 Runtime 在實際開發過程當中,具體的應用場景。objective-c

這一篇咱們來學習一下被稱爲 Runtime 運行時系統中最具爭議的黑魔法:Method Swizzling(動態方法交換)數組


1. Method Swizzling(動態方法交換)簡介

Method Swizzling 用於改變一個已經存在的 selector 實現。咱們能夠在程序運行時,經過改變 selector 所在 Class(類)的 method list(方法列表)的映射從而改變方法的調用。其實質就是交換兩個方法的 IMP(方法實現)。安全

上一篇文章中咱們知道:Method(方法)對應的是 objc_method 結構體;而 objc_method 結構體 中包含了 SEL method_name(方法名)IMP method_imp(方法實現)bash

// objc_method 結構體
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name;                    // 方法名
    char * _Nullable method_types;               // 方法類型
    IMP _Nonnull method_imp;                     // 方法實現
};
複製代碼

Method(方法)SEL(方法名)IMP(方法實現)三者的關係能夠這樣來表示:網絡

在運行時,Class(類) 維護了一個 method list(方法列表) 來肯定消息的正確發送。method list(方法列表) 存放的元素就是 Method(方法)。而 Method(方法) 中映射了一對鍵值對:SEL(方法名):IMP(方法實現)session

Method swizzling 修改了 method list(方法列表),使得不一樣 Method(方法)中的鍵值對發生了交換。好比交換前兩個鍵值對分別爲 SEL A : IMP ASEL B : IMP B,交換以後就變爲了 SEL A : IMP BSEL B : IMP A。如圖所示:框架


2. Method Swizzling 使用方法

假如當前類中有兩個方法:- (void)originalFunction;- (void)swizzledFunction;。若是咱們想要交換兩個方法的實現,從而實現調用 - (void)originalFunction; 方法實際上調用的是 - (void)swizzledFunction; 方法,而調用 - (void)swizzledFunction; 方法實際上調用的是 - (void)originalFunction; 方法的效果。那麼咱們須要像下邊代碼同樣來實現。


2.1 Method Swizzling 簡單使用

在當前類的 + (void)load; 方法中增長 Method Swizzling 操做,交換 - (void)originalFunction;- (void)swizzledFunction; 的方法實現。

#import "ViewController.h"
#import <objc/runtime.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self SwizzlingMethod];
    [self originalFunction];
    [self swizzledFunction];
}


// 交換 原方法 和 替換方法 的方法實現
- (void)SwizzlingMethod {
    // 當前類
    Class class = [self class];
    
    // 原方法名 和 替換方法名
    SEL originalSelector = @selector(originalFunction);
    SEL swizzledSelector = @selector(swizzledFunction);
    
    // 原方法結構體 和 替換方法結構體
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    // 調用交換兩個方法的實現
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

// 原始方法
- (void)originalFunction {
    NSLog(@"originalFunction");
}

// 替換方法
- (void)swizzledFunction {
    NSLog(@"swizzledFunction");
}

@end
複製代碼

打印結果: 2019-07-12 09:59:19.672349+0800 Runtime-MethodSwizzling[91009:30112833] swizzledFunction 2019-07-12 09:59:20.414930+0800 Runtime-MethodSwizzling[91009:30112833] originalFunction

能夠看出二者方法成功進行了交換。


剛纔咱們簡單演示瞭如何在當前類中如何進行 Method Swizzling 操做。但通常平常開發中,並非直接在原有類中進行 Method Swizzling 操做。更多的是爲當前類添加一個分類,而後在分類中進行 Method Swizzling 操做。另外真正使用會比上邊寫的考慮東西要多一點,要複雜一些。

在平常使用 Method Swizzling 的過程當中,有幾種很經常使用的方案,具體狀況以下。

2.2 Method Swizzling 方案 A

在該類的分類中添加 Method Swizzling 交換方法,用普通方式

這種方式在開發中應用最多的。可是仍是要注意一些事項,我會在接下來的 3. Method Swizzling 使用注意 進行詳細說明。

@implementation UIViewController (Swizzling)

// 交換 原方法 和 替換方法 的方法實現
+ (void)load {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 當前類
        Class class = [self class];
        
        // 原方法名 和 替換方法名
        SEL originalSelector = @selector(originalFunction);
        SEL swizzledSelector = @selector(swizzledFunction);
        
        // 原方法結構體 和 替換方法結構體
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        /* 若是當前類沒有 原方法的 IMP,說明在從父類繼承過來的方法實現, * 須要在當前類中添加一個 originalSelector 方法, * 可是用 替換方法 swizzledMethod 去實現它 */
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            // 原方法的 IMP 添加成功後,修改 替換方法的 IMP 爲 原始方法的 IMP
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            // 添加失敗(說明已包含原方法的 IMP),調用交換兩個方法的實現
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

// 原始方法
- (void)originalFunction {
    NSLog(@"originalFunction");
}

// 替換方法
- (void)swizzledFunction {
    NSLog(@"swizzledFunction");
}

@end
複製代碼

2.3 Method Swizzling 方案 B

在該類的分類中添加 Method Swizzling 交換方法,可是使用函數指針的方式。

方案 B 和方案 A 的最大不一樣之處在於使用了函數指針的方式,使用函數指針最大的好處是能夠有效避免命名錯誤。

#import "UIViewController+PointerSwizzling.h"
#import <objc/runtime.h>

typedef IMP *IMPPointer;

// 交換方法函數
static void MethodSwizzle(id self, SEL _cmd, id arg1);
// 原始方法函數指針
static void (*MethodOriginal)(id self, SEL _cmd, id arg1);

// 交換方法函數
static void MethodSwizzle(id self, SEL _cmd, id arg1) {
    
    // 在這裏添加 交換方法的相關代碼
    NSLog(@"swizzledFunc");
    
    MethodOriginal(self, _cmd, arg1);
}

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}

@implementation UIViewController (PointerSwizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzle:@selector(originalFunc) with:(IMP)MethodSwizzle store:(IMP *)&MethodOriginal];
    });
}

+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}

// 原始方法
- (void)originalFunc {
    NSLog(@"originalFunc");
}

@end
複製代碼

2.4 Method Swizzling 方案 C

在其餘類中添加 Method Swizzling 交換方法

這種狀況通常用的很少,最出名的就是 AFNetworking 中的_AFURLSessionTaskSwizzling 私有類。_AFURLSessionTaskSwizzling 主要解決了 iOS7 和 iOS8 系統上 NSURLSession 差異的處理。讓不一樣系統版本 NSURLSession 版本基本一致。

static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) {
    Method originalMethod = class_getInstanceMethod(theClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector);
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) {
    return class_addMethod(theClass, selector,  method_getImplementation(method),  method_getTypeEncoding(method));
}

@interface _AFURLSessionTaskSwizzling : NSObject

@end

@implementation _AFURLSessionTaskSwizzling

+ (void)load {
    if (NSClassFromString(@"NSURLSessionTask")) {
        
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
        NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"
        NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
#pragma clang diagnostic pop
        IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
        Class currentClass = [localDataTask class];
        
        while (class_getInstanceMethod(currentClass, @selector(resume))) {
            Class superClass = [currentClass superclass];
            IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));
            IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));
            if (classResumeIMP != superclassResumeIMP &&
                originalAFResumeIMP != classResumeIMP) {
                [self swizzleResumeAndSuspendMethodForClass:currentClass];
            }
            currentClass = [currentClass superclass];
        }
        
        [localDataTask cancel];
        [session finishTasksAndInvalidate];
    }
}

+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass {
    Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume));
    Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend));

    if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) {
        af_swizzleSelector(theClass, @selector(resume), @selector(af_resume));
    }

    if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) {
        af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend));
    }
}

- (void)af_resume {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    [self af_resume];
    
    if (state != NSURLSessionTaskStateRunning) {
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self];
    }
}

- (void)af_suspend {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    [self af_suspend];
    
    if (state != NSURLSessionTaskStateSuspended) {
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidSuspendNotification object:self];
    }
}
複製代碼

2.5 Method Swizzling 方案 D

優秀的第三方框架:JRSwizzleRSSwizzle

JRSwizzle 和 RSSwizzle 都是優秀的封裝 Method Swizzling 的第三方框架。

  1. JRSwizzle 嘗試解決在不一樣平臺和系統版本上的 Method Swizzling 與類繼承關係的衝突。對各平臺低版本系統兼容性較強。JRSwizzle 核心是用到了 method_exchangeImplementations 方法。在健壯性上先作了 class_addMethod 操做。

  2. RSSwizzle 主要用到了 class_replaceMethod 方法,避免了子類的替換影響了父類。並且對交換方法過程加了鎖,加強了線程安全。它用很複雜的方式解決了 What are the dangers of method swizzling in Objective-C? 中提到的問題。是一種更安全優雅的 Method Swizzling 解決方案。


總結:

在開發中咱們一般使用方案 A,或者方案 D 中的第三方框架 RSSwizzle 來實現 Method Swizzling。在接下來 3. Method Swizzling 使用注意 中,咱們還講看到不少的注意事項。這些注意事項並非爲了嚇退初學者,而是爲了更好的使用 Method Swizzling 這一利器。而至於方案的選擇,不管是選擇哪一種方案,我認爲只有最適合項目的方案纔是最佳方案。


3. Method Swizzling 使用注意

Method Swizzling 之因此被你們稱爲黑魔法,就是由於使用 Method Swizzling 進行方法交換是一個危險的操做。Stack Overflow 上邊有人提出了使用 Method Swizzling 會形成的一些危險和缺陷。更是把 Method Swizzling 比做是廚房裏一把鋒利的刀。有些人會懼怕刀過於鋒利,會傷到本身,從而放棄了刀,或者使用了鈍刀。可是事實倒是:鋒利的刀比鈍刀反而更加安全,前提是你有足夠的經驗。

Method Swizzling 可用於編寫更好,更高效,更易維護的代碼。但也可能由於被濫用而致使可怕的錯誤。因此在使用 Method Swizzling 的時候,咱們仍是要注意一些事項,以規避可能出現的危險。

下面咱們結合還有其餘博主關於 Method Swizzling 的博文、 以及 Stack Overflow 上邊提到的危險和缺陷,還有筆者的我的看法,來綜合說一下使用 Method Swizzling 須要注意的地方。

  1. 應該只在 +load 中執行 Method Swizzling。

程序在啓動的時候,會先加載全部的類,這時會調用每一個類的 +load 方法。並且在整個程序運行週期只會調用一次(不包括外部顯示調用)。因此在 +load 方法進行 Method Swizzling 再好不過了。

而爲何不用 +initialize 方法呢。

由於 +initialize 方法的調用時機是在 第一次向該類發送第一個消息的時候纔會被調用。若是該類只是引用,沒有調用,則不會執行 +initialize 方法。 Method Swizzling 影響的是全局狀態,+load 方法能保證在加載類的時候就進行交換,保證交換結果。而使用 +initialize 方法則不能保證這一點,有可能在使用的時候起不到交換方法的做用。

  1. Method Swizzling 在 +load 中執行時,不要調用 [super load];

上邊咱們說了,程序在啓動的時候,會先加載全部的類。若是在 + (void)load方法中調用 [super load] 方法,就會致使父類的 Method Swizzling 被重複執行兩次,而方法交換也被執行了兩次,至關於互換了一次方法以後,第二次又換回去了,從而使得父類的 Method Swizzling 失效。

  1. Method Swizzling 應該老是在 dispatch_once 中執行。

Method Swizzling 不是原子操做,dispatch_once 能夠保證即便在不一樣的線程中也能確保代碼只執行一次。因此,咱們應該老是在 dispatch_once 中執行 Method Swizzling 操做,保證方法替換隻被執行一次。

  1. 使用 Method Swizzling 後要記得調用原生方法的實現。

在交換方法實現後記得要調用原生方法的實現(除非你很是肯定能夠不用調用原生方法的實現):APIs 提供了輸入輸出的規則,而在輸入輸出中間的方法實現就是一個看不見的黑盒。交換了方法實現而且一些回調方法不會調用原生方法的實現這可能會形成底層實現的崩潰。

  1. 避免命名衝突和參數 _cmd 被篡改。
  1. 避免命名衝突一個比較好的作法是爲替換的方法加個前綴以區別原生方法。必定要確保調用了原生方法的全部地方不會由於本身交換了方法的實現而出現意料不到的結果。 在使用 Method Swizzling 交換方法後記得要在交換方法中調用原生方法的實現。在交換了方法後而且不調用原生方法的實現可能會形成底層實現的崩潰。

  2. 避免方法命名衝突另外一個更好的作法是使用函數指針,也就是上邊提到的 方案 B,這種方案能有效避免方法命名衝突和參數 _cmd 被篡改。

  1. 謹慎對待 Method Swizzling。

使用 Method Swizzling,會改變非本身擁有的代碼。咱們使用 Method Swizzling 一般會更改一些系統框架的對象方法,或是類方法。咱們改變的不僅是一個對象實例,而是改變了項目中全部的該類的對象實例,以及全部子類的對象實例。因此,在使用 Method Swizzling 的時候,應該保持足夠的謹慎。

例如,你在一個類中重寫一個方法,而且不調用 super 方法,則可能會出現問題。在大多數狀況下,super 方法是指望被調用的(除非有特殊說明)。若是你是用一樣的思想來進行 Method Swizzling ,可能就會引發不少問題。若是你不調用原始的方法實現,那麼你 Method Swizzling 改變的越多代碼就越不安全。

  1. 對於 Method Swizzling 來講,調用順序 很重要。

+ load 方法的調用規則爲:

  1. 先調用主類,按照編譯順序,順序地根據繼承關係由父類向子類調用;
  2. 再調用分類,按照編譯順序,依次調用;
  3. + load 方法除非主動調用,不然只會調用一次。

這樣的調用規則致使了 + load 方法調用順序並不必定肯定。一個順序多是:父類 -> 子類 -> 父類類別 -> 子類類別,也多是 父類 -> 子類 -> 子類類別 -> 父類類別。因此 Method Swizzling 的順序不能保證,那麼就不能保證 Method Swizzling 後方法的調用順序是正確的。

因此被用於 Method Swizzling 的方法必須是當前類自身的方法,若是把繼承父類來的 IMP 複製到自身上面可能會存在問題。若是 + load 方法調用順序爲:父類 -> 子類 -> 父類類別 -> 子類類別,那麼形成的影響就是調用子類的替換方法並不能正確調起父類分類的替換方法。緣由解釋能夠參考這篇文章:南梔傾寒:iOS界的毒瘤-MethodSwizzling

關於調用順序更細緻的研究能夠參考這篇博文:玉令天下的博客:Objective-C Method Swizzling


4. Method Swizzling 應用場景

Method Swizzling 能夠交換兩個方法的實現,在開發中更多的是應用於系統類庫,以及第三方框架的方法替換。在官方不公開源碼的狀況下,咱們能夠藉助 Runtime 的 Method Swizzling 爲原有方法添加額外的功能,這使得咱們能夠作不少有趣的事情。


4.1 全局頁面統計功能

需求:在全部頁面添加統計功能,用戶每進入一次頁面就統計一次。

若是有一天公司產品須要咱們來實現這個需求。咱們應該如何來實現?

先來思考一下有幾種實現方式:

第一種:手動添加

直接在全部頁面添加一次統計代碼。你須要作的是寫一份統計代碼,而後在全部頁面的 viewWillAppear: 中不停的進行復制、粘貼。

第二種:利用繼承

建立基類,全部頁面都繼承自基類。這樣的話只須要在基類的 viewDidAppear: 中添加一次統計功能。這樣修改代碼仍是不少,若是全部頁面不是一開始繼承自定義的基類,那麼就須要把全部頁面的繼承關係修改一下,一樣會形成不少重複代碼,和極大的工做量。

第三種:利用分類 + Method Swizzling

咱們能夠利用 Category 的特性來實現這個功能。若是一個類的分類重寫了這個類的方法以後,那麼該類的方法將會失效,起做用的將會是分類中重寫的方法。

這樣的話,咱們能夠爲 UIViewController 創建一個 Category,在分類中重寫 viewWillAppear:,在其中添加統計代碼,而後在全部的控制器中引入這個 Category。可是這樣的話,全部繼承自 UIViewController 類自身的 viewWillAppear: 就失效了,不會被調用。

這就須要用 Method Swizzling 來實現了。步驟以下:

  1. 在分類中實現一個自定義的xxx_viewWillAppear: 方法;
  2. 利用 Method Swizzling 將 viewDidAppear: 和自定義的 xxx_viewWillAppear: 進行方法交換。
  3. 而後在 xxx_viewWillAppear: 中添加統計代碼和調用xxx_viewWillAppear:實現; 由於兩個方法發生了交換,因此最後實質是調用了 viewWillAppear: 方法。
  • 代碼實現:
#import "UIViewController+Swizzling.h"
#import <objc/runtime.h>

@implementation UIViewController (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    
    if (![self isKindOfClass:[UIViewController class]]) {  // 剔除系統 UIViewController
        // 添加統計代碼
        NSLog(@"進入頁面:%@", [self class]);
    }
    
    [self xxx_viewWillAppear:animated];
}

@end
複製代碼

4.2 字體根據屏幕尺寸適配

需求:全部的控件字體必須依據屏幕的尺寸等比縮放。

照例,咱們先來想一想幾種實現方式。

第一種:手動修改

全部用到的 UIFont 的地方,手動判斷,添加適配代碼。一想到那個工做量,不忍直視。

第二種:利用宏定義

在 PCH 文件定義一個計算縮放字體的方法。在使用設置字體時,先調用宏定義的縮放字體的方法。可是這樣一樣須要修改全部用到的 UIFont 的地方。工做量依舊很大。

//宏定義
#define UISCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width)

/**
 *  計算縮放字體的方法
 */
static inline CGFloat FontSize(CGFloat fontSize){
    return fontSize * UISCREEN_WIDTH / XXX_UISCREEN_WIDTH;
}
複製代碼

第三種:利用分類 + Method Swizzling

  1. 爲 UIFont 創建一個 Category。
  2. 在分類中實現一個自定義的 xxx_systemFontOfSize: 方法,在其中添加縮放字體的方法。
  3. 利用 Method Swizzling 將 systemFontOfSize: 方法和 xxx_systemFontOfSize: 進行方法交換。
  • 代碼實現:
#import "UIFont+AdjustSwizzling.h"
#import <objc/runtime.h>

#define XXX_UISCREEN_WIDTH 375

@implementation UIFont (AdjustSwizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(systemFontOfSize:);
        SEL swizzledSelector = @selector(xxx_systemFontOfSize:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

+ (UIFont *)xxx_systemFontOfSize:(CGFloat)fontSize {
    UIFont *newFont = nil;
    newFont = [UIFont xxx_systemFontOfSize:fontSize * [UIScreen mainScreen].bounds.size.width / XXX_UISCREEN_WIDTH];
    
    return newFont;
}

@end
複製代碼

注意:這種方式只適用於純代碼的狀況,關於 XIB 字體根據屏幕尺寸適配,能夠參考這篇博文: 小生不怕:iOS xib文件根據屏幕等比例縮放的適配


4.3 處理按鈕重複點擊

需求:避免一個按鈕被快速屢次點擊。

仍是來思考一下有幾種作法。

第一種:利用 Delay 延遲,和不可點擊方法。

這種方法很直觀,也很簡單。但就是工做量很大,須要在全部有按鈕的地方添加代碼。很不想認可:在以前項目中,我使用的就是這種方式。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *button = [[UIButton alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
    button.backgroundColor = [UIColor redColor];
    [button addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)buttonClick:(UIButton *)sender {
    sender.enabled = NO;
    [self performSelector:@selector(changeButtonStatus:) withObject:sender afterDelay:0.8f];
    
    NSLog(@"點擊了按鈕");
}

- (void)changeButtonStatus:(UIButton *)sender {
    sender.enabled = YES;
}
複製代碼

第二種:利用分類 + Method Swizzling

  1. UIControlUIButton 創建一個 Category。
  2. 在分類中添加一個 NSTimeInterval xxx_acceptEventInterval; 的屬性,設定重複點擊間隔
  3. 在分類中實現一個自定義的 xxx_sendAction:to:forEvent: 方法,在其中添加限定時間相應的方法。
  4. 利用 Method Swizzling 將 sendAction:to:forEvent: 方法和 xxx_sendAction:to:forEvent: 進行方法交換。
  • 代碼實現:
#import "UIButton+DelaySwizzling.h"
#import <objc/runtime.h>

@interface UIButton()

// 重複點擊間隔
@property (nonatomic, assign) NSTimeInterval xxx_acceptEventInterval;

@end


@implementation UIButton (DelaySwizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzledSelector = @selector(xxx_sendAction:to:forEvent:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)xxx_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    
    // 若是想要設置統一的間隔時間,能夠在此處加上如下幾句
    if (self.xxx_acceptEventInterval <= 0) {
        // 若是沒有自定義時間間隔,則默認爲 0.4 秒
        self.xxx_acceptEventInterval = 0.4;
    }
    
    // 是否小於設定的時間間隔
    BOOL needSendAction = (NSDate.date.timeIntervalSince1970 - self.xxx_acceptEventTime >= self.xxx_acceptEventInterval);
    
    // 更新上一次點擊時間戳
    if (self.xxx_acceptEventInterval > 0) {
        self.xxx_acceptEventTime = NSDate.date.timeIntervalSince1970;
    }
    
    // 兩次點擊的時間間隔小於設定的時間間隔時,才執行響應事件
    if (needSendAction) {
        [self xxx_sendAction:action to:target forEvent:event];
    }
}

- (NSTimeInterval )xxx_acceptEventInterval{
    return [objc_getAssociatedObject(self, "UIControl_acceptEventInterval") doubleValue];
}

- (void)setXxx_acceptEventInterval:(NSTimeInterval)xxx_acceptEventInterval{
    objc_setAssociatedObject(self, "UIControl_acceptEventInterval", @(xxx_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSTimeInterval )xxx_acceptEventTime{
    return [objc_getAssociatedObject(self, "UIControl_acceptEventTime") doubleValue];
}

- (void)setXxx_acceptEventTime:(NSTimeInterval)xxx_acceptEventTime{
    objc_setAssociatedObject(self, "UIControl_acceptEventTime", @(xxx_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
複製代碼

參考博文:大斑馬小斑馬:IOS 防止UIButton 重複點擊


4.4 TableView、CollectionView 異常加載佔位圖

在項目中遇到網絡異常,或者其餘各類緣由形成 TableView、CollectionView 數據爲空的時候,一般須要加載佔位圖顯示。那麼加載佔位圖有沒有什麼好的方法或技巧?

第一種:刷新數據後進行判斷

這應該是一般的作法。當返回數據,刷新 TableView、CollectionView 時候,進行判斷,若是數據爲空,則加載佔位圖。若是數據不爲空,則移除佔位圖,顯示數據。

第二種:利用分類 + Method Swizzling 重寫 reloadData 方法。

以 TableView 爲例:

  1. 爲 TableView 創建一個 Category,Category 中添加刷新回調 block 屬性、佔位圖 View 屬性。
  2. 在分類中實現一個自定義的 xxx_reloadData 方法,在其中添加判斷是否爲空,以及加載佔位圖、隱藏佔位圖的相關代碼。
  3. 利用 Method Swizzling 將 reloadData 方法和 xxx_reloadData 進行方法交換。
  • 代碼實現:
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UITableView (ReloadDataSwizzling)

@property (nonatomic, assign) BOOL firstReload;
@property (nonatomic, strong) UIView *placeholderView;
@property (nonatomic,   copy) void(^reloadBlock)(void);

@end

/*--------------------------------------*/

#import "UITableView+ReloadDataSwizzling.h"
#import "XXXPlaceholderView.h"
#import <objc/runtime.h>

@implementation UITableView (ReloadDataSwizzling)


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(reloadData);
        SEL swizzledSelector = @selector(xxx_reloadData);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)xxx_reloadData {
    if (!self.firstReload) {
        [self checkEmpty];
    }
    self.firstReload = NO;
    
    [self xxx_reloadData];
}


- (void)checkEmpty {
    BOOL isEmpty = YES; // 判空 flag 標示
    
    id <UITableViewDataSource> dataSource = self.dataSource;
    NSInteger sections = 1; // 默認TableView 只有一組
    if ([dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
        sections = [dataSource numberOfSectionsInTableView:self] - 1; // 獲取當前TableView 組數
    }
    
    for (NSInteger i = 0; i <= sections; i++) {
        NSInteger rows = [dataSource tableView:self numberOfRowsInSection:i]; // 獲取當前TableView各組行數
        if (rows) {
            isEmpty = NO; // 若行數存在,不爲空
        }
    }
    if (isEmpty) { // 若爲空,加載佔位圖
        if (!self.placeholderView) { // 若未自定義,加載默認佔位圖
            [self makeDefaultPlaceholderView];
        }
        self.placeholderView.hidden = NO;
        [self addSubview:self.placeholderView];
    } else { // 不爲空,隱藏佔位圖
        self.placeholderView.hidden = YES;
    }
}

- (void)makeDefaultPlaceholderView {
    self.bounds = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
    XXXPlaceholderView *placeholderView = [[XXXPlaceholderView alloc] initWithFrame:self.bounds];
    __weak typeof(self) weakSelf = self;
    [placeholderView setReloadClickBlock:^{
        if (weakSelf.reloadBlock) {
            weakSelf.reloadBlock();
        }
    }];
    self.placeholderView = placeholderView;
}

- (BOOL)firstReload {
    return [objc_getAssociatedObject(self, @selector(firstReload)) boolValue];
}

- (void)setFirstReload:(BOOL)firstReload {
    objc_setAssociatedObject(self, @selector(firstReload), @(firstReload), OBJC_ASSOCIATION_ASSIGN);
}

- (UIView *)placeholderView {
    return objc_getAssociatedObject(self, @selector(placeholderView));
}

- (void)setPlaceholderView:(UIView *)placeholderView {
    objc_setAssociatedObject(self, @selector(placeholderView), placeholderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void (^)(void))reloadBlock {
    return objc_getAssociatedObject(self, @selector(reloadBlock));
}

- (void)setReloadBlock:(void (^)(void))reloadBlock {
    objc_setAssociatedObject(self, @selector(reloadBlock), reloadBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end
複製代碼

參考博文:賣報的小畫家Sure:零行代碼爲App添加異常加載佔位圖


4.5 APM(應用性能管理)、防止程序崩潰

  1. 經過 Method Swizzling 替換 NSURLConnection , NSURLSession 相關的原始實現(例如 NSURLConnection 的構造方法和 start 方法),在實現中加入網絡性能埋點行爲,而後調用原始實現。從而來監控網絡。
  2. 防止程序崩潰,能夠經過 Method Swizzling 攔截容易形成崩潰的系統方法,而後在替換方法捕獲異常類型 NSException ,再對異常進行處理。最多見的例子就是攔截 arrayWithObjects:count: 方法避免數組越界,這種例子網上不少,就再也不展現代碼了。

參考資料


最後

寫 Method Swizzling 花費了整整兩週的時間,其中查閱了大量的 Method Swizzling 相關的資料,但獲得的收穫是很值得的。同時但願能帶給你們一些幫助。

下一篇,我打算 Runtime 中 Category(分類)的底層原理。

文中如如有誤,煩請指正,感謝。


iOS 開發:『Runtime』詳解 系列文章:

還沒有完成:

  • iOS 開發:『Runtime』詳解(五)Crash 防禦系統
  • iOS 開發:『Runtime』詳解(六)Objective-C 2.0 結構解析
  • iOS 開發:『Runtime』詳解(七)KVO 底層實現
相關文章
相關標籤/搜索