【iOS】category重寫方法的呼叫

Yaso_GG發表於2017-07-25

前兩天工程中,出現了一個類的兩個分類(自己實現了一個,第三方SDK裡有一個),同時實現了一個方法名相同的方法,當時就產生了一個疑問,當實際呼叫時調的是哪個分類的方法呢?

一、category重寫主類方法

首先先來看看這一點,相信大部分人已經知道,如果一個類的分類重寫了這個類的方法後,那麼這個類的這個方法將失效,起作用的將會是分類的那個重寫方法,在分類重寫的時候Xcode也會給出相應警告:分類實現將會替代主類實現
分類重寫主類已有方法警告 ️

那麼原方法失效,分類方法生效的原理是?

想弄清這點先來看一下類的初始化,首先oc是動態語言,建立在runtime 的基礎上,同樣類的初始化也是動態的,根類NSObject 的+load+initilize兩個方法,用於類的初始化,我們這裡要著重看的是+load方法:

+load 方法是當類或分類被新增到 Objective-C runtime 時被呼叫的,實現這個方法可以讓我們在類載入的時候執行一些類相關的行為。子類的 +load 方法會在它的所有父類的 +load 方法執行之後執行,而分類的 +load 方法會在它的主類的 +load 方法執行之後執行。但是不同的類之間的 +load 方法的呼叫順序是不確定的。

原因就在這裡,因為載入順序是父類先+load,然後子類+load,然後分類+load,那麼如果分類重寫子類方法:首先子類+load,將方法新增到類的方法列表methodLists,然後分類+load,將重寫的方法新增到方法列表中,但是這裡存在幾點疑問:
1. 方法列表methodLists裡是否會有兩個SEL相同的方法?
2. 如果會有,這兩個方法在方法列表中的順序是怎樣的?(順序決定哪個被呼叫)

下面來求證一下:

#import "TestCategory.h"
/*主類實現*/
@implementation TestCategory
- (void)newMethod {
    NSLog(@"主類");
}
@end

#import "TestCategory+add.h"
/*分類一實現*/
@implementation TestCategory (add)
- (void)newMethod {
    NSLog(@"分類一");
}
@end


#import <objc/runtime.h>
#import "TestCategory.h"
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    id LenderClass = objc_getClass("TestCategory");
    unsigned int outCount, i;
    //獲取例項方法列表
    Method *methodList = class_copyMethodList(LenderClass, &outCount);
    for (i=0; i<outCount; i++) {
        Method method = methodList[i];
        NSLog(@"instanceMethod:%@", NSStringFromSelector(method_getName(method)));
    }

    //呼叫一下
    TestCategory *tCategory = [[TestCategory alloc]init];
    [tCategory newMethod];
}

看輸出:

2017-07-19 21:38:13.593 TestRuntimeProperty[27827:1306334] instanceMethodnewMethod
2017-07-19 21:38:13.593 TestRuntimeProperty[27827:1306334] instanceMethodnewMethod
2017-07-19 21:38:13.594 TestRuntimeProperty[27827:1306334] 分類一

可見:
1. 方法列表裡會存在兩個SEL相同的方法。
2. 實際呼叫時,呼叫的是後新增的方法,即後新增的方法在方法列表methodLists的這個陣列的頂部

答案已經很明確了:後+load的類的方法,後新增到方法列表,而這時的新增方式又是插入頂部新增,即[methodLists insertObject:category_method atIndex:0]; 所以objc_msgSend遍歷方法列表查詢SEL 對應的IMP時,會先找到分類重寫的那個,呼叫執行。然後新增到快取列表中,這樣主類方法實現永遠也不會調到。

(注:methodLists和method的結構定義可以看下我上篇文章-【iOS】Runtime解讀

二、多個category實現同一個方法

但是如果多個分類同時重寫同一個方法,執行順序又是怎樣的呢?

答案是:對於多個分類同時重寫同一個方法,Xcode在執行時是根據buildPhases->Compile Sources裡面的順序從上至下編譯的,那麼很明顯就像子類和分類一樣,後編譯的後load,即後新增到方法列表,所以後編譯的分類,方法會放到方法列表頂部,呼叫的時候先執行。

新增程式碼驗證一下:

#import "TestCategory+addAgain.h"
/*分類二實現*/
@implementation TestCategory (addAgain)
- (void)newMethod {
    NSLog(@"分類二");
}
@end

看輸出:

2017-07-19 22:18:13.593 TestRuntimeProperty[28385:1331972] instanceMethodnewMethod
2017-07-19 22:18:13.593 TestRuntimeProperty[28385:1331972] instanceMethodnewMethod
2017-07-19 21:18:13.593 TestRuntimeProperty[28385:1331972] instanceMethodnewMethod
2017-07-19 22:18:13.594 TestRuntimeProperty[28385:1331972] 分類一

結果輸出仍然是分類一,那就說明"TestCategory+add.h"在buildPhases->Compile Sources裡面的順序是靠下的,看下buildPhases的確如此:
Compile Sources .m檔案排序

總結一句話:類的載入順序,決定方法的新增順序,呼叫的時候,後新增的方法會先被找到,所以呼叫的始終是後載入的類的方法實現。

相關文章