iOS底層原理探索—Category的本質(zhì)(一)

探索底層原理,積累從點(diǎn)滴做起。大家好,我是Mars。

往期回顧

iOS底層原理探索—OC對(duì)象的本質(zhì)
iOS底層原理探索—class的本質(zhì)
iOS底層原理探索—KVO的本質(zhì)
iOS底層原理探索— KVC的本質(zhì)

今天帶領(lǐng)大家探索iOS之Category的本質(zhì)。

Category

首先我們聲明一個(gè)Person

//Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
{
    int _age;
}
- (void)run;
@end
//Person.m
#import "Person.h"
@implementation Person
- (void)run
{
    NSLog(@"Person:run");
}
@end

我們之前在iOS底層原理探索—OC對(duì)象的本質(zhì)中講到:實(shí)例對(duì)象的isa指針指向類對(duì)象,類對(duì)象的isa指針指向元類對(duì)象。我們創(chuàng)建一個(gè)Person對(duì)象p,當(dāng)p調(diào)用run方法時(shí),通過(guò)實(shí)例對(duì)象的isa指針找到類對(duì)象,然后在類對(duì)象中查找對(duì)象方法,如果沒有找到,就通過(guò)類對(duì)象的superclass指針找到父類對(duì)象,接著去尋找run方法。

那么當(dāng)我們調(diào)用分類的方法時(shí),是否跟上面的調(diào)用順序一樣呢?下面我們創(chuàng)建分類來(lái)驗(yàn)證一下:

創(chuàng)建Person的分類:

New FileiOS文件下選擇Objective-C File

創(chuàng)建分類1.png

File Type選擇CategoryClass父類選擇Person

創(chuàng)建分類2.png

//Person+test.h
#import "Person.h"
@interface Person (Test)
- (void)test;
+ (void)abc;
@property (assign, nonatomic) int age;
- (void)setAge:(int)age;
- (int)age;
@end
//Person+test.m
#import "Person+Test.h"
@implementation Person (Test)
- (void)test
{
    
}
+ (void)abc
{
    
}
- (void)setAge:(int)age
{
    
}
- (int)age
{
    return 18;
}

- (void)run
{
    NSLog(@"Person+test:run");
}
@end

以上我們就完成創(chuàng)建了PersonTest分類。

在此先告訴大家結(jié)論:分類中的對(duì)象方法是存儲(chǔ)在類對(duì)象中的,和類對(duì)象方法在同一個(gè)地方,調(diào)用步驟也和調(diào)用對(duì)象方法一樣。如果是類方法的話,同樣也是存儲(chǔ)在元類對(duì)象中

這一點(diǎn)大致可以從分類的底層結(jié)構(gòu)中看出來(lái):

分類的底層結(jié)構(gòu)

struct category_t {
    const char *name;
    classref_t cls;
    //對(duì)象方法
    struct method_list_t *instanceMethods;
    // 類方法
    struct method_list_t *classMethods;
    // 協(xié)議
    struct protocol_list_t *protocols; 
    // 屬性
    struct property_list_t *instanceProperties; 
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

從分類的源碼中可以看出Categroy在底層是以categroy _t的結(jié)構(gòu)存在,里面包括對(duì)象方法,類方法,協(xié)議,和屬性。注意分類結(jié)構(gòu)體中是不存在成員變量的,因此分類中是不允許添加成員變量。分類中添加的屬性并不會(huì)幫助我們自動(dòng)生成成員變量,只會(huì)生成set、get方法的聲明,需要我們自己去實(shí)現(xiàn)。

至此我們可以得出結(jié)論:

