iOS底層原理總結 - 探尋Category本質

本篇主要是對小碼哥底層視頻學習的總結。方便日后復習。
上一篇《iOS底層原理總結 - 探尋KVO的本質》:
http://www.lxweimin.com/p/f5b3199982e6

本篇學習總結:

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

好了,帶著問題,我們一一開始閱讀吧 ??

一.探尋Category本質

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

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

@interface MJPerson : NSObject

- (void)run;

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

// class extension (匿名分類\類擴展)
@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;
}

我們之前講過實例對象的isa指針指向類對象,類對象的isa指針指向元類對象,當p調用run方法時,通過實例對象的isa指針找到類對象,然后在類對象中查找對象方法,如果沒有找到,通過類對象的superclass指針找到父類的類對象,接著去尋找run方法。
那么問題來了,當我們調用分類中的run方法時,它也會按照我們前面說的方式去查找run方法嗎?
小碼哥直接給出結論了,先看結論,再看底層代碼實現

分類中的對象方法依然存儲在類對象中,同本類對象在同一個地方,調用步驟也同本類調用對象方法一樣,同理,如果是類方法的話,是存儲在元類對象中的。

二.Category底層代碼分析

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

轉化C++文件.png

  • _category_t:分類結構體

我們點開MJPerson+Eat.cpp文件,搜索category,我們可以找到 _category_t 結構體,結構如下:

_category_t結構體信息.png

從底層代碼中可以看出,_category_t 結構體包含對象方法列表,類方法列表,協議列表,屬性列表等信息,但是沒有找到成員變量信息,是不是可以初步肯定一下之前的結論:分類中可以添加對象方法,類方法,協議,屬性,不可以添加成員變量,category可以添加屬性,但是不會自動生成成員變量,只能生成setter getter方法,還需要我們手動實現一下方法。

那么我們就一個變量一個變量的分析吧

  • _method_list_t:方法列表

然后搜索 _method_list_t,結構體如下:

_method_list_t結構體信息.png

此時我們發現了兩個名字比較長的變量名,分別是
_OBJC__CATEGORY_INSTANCE_METHODS_MJPerson__Eat_OBJC__CATEGORY_CLASS_METHODS_MJPerson__Eat,從名稱上可以推測是類方法實例方法,下面的代碼賦值跟上面的結構體成員變量一一對應,我們可以看到結構體中存儲了方法占用的內存,方法數量,以及方法列表。并且從上圖中找到分類中我們實現對應的對象方法。

  • _protocol_list_t:協議列表信息

繼續搜索 _protocol_list_t,結構體如下:

_protocol_list_t結構體信息.png

這里同樣看到了一個名字比較長的變量名:_OBJC_CATEGORY_PROTOCOLS__MJPerson__Ea,下面的代碼賦值跟上面的結構體成員變量一一對應。

  • _prop_list_t:屬性列表信息

最后搜索 _prop_list_t,結構體如下:

_prop_list_t結構體信息.png

這里同樣看到了一個名字比較長的變量名:_OBJC__PROP_LIST_MJPerson__Eat,下面的代碼賦值跟上面的結構體成員變量一一對應。

最后我們再搜索一下category 發現了這么一個變量:
**_OBJC__CATEGORY_MJPerson__Eat ** 變量時屬于 _category_t 結構體類型,我們再來看一下 _category_t 結構體信息。

_OBJC_$_CATEGORY_MJPerson_$_Eat結構體信息.png
_category_t結構體信息.png

上下兩張圖對比來看,我們發現定義了category_t類型的變量,
MJPerson:賦值給name;
OBJC_CLASS
$_MJPreson :賦值給cls指針變量;
對應的列表信息一一賦值給變量。

通過以上分析我們發現,編譯時期,分類被編譯成了catagory_t結構體類型的變量,分類中的對象方法,類方法,屬性,協議等都存放在catagory_t結構體對應的成員變量中。

回到最初的結論,分類中的實例方法是如何放到類對象中去呢,這要從runtime源碼說起,下面是通過查看runtime源碼找到catagory_t存儲的方法,屬性,協議等是如何存儲在類對象中的。

我們先記錄一下看源碼的順序,源碼本來就有點晦澀難懂,我們根據順序去查看


runtime查看源碼順序.png

首先我們下載源碼,打開objc-os.mm文件,先從runtime初始函數看起

objc_init函數.png

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

map_images.png

點擊 map_images_nolock 函數中找到_read_images函數,

map_images_nolock部分代碼.png

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

_read_images重組類信息.png

從上述代碼中我們可以知道這段代碼是用來查找有沒有分類的。通過_getObjc2CategoryList函數獲取分類列表之后,進行遍歷,獲取其中的方法,協議,屬性等,可以看到最終都調用了remethodizeClass(cls)函數,我們點進去查看一下

remethodizeClass.png

通過上述代碼我們發現attachCategories函數接收了類對象cls和分類數組cats,如我們一開始寫的代碼所示,一個類有多少個分類,就會有多少個category_t結構體類型的變量,這些分類信息都都保存在category_list中。我們來到attachCategories函數內部

attachCategories-1.png

attachCategories-2.png

attachCategories-3.png

