Category、load、initialize 原始碼講解
今天深圳天氣有暴風雨,沒有事情幹,趁著週末和平常晚上寫一篇關於Category知識的梳理!可能針對平常只會知道些category基本結論知道的人有些幫助,寫這篇部落格會按照下面的目錄結合例項以及Category的原始碼進行一一講解!!!
- Category的實現原理?
- Category中有load方法嗎?load方法是什麼時候呼叫的?
- load、initialize方法的區別是什麼?它們在Category中的呼叫的順序?以及出現繼承時呼叫過程發生怎樣的變化?
- Category能否新增成員變數?如果可以,如何給Category新增成員變數?
一、Category的實現原理
1. 前沿Category講解-所有知識在這
在Xcode中使用Category,可以在裡面新增方法以及遵守相應的協議。下面以例項講解,首先建立ZXYPerson類,類中有物件方法run方法,建立兩個分類ZXYPerson+Test和ZXYPerson+Eat,方法分別為test和eat方法。,結構程式碼如下:
View Code執行程式碼執行結果如下:
上面person是例項物件,所以run例項方法是放在ZXYPerson的例項物件方法中,而對於ZXYPerson(Test)和ZXYPerson(Eat)中的test和eat方法放在哪裡了呢?
拓展: isa指標方法
- instance的isa指向class
當呼叫物件方法時,通過instance的isa找到class,最後找到物件方法的實現進行呼叫
- class的isa指向meta-class
當呼叫類方法時,通過class的isa找到meta-class,最後找到類方法的實現進行呼叫
答:Category的方法並不是在編譯的時候將方法加入到ZXYPerson的例項物件中的,而是在執行時通過Runtime執行時機制將分類的方法合併到到例項物件中的!
將ZXYPerson+Eat.m clang編譯成.cpp檔案,檢視編譯之後的程式碼(xcrun -sdk iphonesimulator clang -rewrite-objc ZXYPerson+Eat.m)
檢視一下協議Category的結構體如下:
上面協議ZXYPerson+Test以及ZXYPerson+Eat經過編譯會編譯成兩個_category_t結構體,在執行時會把它加入到ZXYPerson的例項物件中(也有可能是類物件中,或者元類物件)
如果換成ZXYPerson+Test協議變為.cpp檔案,_category_t這個結構體不會發生改變,但是後面的傳參會發生改變如下:
2. 原始碼講解
下面來驗證一下為什麼Category的優先順序高於主類?原理是什麼?
2.1 結論
View Code上面的run方法到底會呼叫哪一個呢,這個編譯順序有關,最後參與編譯的分類會優先呼叫
抽出上面的main.m程式碼,ZXYPerson、ZXYPerson+Eat以及ZXYPerson+Test都具有run方法(看上面收起來的程式碼)
#import <Foundation/Foundation.h> #import "ZXYPerson.h" #import "ZXYPerson+Eat.h" #import "ZXYPerson+Test.h" int main(int argc, const char * argv[]) { @autoreleasepool { ZXYPerson *person = [[ZXYPerson alloc]init]; [person run]; } return 0; }
看下編譯順序和執行結果
如果將編譯順序改變下
2.2 原因
檢視的OC的原始碼是objc4-723版本 檢視Category的載入處理過程,過程可以為以下步驟:
- objc-os.m
- _objc_init
- map_images
- map_images_nolock
2. objc-runtime-new.mm
- _read_images(images是映象,逆向開發部落格有)
- remethodizeClass
- attachCategories
- attachLists
- realloc、memmove、memcpy
直接看Category載入邏輯,搜尋attachCategories,找到對應的實現
下面針對attachCategories的實現開始講解,大家認認真真看下程式碼
static void attachCategories(Class cls, category_list *cats, bool flush_caches) { // cls: [ZXYPerson class] // cats - category: 代表[ZXYPerson+Eat,ZXYPerson+Test] if (!cats) return; if (PrintReplacedMethods) printReplacements(cls, cats); bool isMeta = cls->isMetaClass(); /**方法列表 **mlists 二維陣列 *[ * [method_t,method_t], * [method_t,method_t] * ] */ method_list_t **mlists = (method_list_t **) malloc(cats->count * sizeof(*mlists)); /**屬性列表 **proplists 二維陣列 *[ * [property_t,property_t], * [property_t,property_t] * ] */ property_list_t **proplists = (property_list_t **) malloc(cats->count * sizeof(*proplists)); /**協議列表 **protolists 二維陣列 *[ * [protol_t,protol_t], * [protol_t,protol_t] * ] */ protocol_list_t **protolists = (protocol_list_t **) malloc(cats->count * sizeof(*protolists)); // Count backwards through cats to get newest categories first int mcount = 0; int propcount = 0; int protocount = 0; int i = cats->count; bool fromBundle = NO; while (i--) {//最後面編譯的分類會優先呼叫,i--,首先取最後一個分類 auto& entry = cats->list[i]; //將category裡面的方法列表組都加入到一個數組中 method_list_t *mlist = entry.cat->methodsForMeta(isMeta); if (mlist) { mlists[mcount++] = mlist; fromBundle |= entry.hi->isBundle(); } //將category裡面的屬性列表組都加入到一個數組中 property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi); if (proplist) { proplists[propcount++] = proplist; } //將category裡面的協議列表組都加入到一個數組中 protocol_list_t *protolist = entry.cat->protocols; if (protolist) { protolists[protocount++] = protolist; } } //得到類裡面的資料 auto rw = cls->data(); prepareMethodLists(cls, mlists, mcount, NO, fromBundle); // 將所有分類的物件方法,附加到類(物件)物件的方法列表中 rw->methods.attachLists(mlists, mcount); free(mlists); if (flush_caches && mcount > 0) flushCaches(cls); // 將所有分類的屬性,附加到類(物件)物件的屬性列表中 rw->properties.attachLists(proplists, propcount); free(proplists); //將所有分類的協議,附加到類(物件)物件的協議列表中 rw->protocols.attachLists(protolists, protocount); free(protolists); }
對於方法attachCategories的引數cls:在本次例子中代表的是[ZXYPerson class]; cats代表的是分類列表[ZXYPerson+Eat,ZXYPerson+Test]
然後緊接著分配了三個陣列空間,三個二維陣列mlists,proplists以及protolists分別放著方法、屬性和協議列表,然後看一個附加載入過程的原理attachLists()方法
/**方法列表 **mlists 二維陣列 *[ * [method_t,method_t],協議ZXYPerson+Test的方法test * [method_t,method_t] 協議ZXYPerson+Eat的方法Eat * ] * addedCount = 2 */ void attachLists(List* const * addedLists, uint32_t addedCount) { if (addedCount == 0) return; if (hasArray()) { // many lists -> many lists uint32_t oldCount = array()->count; //因為ZXYPerson有一個方法run,所以oldCount為1 ,addCount兩個分類共有兩個方法,所以newCount = 3 uint32_t newCount = oldCount + addedCount; //通過realloc方法重新分配記憶體,並分配newCount的空間大小 setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); array()->count = newCount; //array()->lists:返回原來的方法列表 //void *memmove(void *__dst, const void *__src, size_t __len);將src的array()->lists移到array()->lists + addedCount,因為addedCount = 2,相當於將array()->lists向後移動2位 memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0])); //addedLists:所有分類的方法列表 //void *memcpy(void *__dst, const void *__src, size_t __n); 將src的addedLists拷貝到dst的array()->lists(原來方法的列表) //相當於分類的方法列表拷貝到了原來的方法列表位置,而原來的方法列表位於前面 memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } else if (!list && addedCount == 1) { // 0 lists -> 1 list list = addedLists[0]; } else { // 1 list -> many lists List* oldList = list; uint32_t oldCount = oldList ? 1 : 0; uint32_t newCount = oldCount + addedCount; setArray((array_t *)malloc(array_t::byteSize(newCount))); array()->count = newCount; if (oldList) array()->lists[addedCount] = oldList; memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } }
上面原始碼對應方法內容也做了比較詳細的解釋,大家可以細細體會看下!下面memcpy(拷貝)和memmove(移動)函式
現在就來回答第一個問題:Category的實現原理?
1. 通過Runtime載入某個類的所有Category資料
2. 把所有Category的方法、屬性、協議資料,合併到一個大陣列中,後面參與編譯的Category資料,會放在陣列的簽名
3. 將合併後的分類資料(方法、屬性、協議)插入到類原來資料的前面
寫到上面了,大家可能有人有疑問,那Category和Objective-C 的class Extension有什麼關係區別嘛?
- Objective-C 的class Extension 是在編譯的時候,它的資料已經包含在類資訊中
- Category在執行時才會將資料合併到類資訊中
二、Category中有load方法嘛?load方法什麼時候呼叫?
建立ZXYPerson以及ZXYPerson的分類ZXYPerson+Test和ZXYPerson+Eat,在程式碼中加入load程式碼,下面是原始碼
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { } return 0; } #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface ZXYPerson : NSObject @end NS_ASSUME_NONNULL_END #import "ZXYPerson.h" @implementation ZXYPerson +(void)load { NSLog(@"ZXYPerson的load方法"); } @end #import <Foundation/Foundation.h> #import "ZXYPerson.h" NS_ASSUME_NONNULL_BEGIN @interface ZXYPerson (Test) @end #import "ZXYPerson+Test.h" #import <Foundation/Foundation.h> @implementation ZXYPerson (Test) +(void)load { NSLog(@"ZXYPerson (Test)的load方法"); } @end #import <Foundation/Foundation.h> #import "ZXYPerson.h" NS_ASSUME_NONNULL_BEGIN @interface ZXYPerson (Eat) @end NS_ASSUME_NONNULL_END #import "ZXYPerson+Eat.h" @implementation ZXYPerson (Eat) +(void)load { NSLog(@"ZXYPerson (Test)的load方法"); } @endView Code
剛剛程式碼的編譯順序如下:
下面通過原始碼分析一下load的呼叫順序! objc4原始碼解讀過程:objc-os.mm
1. _objc_init,點選load_images
2. 進入load_images,看到prepare_load_methods和call_load_methods,首先看call_load_methods
3. 檢視call_load_methods
上面就驗證了一個觀點:先呼叫類的+load,然後再呼叫分類的+load
緊接著分別檢視call_class_loads方法和call_category_loads方法
4. call_class_loads 載入類的load方法
假如專案中有100個類, 到底先呼叫哪一個類呢?
回到上面2中的load_images中,發現呼叫call_classes_loads之前也做了呼叫prepare_load_methods,再次進入了prepare_load_method中,看看有沒有做一些準備工作:
5. prepare_load_method的實現
prepare_load_method 實現分為以下步驟:
- 獲取所有類,呼叫schedule_class_load
- 關於classlist和categorylist兩個陣列順序是根據類、分類被編譯的順序放到了對應的陣列中去
6. 類load的呼叫順序
確定類load的呼叫順序,依賴於schedule_class_load,它的實現如下:
上面源碼錶明首先呼叫父類,然後子類!
對於重寫了+load的類,load方法呼叫順序是先編譯的類的父類>先編譯的類>後編譯類的父類>後編譯的類
總結:load呼叫順序(+load)方法是根據方法地址直接呼叫,並不是經過objc_msgSend函式呼叫
1. 先呼叫類的+load
- 按照編譯先後順序呼叫(先編譯先呼叫)
- 呼叫子類的+load之前會先呼叫父類的+load
2. 再呼叫分類的+load方法
- 按照編譯先後順序呼叫(先編譯先呼叫)
三、initialize講解
+initialize的方法在類第一次接受訊息時會被呼叫
1. 例子
首先給ZXYStudent類和Person類覆蓋+initialize,主要程式碼如下
//ZXYPerson + (void)initialize{ NSLog(@"Person + initialize"); } //ZXYPerson +Test + (void)initialize{ NSLog(@"Person (Test1) + initialize"); } //ZXYPerson+Eat + (void)initialize{ NSLog(@"Person (Eat) + initialize"); } //ZXYStudent + (void)initialize{ NSLog(@"Student + initialize"); } //ZXYStudent (Test) + (void)initialize{ NSLog(@"Student (Test) + initialize"); } //ZXYStudent (Eat) + (void)initialize{ NSLog(@"Student (Eat) + initialize"); }
此時執行程式,並沒有發現列印,說明執行沒有呼叫initialize
假如給ZXYPerson類傳送訊息,如下,打印出ZXYPerson+Test的initialize方法
因為initialize是走runtime那套,通過msgSend()方式,所以先打印出ZXYPerson的分類方法,至於先打印出哪一個分類的initialize,看編譯順序(先編譯後執行)
當使用ZXYPerson的子類ZXYStudent傳送訊息
發現先呼叫ZXYPerson的分類,然後再呼叫子類的分類!也可以多寫幾行[ZXYStudent alloc],發現還是一樣的列印,說明initialize僅僅在該類第一次收到訊息才會呼叫,下面通過檢視原始碼講解其呼叫順序!
2. 原始碼講解
使用objc4檢視initialize的原始碼解讀過程主要在 objc-runtime-new.mm中
- class_getInstanceMethod
- lookUpImpOrNil
- lookUpImpOrForward
- _class_initialize
- callInitialize
- objc_msgSend(cls, SEL initialize)
1). 上面[ZXYStudent alloc]相當於objc_msgSend([ZXYStudent class], @selector(alloc))
2). 然後進行點選紅色內容
3). 點進去,然後繼續尋找lookUpImpOrForward,原始碼主要內容
這樣的原始碼說明了每個類的+initialize方法只會呼叫一次
4). _class_initialize原始碼如下
上面的原始碼說明首先呼叫父類的+initialize,然後再呼叫子類的+initialize,就像上面先呼叫ZXYPerson的+initialize,然後再呼叫ZXYStudent的+initialize
5). 最後檢視callInitialize
說明+initialize方法是通過msgSend()的方式進行呼叫
3. 總結
initialize呼叫順序
首先呼叫父類的+initialize方法,然後再呼叫子類的+initialize
(先初始化父類,然後初始化子類,每個類只會被初始化1次)
通過上面總結下+initialize和+load區別
1. 呼叫方式
- load是根據函式地址直接呼叫
- initialize是通過objc_msgSend()呼叫
2. 呼叫時刻
- load是runtime載入類、分類的時候呼叫(只會被呼叫1次)
- initialize是類第一次接收到訊息的時候呼叫,每一個類只會initialize一次(父類的initialize可能會呼叫多次-子類和分類都沒有實現+initialize可能存在)
3. 呼叫順序
load方法
- load方法先呼叫類的load(先編譯的類優先呼叫load,呼叫子類的load之前,會優先呼叫父類)
- 然後呼叫分類的load,先編譯的分類,會優先呼叫load方法
initialize
- 先初始化父類
- 再初始化子類
四. category能新增成員變數(例項變數)(屬性)嘛?
預設情況下: 不能
因為類的記憶體佈局在編譯時候就已經確定了,Category是在執行時才載入的,無法更改早已確定的記憶體佈局。
由於分類底層結構的限制,不能新增成員變數到分類中,但是可以通過關聯(Associate)方式進行間接實現!
上面就是Category、load和initialize的講解,希望對大家有所幫助!!!如果覺得寫得還不錯,給個贊吧!