首先咱們寫一段簡單的代碼,以後的分析都基於這段代碼。c++
Presen類 // Presen.h #import <Foundation/Foundation.h> @interface Preson : NSObject { int _age; } - (void)run; @end // Presen.m #import "Preson.h" @implementation Preson - (void)run { NSLog(@"Person - run"); } @end Presen擴展1 // Presen+Test.h #import "Preson.h" @interface Preson (Test) <NSCopying> - (void)test; + (void)abc; @property (assign, nonatomic) int age; - (void)setAge:(int)age; - (int)age; @end // Presen+Test.m #import "Preson+Test.h" @implementation Preson (Test) - (void)test { } + (void)abc { } - (void)setAge:(int)age { } - (int)age { return 10; } @end Presen分類2 // Preson+Test2.h #import "Preson.h" @interface Preson (Test2) @end // Preson+Test2.m #import "Preson+Test2.h" @implementation Preson (Test2) - (void)run { NSLog(@"Person (Test2) - run"); } @end 複製代碼
咱們以前講到過實例對象的isa指針指向類對象,類對象的isa指針指向元類對象,當p調用run方法時,類對象的isa指針找到類對象的isa指針,而後在類對象中查找對象方法,若是沒有找到,就經過類對象的superclass指針找到父類對象,接着去尋找run方法。面試
那麼當調用分類的方法時,步驟是否和調用對象方法同樣呢? 分類中的對象方法依然是存儲在類對象中的,同對象方法在同一個地方,那麼調用步驟也同調用對象方法同樣。若是是類方法的話,也一樣是存儲在元類對象中。 那麼分類方法是如何存儲在類對象中的,咱們來經過源碼看一下分類的底層結構。數組
如何驗證上述問題?經過查看分類的源碼咱們能夠找到category_t 結構體。bash
struct category_t { const char *name; classref_t cls; struct method_list_t *instanceMethods; // 對象方法 struct method_list_t *classMethods; // 類方法 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的方式,對象方法,類方法,協議,和屬性均可以找到對應的存儲方式。而且咱們發現分類結構體中是不存在成員變量的,所以分類中是不容許添加成員變量的。分類中添加的屬性並不會幫助咱們自動生成成員變量,只會生成get set方法的聲明,須要咱們本身去實現。markdown
經過源碼咱們發現,分類的方法,協議,屬性等好像確實是存放在categroy結構體裏面的,那麼他又是如何存儲在類對象中的呢? 咱們來看一下底層的內部方法探尋其中的原理。 首先咱們經過命令行將Preson+Test.m文件轉化爲c++文件,查看其中的編譯過程。app
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Preson+Test.m
複製代碼
在分類轉化爲c++文件中能夠看出_category_t結構體中,存放着類名,對象方法列表,類方法列表,協議列表,以及屬性列表。iphone
緊接着,咱們能夠看到_method_list_t類型的結構體,以下圖所示函數
上圖中咱們發現這個結構體_OBJC_$_CATEGORY_INSTANCE_METHODS_Preson_$_Test
從名稱能夠看出是INSTANCE_METHODS對象方法,而且一一對應爲上面結構體內賦值。咱們能夠看到結構體中存儲了方法佔用的內存,方法數量,以及方法列表。而且從上圖中找到分類中咱們實現對應的對象方法,test , setAge, age三個方法學習
接下來咱們發現一樣的_method_list_t類型的類方法結構體,以下圖所示this
同上面對象方法列表同樣,這個咱們能夠看出是類方法列表結構體 _OBJC_$_CATEGORY_CLASS_METHODS_Preson_$_Test
,同對象方法結構體相同,一樣能夠看到咱們實現的類方法,abc。
接下來是協議方法列表
經過上述源碼能夠看到先將協議方法經過_method_list_t結構體存儲,以後經過_protocol_t結構體存儲在_OBJC_CATEGORY_PROTOCOLS_$_Preson_$_Test
中同_protocol_list_t結構體一一對應,分別爲protocol_count 協議數量以及存儲了協議方法的_protocol_t結構體。
最後咱們能夠看到屬性列表
屬性列表結構體_OBJC_$_PROP_LIST_Preson_$_Test
同_prop_list_t結構體對應,存儲屬性的佔用空間,屬性屬性數量,以及屬性列表,從上圖中能夠看到咱們本身寫的age屬性。
最後咱們能夠看到定義了_OBJC_$_CATEGORY_Preson_$_Test
結構體,而且將咱們上面着重分析的結構體一一賦值,咱們經過兩張圖片對照一下。
上下兩張圖一一對應,而且咱們看到定義_class_t
類型的OBJC_CLASS_$_Preson
結構體,最後將_OBJC_$_CATEGORY_Preson_$_Test
的cls
指針指向OBJC_CLASS_$_Preson
結構體地址。咱們這裏能夠看出,cls
指針指向的應該是分類的主類類對象的地址。
經過以上分析咱們發現。分類源碼中確實是將咱們定義的對象方法,類方法,屬性等都存放在catagory_t結構體中。接下來咱們在回到runtime源碼查看catagory_t存儲的方法,屬性,協議等是如何存儲在類對象中的。
首先來到runtime初始化函數
接着咱們來到 &map_images讀取模塊(images這裏表明模塊),來到map_images_nolock函數中找到_read_images函數,在_read_images函數中咱們找到分類相關代碼
從上述代碼中咱們能夠知道這段代碼是用來查找有沒有分類的。經過_getObjc2CategoryList函數獲取到分類列表以後,進行遍歷,獲取其中的方法,協議,屬性等。能夠看到最終都調用了remethodizeClass(cls);函數。咱們來到remethodizeClass(cls);函數內部查看。
經過上述代碼咱們發現attachCategories函數接收了類對象cls和分類數組cats,如咱們一開始寫的代碼所示,一個類能夠有多個分類。以前咱們說到分類信息存儲在category_t結構體中,那麼多個分類則保存在category_list中。咱們來到attachCategories函數內部。
上述源碼中能夠看出,首先根據方法列表,屬性列表,協議列表,malloc分配內存,根據多少個分類以及每一塊方法須要多少內存來分配相應的內存地址。以後從分類數組裏面往三個數組裏面存放分類數組裏面存放的分類方法,屬性以及協議放入對應mlist、proplists、protolosts數組中,這三個數組放着全部分類的方法,屬性和協議。 以後經過類對象的data()方法,拿到類對象的class_rw_t結構體rw,在class結構中咱們介紹過,class_rw_t中存放着類對象的方法,屬性和協議等數據,rw結構體經過類對象的data方法獲取,因此rw裏面存放這類對象裏面的數據。 以後分別經過rw調用方法列表、屬性列表、協議列表的attachList函數,將全部的分類的方法、屬性、協議列表數組傳進去,咱們大體能夠猜測到在attachList方法內部將分類和本類相應的對象方法,屬性,和協議進行了合併。
咱們來看一下attachLists函數內部。
上述源代碼中有兩個重要的數組 array()->lists: 類對象原來的方法列表,屬性列表,協議列表。 addedLists:傳入全部分類的方法列表,屬性列表,協議列表。
attachLists函數中最重要的兩個方法爲memmove內存移動和memcpy內存拷貝。咱們先來分別看一下這兩個函數
// memmove :內存移動。
/* __dst : 移動內存的目的地
* __src : 被移動的內存首地址
* __len : 被移動的內存長度
* 將__src的內存移動__len塊內存到__dst中
*/
void *memmove(void *__dst, const void *__src, size_t __len);
// memcpy :內存拷貝。
/* __dst : 拷貝內存的拷貝目的地
* __src : 被拷貝的內存首地址
* __n : 被移動的內存長度
* 將__src的內存拷貝__n塊內存到__dst中
*/
void *memcpy(void *__dst, const void *__src, size_t __n);
複製代碼
下面咱們圖示通過memmove和memcpy方法事後的內存變化。
通過memmove方法以後,內存變化爲
// array()->lists 原來方法、屬性、協議列表數組
// addedCount 分類數組長度
// oldCount * sizeof(array()->lists[0]) 原來數組佔據的空間
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
複製代碼
通過memmove方法以後,咱們發現,雖然本類的方法,屬性,協議列表會分別後移,可是本類的對應數組的指針依然指向原始位置。
memcpy方法以後,內存變化
// array()->lists 原來方法、屬性、協議列表數組
// addedLists 分類方法、屬性、協議列表數組
// addedCount * sizeof(array()->lists[0]) 原來數組佔據的空間
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
複製代碼
咱們發現原來指針並無改變,至始至終指向開頭的位置。而且通過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]]; } 複製代碼
經過下圖中打印內容能夠發現,調用的是Test2中的run方法,而且Person類中存儲着兩個run方法。
問: Category的實現原理,以及Category爲何只能加方法不能加屬性?
答:分類的實現原理是將category中的方法,屬性,協議數據放在category_t結構體中,而後將結構體內的方法列表拷貝到類對象的方法列表中。 Category能夠添加屬性,可是並不會自動生成成員變量及set/get方法。由於category_t結構體中並不存在成員變量。經過以前對對象的分析咱們知道成員變量是存放在實例對象中的,而且編譯的那一刻就已經決定好了。而分類是在運行時纔去加載的。那麼咱們就沒法再程序運行時將分類的成員變量中添加到實例對象的結構體中。所以分類中不能夠添加成員變量。
load方法會在程序啓動就會調用,當裝載類信息的時候就會調用。 調用順序看一下源代碼。
經過源碼咱們發現是優先調用類的load方法,以後調用分類的load方法。咱們經過代碼驗證一下: 咱們添加Student繼承Presen類,並添加Student+Test分類,分別重寫只+load方法,其餘什麼都不作經過打印發現
確實是優先調用類的load方法以後調用分類的load方法,不過調用類的load方法以前會保證其父類已經調用過load方法。以後咱們爲Preson、Student 、Student+Test 添加initialize方法。 咱們知道當類第一次接收到消息時,就會調用initialize,至關於第一次使用類的時候就會調用initialize方法。調用子類的initialize以前,會先保證調用父類的initialize方法。若是以前已經調用過initialize,就不會再調用initialize方法了。當分類重寫initialize方法時會先調用分類的方法。可是load方法並不會被覆蓋,首先咱們來看一下initialize的源碼。
上圖中咱們發現,initialize是經過消息發送機制調用的,消息發送機制經過isa指針找到對應的方法與實現,所以先找到分類方法中的實現,會優先調用分類方法中的實現。
咱們再來看一下load方法的調用源碼
咱們看到load方法中直接拿到load方法的內存地址直接調用方法,不在是經過消息發送機制調用。 咱們能夠看到分類中也是經過直接拿到load方法的地址進行調用。所以正如咱們以前試驗的同樣,分類中重寫load方法,並不會優先調用分類的load方法,而不調用本類中的load方法了。問:Category中有load方法嗎?load方法是何時調用的?load 方法能繼承嗎?
答:Category中有load方法,load方法在程序啓動裝載類信息的時候就會調用。load方法能夠繼承。調用子類的load方法以前,會先調用父類的load方法
問:load、initialize的區別,以及它們在category重寫的時候的調用的次序。
答:區別在於調用方式和調用時刻 調用方式:load是根據函數地址直接調用,initialize是經過objc_msgSend調用 調用時刻:load是runtime加載類、分類的時候調用(只會調用1次),initialize是類第一次接收到消息的時候調用,每個類只會initialize一次(父類的initialize方法可能會被調用屢次)
調用順序:先調用類的load方法,先編譯那個類,就先調用load。在調用load以前會先調用父類的load方法。分類中load方法不會覆蓋本類的load方法,先編譯的分類優先調用load方法。initialize先初始化父類,以後再初始化子類。若是子類沒有實現+initialize,會調用父類的+initialize(因此父類的+initialize可能會被調用屢次),若是分類實現了+initialize,就覆蓋類自己的+initialize調用。
本文是對底層原理學習的總結,若是有不對的地方請指正,歡迎你們一塊兒交流學習 xx_cc 。