iOS底層原理總結(jié) - 探尋Category本質(zhì)

本篇主要是對(duì)小碼哥底層視頻學(xué)習(xí)的總結(jié)。方便日后復(fù)習(xí)。
上一篇《iOS底層原理總結(jié) - 探尋KVO的本質(zhì)》:
http://www.itdecent.cn/p/f5b3199982e6

本篇學(xué)習(xí)總結(jié):

  • 探尋Category本質(zhì)
  • Category底層代碼分析
  • load 和 initialize方法底層分析

好了,帶著問(wèn)題,我們一一開(kāi)始閱讀吧 ??

一.探尋Category本質(zhì)

我們還是先來(lái)上一段代碼,之后的分析都是基于這段代碼:
MJPerson類

//MJPerson.h文件
#import <Foundation/Foundation.h>

@interface MJPerson : NSObject

- (void)run;

@end
//MJPerson.m文件
#import "MJPerson.h"

// class extension (匿名分類\類擴(kuò)展)
@interface MJPerson()
{
    int _abc;
}
@property (nonatomic, assign) int age;

- (void)abc;
@end

@implementation MJPerson

- (void)abc
{
    
}

- (void)run
{
    NSLog(@"MJPerson - run");
}

+ (void)run2
{
    
}

MJPerson+Test 分類

//MJPerson(Test) .h文件
#import "MJPerson.h"

@interface MJPerson (Test) 

- (void)test;

@end

//MJPerson(Test) .m文件
#import "MJPerson+Test.h"

@implementation MJPerson (Test)

- (void)run
{
    NSLog(@"MJPerson (Test) - run");
}


- (void)test
{
    NSLog(@"test");
}

+ (void)test2
{
    
}

@end

MJPerson+Eat分類

//MJPerson(Eat).h文件
#import "MJPerson.h"

@interface MJPerson (Eat) <NSCopying, NSCoding>

- (void)eat;

@property (assign, nonatomic) int weight;
@property (assign, nonatomic) double height;

@end
//MJPerson(Eat).m文件
#import "MJPerson+Eat.h"

@implementation MJPerson (Eat)

- (void)run
{
    NSLog(@"MJPerson (Eat) - run");
}

- (void)eat
{
    NSLog(@"eat");
}

- (void)eat1
{
    NSLog(@"eat1");
}

+ (void)eat2
{
    
}

+ (void)eat3
{
    
}

main.m文件

#import "MJPerson+Eat.h"
#import "MJPerson+Test.h"
#import "MJPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *person = [[MJPerson alloc] init];
        [person run];

    }
    return 0;
}

我們之前講過(guò)實(shí)例對(duì)象的isa指針指向類對(duì)象,類對(duì)象的isa指針指向元類對(duì)象,當(dāng)p調(diào)用run方法時(shí),通過(guò)實(shí)例對(duì)象的isa指針找到類對(duì)象,然后在類對(duì)象中查找對(duì)象方法,如果沒(méi)有找到,通過(guò)類對(duì)象的superclass指針找到父類的類對(duì)象,接著去尋找run方法。
那么問(wèn)題來(lái)了,當(dāng)我們調(diào)用分類中的run方法時(shí),它也會(huì)按照我們前面說(shuō)的方式去查找run方法嗎?
小碼哥直接給出結(jié)論了,先看結(jié)論,再看底層代碼實(shí)現(xiàn)

分類中的對(duì)象方法依然存儲(chǔ)在類對(duì)象中,同本類對(duì)象在同一個(gè)地方,調(diào)用步驟也同本類調(diào)用對(duì)象方法一樣,同理,如果是類方法的話,是存儲(chǔ)在元類對(duì)象中的。

二.Category底層代碼分析

我們前面講過(guò)了,所有的OC代碼最終都會(huì)被編譯成C/C++代碼,我們要想進(jìn)一步了解底層代碼,需要將文件轉(zhuǎn)化成C++文件,轉(zhuǎn)化方式在《iOS底層原理-探尋OC對(duì)象本質(zhì)》中講過(guò)了,這里我們直接看.cpp文件。

轉(zhuǎn)化C++文件.png

  • _category_t:分類結(jié)構(gòu)體

