1. 程式人生 > >Category、load、initialize 原始碼講解

Category、load、initialize 原始碼講解

今天深圳天氣有暴風雨,沒有事情幹,趁著週末和平常晚上寫一篇關於Category知識的梳理!可能針對平常只會知道些category基本結論知道的人有些幫助,寫這篇部落格會按照下面的目錄結合例項以及Category的原始碼進行一一講解!!!

  1. Category的實現原理?
  2. Category中有load方法嗎?load方法是什麼時候呼叫的?
  3. load、initialize方法的區別是什麼?它們在Category中的呼叫的順序?以及出現繼承時呼叫過程發生怎樣的變化?
  4. 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的載入處理過程,過程可以為以下步驟:

  1. 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有什麼關係區別嘛?

  1. Objective-C 的class Extension 是在編譯的時候,它的資料已經包含在類資訊中
  2. 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方法");
}

@end
View 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 實現分為以下步驟:

  1. 獲取所有類,呼叫schedule_class_load
  2. 關於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中

  1. class_getInstanceMethod
  2. lookUpImpOrNil
  3. lookUpImpOrForward
  4. _class_initialize
  5. callInitialize
  6. 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的講解,希望對大家有所幫助!!!如果覺得寫得還不錯,給個贊吧!