Objective-C 源碼(四) 再次看 Method Swizzling

    Method Swizzling 的原理程序員

    先打開 objc-private.h 文件 在 235行能夠看到 Method的定義:app

typedef struct method_t *Method;

    

    而後在 objc-runtime-new.h 文件中第82行能夠看到:函數

struct method_t {
    SEL name;
    const char *types;
    IMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

    從本質上講:它就是struct method_t 類型的執行,包括了3個成員變量和一個成員函數:
spa

    name:表示的是方法的名稱,用於惟一標示該方法,好比@selector(viewWillAppear:);
指針

    types:標示的是方法的返回值和參數類型;
code

    imp:是一個函數指針,指向方法的實現;
對象

    SortBySELAddress 是一個根據name的地址對方法進行排序的函數。排序

    由上面能夠看出:Objective-C中的方法名是不包括參數類型的,也就是說下面2個方法在runtime看起來時同樣的。
繼承

- (void)viewWillAppear:(BOOL)animated;
- (void)viewWillAppear:(NSString *)string;

    並且,實例方法和類方法由於是分別保存在類對象和元類對象中的,全部,同名字的類方法和實例方法是能夠共存的。
get

    原則上講:方法的名稱name和方法的實現imp是一一對應的,二Method Swizzling的原理就是動態改變他們的對應關係,以達到替換方法的目的。

    講個簡單的例子,友盟統計的頁面統計的時候:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [MobClick beginLogPageView:@"PageOne"];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [MobClick endLogPageView:@"PageOne"];
}

    要想偷懶解決上面的代碼,正常人的作法就是定義一個BaseViewController,而後其餘類都是從這個viewcontroller繼承出來的,可是,確實能夠很快搞定,問題是,若是是old項目要加入,並且原始項目還不是繼承某個BaseViewController的話就坑爹了,可是你還要一個一個類改繼承的viewcontroller,這個時候使用Method Swizzling是最佳的解決辦法。

    

    Method Swizzling:   

@interface UIViewController (MRCUMAnalytics)

@end

@implementation UIViewController (MRCUMAnalytics)

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

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(mrc_viewWillAppear:);

        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);
        }
    });
}

#pragma mark - Method Swizzling

- (void)mrc_viewWillAppear:(BOOL)animated {
    [self mrc_viewWillAppear:animated];
    [MobClick beginLogPageView:NSStringFromClass([self class])];
}

@end

    解析:在上面的代碼中有三個關鍵點須要引發咱們的注意:    

  1. 爲何是在 +load 方法中實現 Method Swizzling 的邏輯,而不是其餘的什麼方法,好比 +initialize 等;

  2. 爲何 Method Swizzling 的邏輯須要用 dispatch_once 來進行調度;

  3. 爲何須要調用 class_addMethod 方法,而且以它的結果爲依據分別處理兩種不一樣的狀況。

    第 1 個爲何:    

    +load+initialize 是 Objective-C runtime 會自動調用的兩個類方法。可是它們被調用的時機倒是有差異的,+load 方法是在類被加載的時候調用的,而 +initialize 方法是在類或它的子類收到第一條消息以前被調用的,這裏所指的消息包括實例方法和類方法的調用。也就是說 +initialize 方法是以懶加載的方式被調用的,若是程序一直沒有給某個類或它的子類發送消息,那麼這個類的 +initialize 方法是永遠不會被調用的。此外 +load 方法還有一個很是重要的特性,那就是子類、父類和分類中的 +load 方法的實現是被區別對待的。換句話說在 Objective-C runtime 自動調用 +load 方法時,分類中的 +load 方法並不會對主類中的 +load 方法形成覆蓋。綜上所述,+load 方法是實現 Method Swizzling 邏輯的最佳「場所」。

    第 2 個爲何

    咱們上面提到,+load 方法在類加載的時候會被 runtime 自動調用一次,可是它並無限制程序員對 +load 方法的手動調用。什麼?你說不會有程序員這麼幹?那可說不定,我還見過手動調用 viewDidLoad 方法的程序員,就是介麼任性。而咱們所可以作的就是儘量地保證程序可以在各類狀況下正常運行。

    第 3 個爲何

    咱們使用 Method Swizzling 的目的一般都是爲了給程序增長功能,而不是徹底地替換某個功能,因此咱們通常都須要在自定義的實現中調用原始的實現。因此這裏就會有兩種狀況須要咱們分別進行處理:

    

    第 1 種狀況:主類自己有實現須要替換的方法,也就是 class_addMethod 方法返回 NO 。這種狀況的處理比較簡單,直接交換兩個方法的實現就能夠了:

- (void)viewWillAppear:(BOOL)animated {
    /// 先調用原始實現,因爲主類自己有實現該方法,因此這裏實際調用的是主類的實現
    [self mrc_viewWillAppear:animated];
    /// 咱們增長的功能
    [MobClick beginLogPageView:NSStringFromClass([self class])];
}

- (void)mrc_viewWillAppear:(BOOL)animated {
    /// 主類的實現
}

    第 2 種狀況:主類自己沒有實現須要替換的方法,而是繼承了父類的實現,即 class_addMethod 方法返回 YES 。這時使用 class_getInstanceMethod 函數獲取到的 originalSelector 指向的就是父類的方法,咱們再經過執行 class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); 將父類的實現替換到咱們自定義的 mrc_viewWillAppear 方法中。這樣就達到了在 mrc_viewWillAppear 方法的實現中調用父類實現的目的。

- (void)viewWillAppear:(BOOL)animated {
    /// 先調用原始實現,因爲主類自己並無實現該方法,因此這裏實際調用的是父類的實現
    [self mrc_viewWillAppear:animated];
    /// 咱們增長的功能
    [MobClick beginLogPageView:NSStringFromClass([self class])];
}

- (void)mrc_viewWillAppear:(BOOL)animated {
    /// 父類的實現
}

    參考了 leichunfeng sunny yulingtianxia 等博客。

相關文章
相關標籤/搜索