我們點(diǎn)開(kāi)MJPerson+Eat.cpp文件,搜索category,我們可以找到 _category_t 結(jié)構(gòu)體,結(jié)構(gòu)如下:

_category_t結(jié)構(gòu)體信息.png

從底層代碼中可以看出,_category_t 結(jié)構(gòu)體包含對(duì)象方法列表,類方法列表,協(xié)議列表,屬性列表等信息,但是沒(méi)有找到成員變量信息,是不是可以初步肯定一下之前的結(jié)論:分類中可以添加對(duì)象方法,類方法,協(xié)議,屬性,不可以添加成員變量,category可以添加屬性,但是不會(huì)自動(dòng)生成成員變量,只能生成setter getter方法,還需要我們手動(dòng)實(shí)現(xiàn)一下方法。

那么我們就一個(gè)變量一個(gè)變量的分析吧

  • _method_list_t:方法列表

然后搜索 _method_list_t,結(jié)構(gòu)體如下:

_method_list_t結(jié)構(gòu)體信息.png

此時(shí)我們發(fā)現(xiàn)了兩個(gè)名字比較長(zhǎng)的變量名,分別是
_OBJC__CATEGORY_INSTANCE_METHODS_MJPerson__Eat_OBJC__CATEGORY_CLASS_METHODS_MJPerson__Eat,從名稱上可以推測(cè)是類方法實(shí)例方法,下面的代碼賦值跟上面的結(jié)構(gòu)體成員變量一一對(duì)應(yīng),我們可以看到結(jié)構(gòu)體中存儲(chǔ)了方法占用的內(nèi)存,方法數(shù)量,以及方法列表。并且從上圖中找到分類中我們實(shí)現(xiàn)對(duì)應(yīng)的對(duì)象方法。

  • _protocol_list_t:協(xié)議列表信息

繼續(xù)搜索 _protocol_list_t,結(jié)構(gòu)體如下:

_protocol_list_t結(jié)構(gòu)體信息.png

這里同樣看到了一個(gè)名字比較長(zhǎng)的變量名:_OBJC_CATEGORY_PROTOCOLS__MJPerson__Ea,下面的代碼賦值跟上面的結(jié)構(gòu)體成員變量一一對(duì)應(yīng)。

  • _prop_list_t:屬性列表信息

最后搜索 _prop_list_t,結(jié)構(gòu)體如下:

_prop_list_t結(jié)構(gòu)體信息.png

這里同樣看到了一個(gè)名字比較長(zhǎng)的變量名:_OBJC__PROP_LIST_MJPerson__Eat,下面的代碼賦值跟上面的結(jié)構(gòu)體成員變量一一對(duì)應(yīng)。

最后我們?cè)偎阉饕幌耤ategory 發(fā)現(xiàn)了這么一個(gè)變量:
**_OBJC__CATEGORY_MJPerson__Eat ** 變量時(shí)屬于 _category_t 結(jié)構(gòu)體類型,我們?cè)賮?lái)看一下 _category_t 結(jié)構(gòu)體信息。

_OBJC_$_CATEGORY_MJPerson_$_Eat結(jié)構(gòu)體信息.png
_category_t結(jié)構(gòu)體信息.png

上下兩張圖對(duì)比來(lái)看,我們發(fā)現(xiàn)定義了category_t類型的變量,
MJPerson:賦值給name;
OBJC_CLASS
$_MJPreson :賦值給cls指針變量;
對(duì)應(yīng)的列表信息一一賦值給變量。

通過(guò)以上分析我們發(fā)現(xiàn),編譯時(shí)期,分類被編譯成了catagory_t結(jié)構(gòu)體類型的變量,分類中的對(duì)象方法,類方法,屬性,協(xié)議等都存放在catagory_t結(jié)構(gòu)體對(duì)應(yīng)的成員變量中。

回到最初的結(jié)論,分類中的實(shí)例方法是如何放到類對(duì)象中去呢,這要從runtime源碼說(shuō)起,下面是通過(guò)查看runtime源碼找到catagory_t存儲(chǔ)的方法,屬性,協(xié)議等是如何存儲(chǔ)在類對(duì)象中的。