上述代碼中可以看出,這步驟才是關鍵步驟了

  • 1.首先根據傳進來的cats數組分別創建了mlist,proplists,protocols三個二維數組,用于存儲方法每個分類的方法列表,屬性列表,協議列表
  • 2.通過while(i--)倒序方式進行遍歷循環,取出每一個分類中的方法數組,屬性數組,協議數組,存進mlist,proplists,protocols三個二維數組中
  • 3.取出類對象的class_rw_t,我們在《iOS底層原理總結 - 探尋Class的本質》 中已經講過了,類對象中存儲的方法列表,屬性信息,協議信息,成員變量信息都存儲在class_rw_t
  • 4.將存放所有分類方法的二維數組 mlist 附加到類對象的方法列表中
  • 5.將存放所有屬性方法的二維數組 proplists 附加到類對象的屬性列表中
  • 6.將存放所有協議方法的二維數組 protocols 附加到類對象的協議列表中

我們看一下attachLists函數內部實現:

attachLists函數內部實現.png

array()->lists:類對象原來的方法列表,屬性列表,協議列表。
addedLists:所有分類的方法列表,屬性列表,協議列表。
attachLists函數里面最重要的兩個方法為memmove(內存移動方法)和memcpy(內存拷貝方法)。
我們分別說一下這兩個函數,


memmove和memcpy方法說明.png

下圖是經過memmove函數之后數據在內存中分配情況
1.在原有空間上擴容addedCount大小空間


增大內存空間.png

2.遵循menmove方法移動原則,原有方法開始往后移動
// array()->lists 原來方法、屬性、協議列表數組
// addedCount 分類數組長度
// oldCount * sizeof(array()->lists[0]) 原來數組占據的空間
memmove(array()->lists + addedCount, array()->lists, 
                  oldCount * sizeof(array()->lists[0]));
memmove方法之后內存變化.png

經過memmove方法之后,我們發現,雖然本類的方法,屬性,協議列表會分別后移,但是本類的對應數組的指針依然指向原始位置。
memcpy方法之后,內存變化

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

我們發現原來指針并沒有改變,至始至終指向開頭的位置。并且經過memmove和memcpy方法之后,分類的方法,屬性,協議列表被放在了類對象中原本存儲的方法,屬性,協議列表前面。

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 獲得方法數組
    Method *methodList = class_copyMethodList(cls, &count);
    // 存儲方法名
    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]];
}

那么為什么要將分類方法的列表追加到本來的對象方法前面呢,這樣做的目的是為了保證分類方法優先調用,我們知道當分類重寫本類的方法時,會覆蓋本類的方法。
其實經過上面的分析我們知道本質上并不是覆蓋,而是優先調用。本類的方法依然在內存中的。我們可以通過打印所有類的所有方法名來查看

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 獲得方法數組
    Method *methodList = class_copyMethodList(cls, &count);
    // 存儲方法名
    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]];
}

通過下圖中打印內容可以發現,調用的是Test2中的run方法,并且Person類中存儲著兩個run方法


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

load 方法會在程序啟動,加載類,分類信息的時候調用,只調用一次,調用方法是指針直接調用,一般不主動調用。
先看代碼

load方法調用順序.png

再來看一下調用load方法的具體實現

load方法內部實現.png

通過源碼我們發現load方法調用順序是優先調用類的load方法,如有繼承關系的類,調用子類的時候會有限調用父類的load方法,之后調用分類的load方法,分類按照編譯順序調用

我們看到load方法中直接拿到load方法的內存地址直接調用方法,不在是通過消息發送機制調用。


分類load方法調用源碼.png

代碼驗證如下:
我們添加Student繼承Presen類,并添加Student+Test分類,分別重寫只+load方法,其他什么都不做通過打印發現:


load方法打印.png

最后用一張圖總結load方法


load方法總結及源碼查看順序.png

** initialize** 方法當類第一次接收到消息時,優先調用父類的initialize方法,在調用子類的initialize方法。
之后我們為Preson、Student 、Student+Test 添加initialize方法。
第一次使用類的時候就會調用initialize方法。調用子類的initialize之前,會先保證調用父類的initialize方法。如果之前已經調用過initialize,就不會再調用initialize方法了。當分類重寫initialize方法時會先調用分類的方法。但是load方法并不會被覆蓋,首先我們來看一下initialize的源碼。

initialize調用機制.png

最后用一張圖總結:


initialize方法總結及源碼順序.png

總結本篇面試題:

  • 1.Category的實現原理,或者加載處理過程是什么樣的?

1>編譯時期將所有categor轉化成category_t的結構體變量,并賦值
2>通過runtime加載某個類的所有category數據
3>把所有category的方法,屬性,協議數組,合并到一個大數組中
a)后面參與編譯的category數據,會在數組的前面
4》將合并后的分類數據(方法,屬性,協議),合并到類原來數據的前面

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

category可以添加屬性,但是并不會主動生成成員變量及setter/getter方法,因為category_t結構體中并不存在成員變量,通過之前對對象的分析我們知道成員變量是存放在實例對象中,并且編譯的那一刻都已經決定好了,而分類是在運行時才去加載的,那么我們就無法運行時將分類的成員變量添加到實例對象的結構體中,因此category中可以添加屬性,不可添加成員變量。

  • 3.load initialize方法的區別是什么?

a.調用方式:
1>load 是根據函數地址直接調用;
2>initialize是通過objc_msgSend方法調用;
b>調用時刻
1>load 是runtime加載類,分類的時候調用(只會調用一次);
2>initialize 是類第一次接收到消息的時候調用,每一個類只會initialize一次(父類的initialize方法可能會被調用多次,具體例子是當多個子類都沒有實現initialize方法,卻是第一次給類發送消息)

  • 4.load initialize 方法的調用順序?

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

本篇學習先記錄到此,感謝閱讀,如有錯誤,不吝賜教。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容