  • 分類的實(shí)現(xiàn)原理是將分類中的方法,屬性,協(xié)議信息放在 category_t結(jié)構(gòu)體中,然后將結(jié)構(gòu)體內(nèi)的方法列表拷貝到類對(duì)象的方法列表中。

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

由于上述結(jié)論的驗(yàn)證是依據(jù)底層源碼,過(guò)程比較枯燥,也不能保證大家閱讀一次就能弄清楚整個(gè)流程,所以將結(jié)論提前告知。不愿意閱讀源碼的讀者也可以忽略以下內(nèi)容,掌握上面的結(jié)論即可。

首先把Person+Test.m文件通過(guò)命令行轉(zhuǎn)化為c++文件,查看底層編譯過(guò)程。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m

然后將生成的.cpp文件拖拽至Xcode中查看。在.cpp文件中搜索category_t,通過(guò)搜索結(jié)果我們可以看到,_category_t結(jié)構(gòu)體中,存放著類名,對(duì)象方法列表類方法列表,協(xié)議列表,以及屬性列表

category_t結(jié)構(gòu)體.png

.cpp文件中繼續(xù)往下看,我們可以看到_method_list_t *instance_methods結(jié)構(gòu)體的內(nèi)容:

method_list_t對(duì)象方法結(jié)構(gòu)體.png

通過(guò)結(jié)構(gòu)體名稱_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test可以看出是INSTANCE_METHODS--對(duì)象方法。我們可以看到結(jié)構(gòu)體中存儲(chǔ)了方法占用的內(nèi)存,方法數(shù)量以及方法列表。并且從上圖中可以看到在分類中我們實(shí)現(xiàn)的test,setAge, agerun四個(gè)方法。

同樣,我們繼續(xù)往下閱讀,查看看到_method_list_t *class_methods結(jié)構(gòu)體的內(nèi)容:

method_list_t類方法結(jié)構(gòu)體.png

同樣通過(guò)結(jié)構(gòu)體名稱 _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test可以看出是CLASS_METHODS--類方法。同樣可以看到我們實(shí)現(xiàn)的abc類方法。

繼續(xù)往下查看時(shí),我們可以看到屬性列表結(jié)構(gòu)體_prop_list_t

prop_list_t屬性列表結(jié)構(gòu)體.png

屬性列表結(jié)構(gòu)體_OBJC_$_PROP_LIST_Person_$_Test_prop_list_t結(jié)構(gòu)體,里面存儲(chǔ)了屬性的占用空間屬性數(shù)量以及屬性列表,從上圖中可以看到我們聲明的age屬性。

同時(shí)我們發(fā)現(xiàn),.cpp文件中沒有protocol_list_t *protocols協(xié)議信息列表結(jié)構(gòu)體的相關(guān)信息。這是由于我們創(chuàng)建分類是并沒有遵守任何協(xié)議,自認(rèn)分類里面也就沒有任何協(xié)議相關(guān)的信息。我們返回分類Person+Test,使其遵守NSCopying協(xié)議,再通過(guò)命令行將分類的.m文件編譯成.cpp文件后查看:

protocol_list_t *protocols協(xié)議列表結(jié)構(gòu)體.png

通過(guò)上圖可以看到分類底層先將協(xié)議方法通過(guò)_method_list_t結(jié)構(gòu)體存儲(chǔ),之后通過(guò)_protocol_t結(jié)構(gòu)體存儲(chǔ)在_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test中,分別為protocol_count--協(xié)議數(shù)量以及存儲(chǔ)協(xié)議方法_protocol_t結(jié)構(gòu)體。

.cpp文件末尾處,我們看到系統(tǒng)定義了_category_t類型的_OBJC_$_CATEGORY_Person_$_Test結(jié)構(gòu)體:

_category_t _OBJC_$_CATEGORY_Person_$_Test.png

_OBJC_$_CATEGORY_Person_$_Test結(jié)構(gòu)體跟上文提到的catrgory_t結(jié)構(gòu)體對(duì)照:

category_t結(jié)構(gòu)體.png

不難看出,上下兩圖中兩個(gè)結(jié)構(gòu)體內(nèi)容一一對(duì)應(yīng),并且我們?cè)诩t框標(biāo)注的方法中看到,定義的_class_t類型的OBJC_CLASS_$_Person結(jié)構(gòu)體,最后將_OBJC_$_CATEGORY_Person_$_Testcls指針指向OBJC_CLASS_$_Person結(jié)構(gòu)體地址。我們可以得出結(jié)論,cls指針指向的應(yīng)該是分類的主類類對(duì)象的地址。

通過(guò)以上分析我們發(fā)現(xiàn),分類確實(shí)是將我們定義的對(duì)象方法,類方法屬性等都存放在catagory_t結(jié)構(gòu)體中。那么catagory_t結(jié)構(gòu)體又如何讓將這些信息存儲(chǔ)到類對(duì)象中呢?我們通過(guò)分析runtime的源碼來(lái)進(jìn)一步了解。

runtime源碼

我們通過(guò)opensource網(wǎng)站下載最新的源碼來(lái)進(jìn)一步分析。

首先來(lái)到runtime初始化函數(shù)

runtime初始化函數(shù).png

接著我們來(lái)到&map_images讀取模塊,來(lái)到map_images_nolock函數(shù)中找到_read_images函數(shù),在_read_images函數(shù)中我們找到分類相關(guān)代碼:

Discover categories代碼.png

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

remethodizeClass函數(shù).png

通過(guò)上述代碼我們發(fā)現(xiàn)attachCategories函數(shù)接收了類對(duì)象cls和分類數(shù)組cats,當(dāng)然一個(gè)類可以有多個(gè)分類,分類信息存儲(chǔ)在category_t結(jié)構(gòu)體中,那么多個(gè)分類則保存在category_list中。

我們來(lái)到attachCategories函數(shù)內(nèi)部:

attachCategories函數(shù).png