我們先記錄一下看源碼的順序,源碼本來(lái)就有點(diǎn)晦澀難懂,我們根據(jù)順序去查看


runtime查看源碼順序.png

首先我們下載源碼,打開(kāi)objc-os.mm文件,先從runtime初始函數(shù)看起

objc_init函數(shù).png

接著我們來(lái)到 & map_images讀取模塊(images這里代表模塊),

map_images.png

點(diǎn)擊 map_images_nolock 函數(shù)中找到_read_images函數(shù),

map_images_nolock部分代碼.png

在_read_images函數(shù)中我們可以找到重組類信息

_read_images重組類信息.png

從上述代碼中我們可以知道這段代碼是用來(lái)查找有沒(méi)有分類的。通過(guò)_getObjc2CategoryList函數(shù)獲取分類列表之后,進(jìn)行遍歷,獲取其中的方法,協(xié)議,屬性等,可以看到最終都調(diào)用了remethodizeClass(cls)函數(shù),我們點(diǎn)進(jìn)去查看一下

remethodizeClass.png

通過(guò)上述代碼我們發(fā)現(xiàn)attachCategories函數(shù)接收了類對(duì)象cls和分類數(shù)組cats,如我們一開(kāi)始寫(xiě)的代碼所示,一個(gè)類有多少個(gè)分類,就會(huì)有多少個(gè)category_t結(jié)構(gòu)體類型的變量,這些分類信息都都保存在category_list中。我們來(lái)到attachCategories函數(shù)內(nèi)部

attachCategories-1.png

attachCategories-2.png

attachCategories-3.png

上述代碼中可以看出,這步驟才是關(guān)鍵步驟了

  • 1.首先根據(jù)傳進(jìn)來(lái)的cats數(shù)組分別創(chuàng)建了mlist,proplists,protocols三個(gè)二維數(shù)組,用于存儲(chǔ)方法每個(gè)分類的方法列表,屬性列表,協(xié)議列表
  • 2.通過(guò)while(i--)倒序方式進(jìn)行遍歷循環(huán),取出每一個(gè)分類中的方法數(shù)組,屬性數(shù)組,協(xié)議數(shù)組,存進(jìn)mlist,proplists,protocols三個(gè)二維數(shù)組中
  • 3.取出類對(duì)象的class_rw_t,我們?cè)?a href="http://www.itdecent.cn/p/748bc1d63184" target="_blank">《iOS底層原理總結(jié) - 探尋Class的本質(zhì)》 中已經(jīng)講過(guò)了,類對(duì)象中存儲(chǔ)的方法列表,屬性信息,協(xié)議信息,成員變量信息都存儲(chǔ)在class_rw_t
  • 4.將存放所有分類方法的二維數(shù)組 mlist 附加到類對(duì)象的方法列表中
  • 5.將存放所有屬性方法的二維數(shù)組 proplists 附加到類對(duì)象的屬性列表中
  • 6.將存放所有協(xié)議方法的二維數(shù)組 protocols 附加到類對(duì)象的協(xié)議列表中

我們看一下attachLists函數(shù)內(nèi)部實(shí)現(xiàn):

attachLists函數(shù)內(nèi)部實(shí)現(xiàn).png

array()->lists:類對(duì)象原來(lái)的方法列表,屬性列表,協(xié)議列表。
addedLists:所有分類的方法列表,屬性列表,協(xié)議列表。
attachLists函數(shù)里面最重要的兩個(gè)方法為memmove(內(nèi)存移動(dòng)方法)和memcpy(內(nèi)存拷貝方法)。
我們分別說(shuō)一下這兩個(gè)函數(shù),


memmove和memcpy方法說(shuō)明.png

下圖是經(jīng)過(guò)memmove函數(shù)之后數(shù)據(jù)在內(nèi)存中分配情況
1.在原有空間上擴(kuò)容addedCount大小空間


增大內(nèi)存空間.png

2.遵循menmove方法移動(dòng)原則,原有方法開(kāi)始往后移動(dòng)
// array()->lists 原來(lái)方法、屬性、協(xié)議列表數(shù)組
// addedCount 分類數(shù)組長(zhǎng)度
// oldCount * sizeof(array()->lists[0]) 原來(lái)數(shù)組占據(jù)的空間
memmove(array()->lists + addedCount, array()->lists, 
                  oldCount * sizeof(array()->lists[0]));
memmove方法之后內(nèi)存變化.png

經(jīng)過(guò)memmove方法之后,我們發(fā)現(xiàn),雖然本類的方法,屬性,協(xié)議列表會(huì)分別后移,但是本類的對(duì)應(yīng)數(shù)組的指針依然指向原始位置。
memcpy方法之后,內(nèi)存變化

// array()->lists 原來(lái)方法、屬性、協(xié)議列表數(shù)組
// addedLists 分類方法、屬性、協(xié)議列表數(shù)組
// addedCount * sizeof(array()->lists[0]) 原來(lái)數(shù)組占據(jù)的空間
memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
memmove方法之后,內(nèi)存變化.png

我們發(fā)現(xiàn)原來(lái)指針并沒(méi)有改變,至始至終指向開(kāi)頭的位置。并且經(jīng)過(guò)memmove和memcpy方法之后,分類的方法,屬性,協(xié)議列表被放在了類對(duì)象中原本存儲(chǔ)的方法,屬性,協(xié)議列表前面。

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 獲得方法數(shù)組
    Method *methodList = class_copyMethodList(cls, &count);
    // 存儲(chǔ)方法名
    NSMutableString *methodNames = [NSMutableString string];
    // 遍歷所有的方法
    for (int i = 0; i < count; i++) {
        // 獲得方法
        Method method = methodList[i];
        // 獲得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    // 釋放
    free(methodList);
    // 打印方法名
    NSLog(@"%@ - %@", cls, methodNames);
}
- (void)viewDidLoad {
    [super viewDidLoad];    
    Preson *p = [[Preson alloc] init];
    [p run];
    [self printMethodNamesOfClass:[Preson class]];
}

那么為什么要將分類方法的列表追加到本來(lái)的對(duì)象方法前面呢,這樣做的目的是為了保證分類方法優(yōu)先調(diào)用,我們知道當(dāng)分類重寫(xiě)本類的方法時(shí),會(huì)覆蓋本類的方法。
其實(shí)經(jīng)過(guò)上面的分析我們知道本質(zhì)上并不是覆蓋,而是優(yōu)先調(diào)用。本類的方法依然在內(nèi)存中的。我們可以通過(guò)打印所有類的所有方法名來(lái)查看

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 獲得方法數(shù)組
    Method *methodList = class_copyMethodList(cls, &count);
    // 存儲(chǔ)方法名
    NSMutableString *methodNames = [NSMutableString string];
    // 遍歷所有的方法
    for (int i = 0; i < count; i++) {
        // 獲得方法
        Method method = methodList[i];
        // 獲得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    // 釋放
    free(methodList);
    // 打印方法名
    NSLog(@"%@ - %@", cls, methodNames);
}
- (void)viewDidLoad {
    [super viewDidLoad];    
    Preson *p = [[Preson alloc] init];
    [p run];
    [self printMethodNamesOfClass:[Preson class]];
}

通過(guò)下圖中打印內(nèi)容可以發(fā)現(xiàn),調(diào)用的是Test2中的run方法,并且Person類中存儲(chǔ)著兩個(gè)run方法


打印所有方法.png
三.load 和 initialize

load 方法會(huì)在程序啟動(dòng),加載類,分類信息的時(shí)候調(diào)用,只調(diào)用一次,調(diào)用方法是指針直接調(diào)用,一般不主動(dòng)調(diào)用。
先看代碼

load方法調(diào)用順序.png

再來(lái)看一下調(diào)用load方法的具體實(shí)現(xiàn)

load方法內(nèi)部實(shí)現(xiàn).png

通過(guò)源碼我們發(fā)現(xiàn)load方法調(diào)用順序是優(yōu)先調(diào)用類的load方法,如有繼承關(guān)系的類,調(diào)用子類的時(shí)候會(huì)有限調(diào)用父類的load方法,之后調(diào)用分類的load方法,分類按照編譯順序調(diào)用