上述源碼中可以看出,首先根據(jù)方法列表,屬性列表,協(xié)議列表通過(guò)malloc分配內(nèi)存,根據(jù)多少個(gè)分類以及每一塊方法需要多少內(nèi)存來(lái)分配相應(yīng)的內(nèi)存地址。之后從分類數(shù)組里面往三個(gè)數(shù)組里面存放分類數(shù)組里面存放的分類方法,屬性以及協(xié)議放入對(duì)應(yīng)mlist、proplists、protolosts數(shù)組中,這三個(gè)數(shù)組放著所有分類的方法,屬性協(xié)議。

之后通過(guò)類對(duì)象的data()方法,拿到類對(duì)象的class_rw_t結(jié)構(gòu)體rw,在class結(jié)構(gòu)中我們介紹過(guò),class_rw_t中存放著類對(duì)象的方法屬性協(xié)議等數(shù)據(jù),rw結(jié)構(gòu)體通過(guò)類對(duì)象的data方法獲取,所以rw里面存放這類對(duì)象里面的數(shù)據(jù)。

之后分別通過(guò)rw調(diào)用方法列表、屬性列表協(xié)議列表attachList函數(shù),將所有的分類的方法、屬性、協(xié)議列表數(shù)組傳進(jìn)去,我們大致可以猜想到在attachList方法內(nèi)部將分類和本類相應(yīng)的對(duì)象方法,屬性協(xié)議進(jìn)行了合并。

我們來(lái)看一下attachLists函數(shù)內(nèi)部查看:


attachLists函數(shù).png

上述源代碼中有兩個(gè)重要的數(shù)組
array()->lists: 類對(duì)象原來(lái)的方法列表,屬性列表,協(xié)議列表。
addedLists:傳入所有分類的方法列表,屬性列表,協(xié)議列表。

attachLists函數(shù)中最重要的兩個(gè)方法為memmove內(nèi)存移動(dòng)和memcpy內(nèi)存拷貝。我們先來(lái)分別看一下這兩個(gè)函數(shù)

// memmove :內(nèi)存移動(dòng)。
/*  __dst : 移動(dòng)內(nèi)存的目的地
*   __src : 被移動(dòng)的內(nèi)存首地址
*   __len : 被移動(dòng)的內(nèi)存長(zhǎng)度
*   將__src的內(nèi)存移動(dòng)__len塊內(nèi)存到__dst中
*/
void    *memmove(void *__dst, const void *__src, size_t __len);

// memcpy :內(nèi)存拷貝。
/*  __dst : 拷貝內(nèi)存的拷貝目的地
*   __src : 被拷貝的內(nèi)存首地址
*   __n : 被移動(dòng)的內(nèi)存長(zhǎng)度
*   將__src的內(nèi)存移動(dòng)__n塊內(nèi)存到__dst中
*/
void    *memcpy(void *__dst, const void *__src, size_t __n);

下面我們圖示經(jīng)過(guò)memmovememcpy方法過(guò)后的內(nèi)存變化:

首先未經(jīng)過(guò)內(nèi)存移動(dòng)和拷貝時(shí):


未經(jīng)過(guò)內(nèi)存移動(dòng)和拷貝時(shí).png

經(jīng)過(guò)memmove方法之后,內(nèi)存變化為:

// 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]));
memcpy方法之后,內(nèi)存變化.png

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

那么為什么要將分類方法的列表追加到本來(lái)的對(duì)象方法前面呢?

其實(shí)這樣做的目的是為了保證分類方法優(yōu)先調(diào)用,我們知道當(dāng)分類重寫本類的方法時(shí),會(huì)覆蓋本類的方法。

但是經(jīng)過(guò)上面的分析我們知道本質(zhì)上并不是覆蓋,而是優(yōu)先調(diào)用。本類的方法依然在內(nèi)存中的,這一點(diǎn)可以通過(guò)打印所有類的所有方法名來(lái)查看,我們自己實(shí)現(xiàn)一個(gè)方法,打印所有類的所有方法名:

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

我們?cè)诳刂破髦幸?code>Person類,在控制器的viewDidLoad方法中創(chuàng)建Person對(duì)象,并且調(diào)用run方法和上面的打印所有類的所有方法名的方法:

- (void)viewDidLoad {
    [super viewDidLoad];    
    Preson *p = [[Preson alloc] init];
    [p run];
    [self printMethodNamesOfClass:[Preson class]];
}

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

打印內(nèi)容.png

關(guān)于Category的底層原理探索我們告一段落,如有疑問(wèn),歡迎在評(píng)論區(qū)留言。

更多技術(shù)知識(shí)請(qǐng)關(guān)注公眾號(hào)
iOS進(jìn)階


iOS進(jìn)階.jpg
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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