我們看到load方法中直接拿到load方法的內(nèi)存地址直接調(diào)用方法,不在是通過(guò)消息發(fā)送機(jī)制調(diào)用。


分類load方法調(diào)用源碼.png

代碼驗(yàn)證如下:
我們添加Student繼承Presen類,并添加Student+Test分類,分別重寫(xiě)只+load方法,其他什么都不做通過(guò)打印發(fā)現(xiàn):


load方法打印.png

最后用一張圖總結(jié)load方法


load方法總結(jié)及源碼查看順序.png

** initialize** 方法當(dāng)類第一次接收到消息時(shí),優(yōu)先調(diào)用父類的initialize方法,在調(diào)用子類的initialize方法。
之后我們?yōu)镻reson、Student 、Student+Test 添加initialize方法。
第一次使用類的時(shí)候就會(huì)調(diào)用initialize方法。調(diào)用子類的initialize之前,會(huì)先保證調(diào)用父類的initialize方法。如果之前已經(jīng)調(diào)用過(guò)initialize,就不會(huì)再調(diào)用initialize方法了。當(dāng)分類重寫(xiě)initialize方法時(shí)會(huì)先調(diào)用分類的方法。但是load方法并不會(huì)被覆蓋,首先我們來(lái)看一下initialize的源碼。

initialize調(diào)用機(jī)制.png

最后用一張圖總結(jié):


initialize方法總結(jié)及源碼順序.png

總結(jié)本篇面試題:

  • 1.Category的實(shí)現(xiàn)原理,或者加載處理過(guò)程是什么樣的?

1>編譯時(shí)期將所有categor轉(zhuǎn)化成category_t的結(jié)構(gòu)體變量,并賦值
2>通過(guò)runtime加載某個(gè)類的所有category數(shù)據(jù)
3>把所有category的方法,屬性,協(xié)議數(shù)組,合并到一個(gè)大數(shù)組中
a)后面參與編譯的category數(shù)據(jù),會(huì)在數(shù)組的前面
4》將合并后的分類數(shù)據(jù)(方法,屬性,協(xié)議),合并到類原來(lái)數(shù)據(jù)的前面

  • 2.Category為什么只能加方法不能加屬性?

category可以添加屬性,但是并不會(huì)主動(dòng)生成成員變量及setter/getter方法,因?yàn)閏ategory_t結(jié)構(gòu)體中并不存在成員變量,通過(guò)之前對(duì)對(duì)象的分析我們知道成員變量是存放在實(shí)例對(duì)象中,并且編譯的那一刻都已經(jīng)決定好了,而分類是在運(yùn)行時(shí)才去加載的,那么我們就無(wú)法運(yùn)行時(shí)將分類的成員變量添加到實(shí)例對(duì)象的結(jié)構(gòu)體中,因此category中可以添加屬性,不可添加成員變量。

  • 3.load initialize方法的區(qū)別是什么?

a.調(diào)用方式:
1>load 是根據(jù)函數(shù)地址直接調(diào)用;
2>initialize是通過(guò)objc_msgSend方法調(diào)用;
b>調(diào)用時(shí)刻
1>load 是runtime加載類,分類的時(shí)候調(diào)用(只會(huì)調(diào)用一次);
2>initialize 是類第一次接收到消息的時(shí)候調(diào)用,每一個(gè)類只會(huì)initialize一次(父類的initialize方法可能會(huì)被調(diào)用多次,具體例子是當(dāng)多個(gè)子類都沒(méi)有實(shí)現(xiàn)initialize方法,卻是第一次給類發(fā)送消息)

  • 4.load initialize 方法的調(diào)用順序?

1.load
1>先調(diào)用類的load
a)先編譯的類,優(yōu)先調(diào)用load
b)調(diào)用子類的load之前,會(huì)先調(diào)用父類的load
2>在調(diào)用分類的load
a)先編譯的分類,優(yōu)先調(diào)用load
2.initialize
1>先初始化父類
2>在初始化子類(可能最終調(diào)用的是父類的initialize方法)

本篇學(xué)習(xí)先記錄到此,感謝閱讀,如有錯(cuò)誤,不吝賜教。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容