【OC Runtime】對象、類、元類、分類的本質(zhì)

目錄

弄明白對象、類、元類的內(nèi)存里存儲的是什么東西就行
一、對象的本質(zhì)
二、類的本質(zhì)
三、元類的本質(zhì)
四、分類的本質(zhì)

2006年蘋果發(fā)布了OC2.0,其中對Runtime的很多API做了改進,并把OC1.0中Runtime的很多API標記為“將來會被廢棄”。 但是兩套API的核心實現(xiàn)思路還是一樣的,而舊API比較簡單,所以我們會分析舊API,然后看看新API作了哪些變化,這里有最新的Runtime源碼


一、對象的本質(zhì)


1、OC1.0

通過查看Runtime的源碼(objc.h文件),我們得到對象的定義如下(偽代碼):

struct objc_object {
    // 固定的成員變量
    Class isa;

    // 我們自定義的成員變量
    NSSring *_name;
    NSSring *_sex;
    int _age;
};

typedef struct objc_object *id; // id類型的本質(zhì)就是一個objc_object類型的結(jié)構(gòu)體指針,所以它可以指向任意一個OC對象

可見對象的本質(zhì)就是一個objc_object類型的結(jié)構(gòu)體。該結(jié)構(gòu)體內(nèi)部只有一個固定的成員變量isa,它是一個Class類型的結(jié)構(gòu)體指針,存儲著一個地址,指向該對象所屬的類。當然結(jié)構(gòu)體內(nèi)部還可能有很多我們自定義的成員變量,存儲著該對象這些成員變量具體的值。

2、OC2.0

通過查看Runtime的源碼(objc-private.h文件),我們得到對象的定義如下(偽代碼):

struct objc_object {
    // 固定的成員變量
    isa_t isa;

    // 自定義的成員變量
    NSSring *_name;
    NSSring *_sex;
    int _age;
}

// 共用體isa_t
//
// 共用體也是C語言的一種數(shù)據(jù)類型,和結(jié)構(gòu)體差不多,
// 都可以定義很多的成員變量,但兩者的主要區(qū)別就在于內(nèi)存的使用。
//
// 一個結(jié)構(gòu)體占用的內(nèi)存等于它所有成員變量占用內(nèi)存之和,而且要遵守內(nèi)存對齊規(guī)則,而一個共用體占用的內(nèi)存等于它最寬成員變量占用的內(nèi)存。
// 結(jié)構(gòu)體里所有的成員變量各自有各自的內(nèi)存,而共用體里所有的成員變量共用這一塊內(nèi)存。
// 所以共用體可以更加節(jié)省內(nèi)存,但是我們要把數(shù)據(jù)處理好,否則很容易出現(xiàn)數(shù)據(jù)覆蓋。
union isa_t {
    Class cls;
    
    unsigned long bits; // 8個字節(jié),64位
    struct { // 其實所有的數(shù)據(jù)都存儲在成員變量bits里面,因為外界只訪問它,而這個結(jié)構(gòu)體則僅僅是用位域來增加代碼的可讀性,讓我們看到bits里面相應(yīng)的位上存儲著什么數(shù)據(jù)
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
        unsigned long nonpointer        : 1;
        unsigned long has_assoc         : 1;
        unsigned long has_cxx_dtor      : 1;
        unsigned long shiftcls          : 33; // 當前對象所屬類的地址信息
        unsigned long magic             : 6;
        unsigned long weakly_referenced : 1; // 當前對象是否有弱引用
        unsigned long deallocating      : 1;
        unsigned long has_sidetable_rc  : 1; // 引用計數(shù)表里是否有當前對象的引用計數(shù)
        unsigned long extra_rc          : 19; // 對象的引用計數(shù) - 1,存不下了就會放到引用計數(shù)表里
# endif
    };
};

typedef struct objc_object *id; // id類型的本質(zhì)就是一個objc_object類型的結(jié)構(gòu)體指針,所以它可以指向任意一個OC對象

可見對象的本質(zhì)還是一個objc_object類型的結(jié)構(gòu)體。該結(jié)構(gòu)體內(nèi)部也還是只有一個固定的成員變量isa,只不過64位操作系統(tǒng)以后,對isa做了內(nèi)存優(yōu)化,它不再直接是一個指針,而是一個isa_t類型的共用體,它同樣占8個字節(jié)64位,但其中只有33位用來存儲對象所屬類的地址信息,還有19位用來存儲(對象的引用計數(shù) - 1)、存不下了就會放到引用計數(shù)表里,還有1位用來存儲對象是否有弱引用,其它位上則存儲著各種各樣的標記信息。

  • nonpointer:占1位,標記isa是否經(jīng)過內(nèi)存優(yōu)化。如果值為0,代表isa沒經(jīng)過內(nèi)存優(yōu)化,它就是一個普通的isa指針,64位全都用來存儲該對象所屬類的地址;如果值為1,代表isa經(jīng)過了內(nèi)存優(yōu)化,只有33位用來存儲對象所屬類的地址信息,其它位則另有用途,了解一下即可;
  • has_assoc:占1位,標記當前對象是否有關(guān)聯(lián)對象,如果沒有,對象銷毀時會更快,了解一下即可;
  • has_cxx_dtor:占1位,標記當前對象是否有C++析構(gòu)函數(shù),如果沒有,對象銷毀時會更快,了解一下即可;
  • shiftcls:占33位,存儲著當前對象所屬類的地址信息;
  • magic:占1位,用來標記在調(diào)試時當前對象是否未完成初始化,了解一下即可;
  • weakly_referenced:占1位,標記弱引用表里是否有當前對象的弱指針數(shù)組——即當前對象是否被弱指針指向著、當前對象是否有弱引用;
  • deallocating:占1位,標記當前對象是否正在釋放,了解一下即可;
  • has_sidetable_rc:占1位,標記引用計數(shù)表里是否有當前對象的引用計數(shù);
  • extra_rc:占19位,存儲著(對象的引用計數(shù) - 1),存不下了就會放到引用計數(shù)表里,存值范圍為0~255。


二、類的本質(zhì)


1、OC1.0

通過查看Runtime的源碼(runtime.h文件),我們得到類的定義如下(偽代碼):

struct objc_class {
    Class isa;
    Class super_class;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    const ivar_list_t *ivars;

    cache_t cache;
   
    const char *name;
    long instance_size;
    long version;
    long info;
};

typedef struct objc_class *Class; // Class類型的本質(zhì)就是一個objc_class類型的結(jié)構(gòu)體指針,所以它可以指向任意一個OC類

可見類的本質(zhì)就是一個objc_class類型的結(jié)構(gòu)體,該結(jié)構(gòu)體內(nèi)部有若干個成員變量,其中有幾個是我們重點關(guān)注的:

  • isa指針:存儲著一個地址,指向該類所屬的類——即元類;
  • superclass指針:存儲著一個地址,指向該類的父類;
  • methods:數(shù)組指針,存儲著該類所有的實例方法信息;
  • properties:數(shù)組指針,存儲著該類所有的屬性信息;
  • protocols:數(shù)組指針,存儲著該類所有遵守的協(xié)議信息;
  • ivars:數(shù)組指針,存儲著該類所有的成員變量信息;
  • cache:結(jié)構(gòu)體,存儲著該類所有的方法緩存信息。

2、OC2.0

通過查看Runtime的源碼(objc-runtime-new.h文件),我們得到類的定義如下(偽代碼):

struct objc_class : objc_object {
//    isa_t isa; // objc_class繼承自objc_object,所以不考慮內(nèi)存對齊的前提下,可以直接把isa成員變量搬過來
    Class superclass;
    
    class_data_bits_t bits; // 存儲著該類的具體信息,按位與掩碼FAST_DATA_MASK便可得到class_rw_t
    
    cache_t cache;
}

// class_rw_t結(jié)構(gòu)體就是該類的可讀可寫信息(rw即readwrite)
struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro; // 該類的只讀信息

    method_array_t methods; // 存儲著該類所有的實例方法信息,包括分類的
    property_array_t properties; // 存儲著該類所有的屬性信息,包括分類的
    protocol_array_t protocols; // 存儲著該類所有遵守的協(xié)議信息,包括分類的

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

// class_ro_t結(jié)構(gòu)體就是該類的只讀信息(ro即readonly)
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList; // 存儲著該類本身的實例方法信息
    protocol_list_t * baseProtocols; // 存儲著該類本身遵守的協(xié)議信息
    const ivar_list_t * ivars; // 存儲著該類本身的成員變量信息

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties; // 存儲著該類本身的屬性信息
}

typedef struct objc_class *Class; // Class類型的本質(zhì)就是一個objc_class類型的結(jié)構(gòu)體指針,所以它可以指向任意一個OC類

可見類的本質(zhì)還是一個objc_class類型的結(jié)構(gòu)體,我們重點關(guān)注的那幾個成員變量都還是可以順利找到的,只不過它的內(nèi)部結(jié)構(gòu)套了兩層rwro,我們先說一下ro,ro內(nèi)部存儲著經(jīng)過編譯后一個類本身定義的所有實例方法、屬性、協(xié)議、成員變量,它是只讀的,然后在運行時系統(tǒng)才會生成rw,把ro里類本身定義的所有實例方法、屬性、協(xié)議搞到rw里,并把這個類所有分類里的實例方法、屬性、協(xié)議合并到rw里,rw是可讀可寫的,這在解釋“為什么分類不能給類擴展成員變量”時是一個很好的證據(jù),因為成員變量是在編譯器就決定好的,是只讀的,不能新增,這可能是為了保證類創(chuàng)建出來的對象都有一樣的內(nèi)存結(jié)構(gòu)。


三、元類的本質(zhì)


所謂元類,是指一個類所屬的類,我們每創(chuàng)建一個類,系統(tǒng)就會自動幫我們創(chuàng)建好該類所屬的類——即元類。如果你覺得不太好理解,這里就多說兩句:我們常說“在面向?qū)ο缶幊汤?,萬事萬物皆對象”,因此在OC里對象其實分為實例對象、類對象、元類對象三類,我們開發(fā)中經(jīng)常說的“對象”其實是指狹義的對象——實例對象,知道了這一點就好理解了,實例對象有它所屬的類——即一個類對象,類對象也有它所屬的類——即一個元類對象,元類對象也有它所屬的類——即基類的元類對象。

其實元類和類的本質(zhì)都是objc_class結(jié)構(gòu)體,只不過它們的用途不一樣,類的methods成員變量里存儲著該類所有的實例方法信息,而元類的methods成員變量里存儲著該類所有的類方法信息。


四、分類的本質(zhì)


1、分類是什么,我們一般用分類來做什么

分類是OC的一個高級特性,我們一般用它來給系統(tǒng)的類或三方庫的類擴展方法、屬性和協(xié)議,或者把一個類不同的功能分散到不同的模塊里去實現(xiàn)。

舉個簡單例子:

比如我們給NSObject類擴展一個test方法。

-----------NSObject+INETest.h-----------

#import <Foundation/Foundation.h>

@interface NSObject (INETest)

- (void)ine_test;

@end


-----------NSObject+INETest.m-----------

#import "NSObject+INETest.h"

@implementation NSObject (INETest)

- (void)ine_test {
    
    NSLog(@"%s", __func__);
}

@end

比如我們有一個INEPerson類,保持它的主體,然后把它“吃”、“喝”的功能分散到不同的模塊里去實現(xiàn)。

-----------INEPerson.h-----------

#import <Foundation/Foundation.h>

@interface INEPerson : NSObject

@property (nonatomic, assign) NSInteger age;

@end


-----------INEPerson.m-----------

#import "INEPerson.h"

@implementation INEPerson

@end
-----------INEPerson+INEEat.h-----------

#import "INEPerson.h"

@interface INEPerson (INEEat)

- (void)ine_eat;

@end


-----------INEPerson+INEEat.m-----------

#import "INEPerson+INEEat.h"

@implementation INEPerson (INEEat)

- (void)ine_eat {
    
    NSLog(@"%s", __func__);
}

@end
-----------INEPerson+INEDrink.h-----------

#import "INEPerson.h"

@interface INEPerson (INEDrink)

- (void)ine_drink;

@end


-----------INEPerson+INEDrink.m-----------

#import "INEPerson+INEDrink.h"

@implementation INEPerson (INEDrink)

- (void)ine_drink {
    
    NSLog(@"%s", __func__);
}

@end
-----------ViewController.m-----------

#import "INEPerson.h"
#import "INEPerson+INEEat.h"
#import "INEPerson+INEDrink.h"

- (void)viewDidLoad {
    [super viewDidLoad];
    
    INEPerson *person = [[INEPerson alloc] init];
    [person ine_eat];// INEPerson (INEEat) eat
    [person ine_drink];// INEPerson (INEDrink) drink
}

分類和延展的區(qū)別:

  • 分類一般用來給系統(tǒng)的類或三方庫的類擴展方法、屬性和協(xié)議,或者把一個類不同的功能分散到不同的模塊里去實現(xiàn);而延展一般用來給我們自定義的類添加私有屬性。
  • 分類的數(shù)據(jù)不是在編譯時就合并到類里面的,而是在運行時;而延展的數(shù)據(jù)是在編譯時就合并到類里面的。

2、分類的本質(zhì)

通過查看Runtime的源碼(objc-runtime-new.h文件),我們得到分類的定義如下:(偽代碼)

struct category_t {
    const char *name; // 該分類所屬的類的名字
    struct classref *cls; // 指向該分類所屬的類
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};

typedef struct category_t *Category;

可見分類的本質(zhì)是一個category_t類型的結(jié)構(gòu)體,該結(jié)構(gòu)體內(nèi)部有若干個成員變量,其中有幾個是我們重點關(guān)注的:

  • classMethods:該分類為類擴展的類方法列表;
  • instanceMethods:該分類為類擴展的實例方法列表;
  • instanceProperties:該分類為類擴展的屬性列表;
  • protocols:該分類為類擴展的協(xié)議列表。

注意分類的本質(zhì)里沒有“該分類為類擴展的成員變量列表”喔,這在解釋“為什么分類不能給類擴展成員變量”時又是一個很好的證據(jù)。

3、分類的實現(xiàn)原理

我們知道一個類所有的實例方法都存儲在類里面,所有的類方法都存儲在元類里面,而對象調(diào)用方法的流程就是根據(jù)isa指針先找到相應(yīng)的類或元類,然后在類或元類里再找到相應(yīng)的方法來調(diào)用,那person對象是怎么找到分類里的ine_eatine_drink方法來調(diào)用的呢?

現(xiàn)在我們可以大膽猜測,因為對象內(nèi)部只有一個isa指針,指向它所屬的類,所以不可能再有一套類似的方法查找機制讓它專門去分類里面查找方法,難道系統(tǒng)會把分類里的方法合并到類里面去?如果會合并的話,那是編譯時合并的,還是運行時合并的?很簡單,我們只需要看看編譯后類里面是否已經(jīng)包含了分類的方法就行。

先給出結(jié)論:系統(tǒng)不是在編譯時讓編譯器把分類的數(shù)據(jù)合并到類、元類里面的,而是在運行時利用Runtime動態(tài)把分類的數(shù)據(jù)合并到類、元類里面的,而且分類的數(shù)據(jù)還放在類本身數(shù)據(jù)的前面,越晚編譯的分類越在前面,所以如果分類里面有和類里面同名的方法,會優(yōu)先調(diào)用分類里面的方法,如果多個分類里面有同名的方法,會優(yōu)先調(diào)用后編譯分類里面的方法,我們可以去Compile Sources里控制分類編譯的順序。

  • 系統(tǒng)不是在編譯時讓編譯器把分類的數(shù)據(jù)合并到類、元類里面的

接著上面INEPerson類的例子,我們用clang編譯器把INEPerson.m文件轉(zhuǎn)換成C/C++代碼,以便窺探編譯后INEPerson類里面是否已經(jīng)包含了分類的方法。

struct objc_class OBJC_CLASS_$_INEPerson = {
    0, // &OBJC_METACLASS_$_INEPerson,
    0, // &OBJC_CLASS_$_NSObject,
    0, // (void *)&_objc_empty_cache,
    
    // 可讀可寫的
    ["age", "setAge:"], // 所有的實例方法
    ["age"], // 所有的屬性
    [], // 所有遵循的協(xié)議
    
    // 只讀的
    "INEPerson", // 類名
    ["_age"], // 所有的成員變量
    16, // 實例對象的實際大小
};

可見經(jīng)過編譯后,INEPerson類里面的數(shù)據(jù)還是它本身擁有的那些數(shù)據(jù),并沒有分類的方法,這就表明系統(tǒng)不是在編譯時讓編譯器把分類的數(shù)據(jù)合并到類、元類里面的。

  • 而是在運行時利用Runtime動態(tài)把分類的數(shù)據(jù)合并到類、元類里面的

既然系統(tǒng)不是在編譯時就把分類的數(shù)據(jù)合并到類里面的,那就只能是在運行時了,接下來我們就找找運行時(Runtime)的相關(guān)源碼(objc-runtime-new.mm文件),看看系統(tǒng)到底是怎么把分類合并到類里面的:

運行時,系統(tǒng)讀取鏡像階段,會讀取所有的類,并且如果發(fā)現(xiàn)有分類,也會讀取所有的分類,然后遍歷所有的分類,根據(jù)分類的cls指針找到它所屬的類,重新組織一下這個類的內(nèi)部結(jié)構(gòu)——即合并分類的數(shù)據(jù)。

// 系統(tǒng)讀取鏡像
void _read_images()
{
    // 讀取所有的類
    // ...

    // 發(fā)現(xiàn)有分類
    // 讀取所有的分類
    category_t **catlist = _getObjc2CategoryList(hi, &count);
    // 遍歷所有的分類
    for (i = 0; i < count; i++) {
        // 讀取某一個分類
        category_t *cat = catlist[I];
        
        // 根據(jù)分類的cls指針找到它所屬的類
        Class cls = cat->cls;
        // 重新組織一下這個類的內(nèi)部結(jié)構(gòu)——即合并分類的數(shù)據(jù)
        remethodizeClass(cls);
    }
}

那具體怎么個合并法呢?系統(tǒng)會去獲取這個類所有的分類,然后倒序遍歷這所有的分類,把每個分類里面的實例方法列表拿出來,存進一個二維數(shù)組里(因為是倒序遍歷分類的,所以越晚編譯的分類的實例方法列表反而越會放在二維數(shù)組的前面),然后再把這個二維數(shù)組內(nèi)所有一維數(shù)組的首地址復制進methods成員變量指向的那塊內(nèi)存里(注意這個存儲過程會把類本身的實例方法列表挪到最后——即高內(nèi)存地址上,而把分類的實例方法列表存在前面)。

// 重新組織一下這個類的內(nèi)部結(jié)構(gòu)——即合并分類的數(shù)據(jù)
static void remethodizeClass(Class cls)
{
    // 系統(tǒng)會去獲取這個類所有的分類(沒有合并過的)
    category_list *cats = unattachedCategoriesForClass(cls);
    // 把所有分類的數(shù)據(jù)合并到類里面
    attachCategories(cls, cats);
    free(cats);
}

/**
 * 把所有分類的數(shù)據(jù)合并到類里面
 *
 * @param cls 當前類
 * @param cats 當前類所有的分類
 */
static void attachCategories(Class cls, category_list *cats)
{
#pragma mark - 倒序遍歷所有的分類,把每個分類里面的實例方法列表拿出來,存進一個二維數(shù)組里
    /*
     創(chuàng)建一個二維數(shù)組,用來存放每個分類里的實例方法列表,最終結(jié)果類似下面這樣:
     [
        [instanceMethod1, instanceMethod2, ...] --> 分類1所有實例方法
        [instanceMethod1, instanceMethod2, ...] --> 分類2所有實例方法
        ...
     ]
     */
    method_list_t **mlists = (method_list_t **) malloc(cats->count * sizeof(*mlists));
    
    // 屬性
    property_list_t **proplists = (property_list_t **) malloc(cats->count * sizeof(*proplists));
    
    // 協(xié)議
    protocol_list_t **protolists = (protocol_list_t **) malloc(cats->count * sizeof(*protolists));
    
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    // 注意:這里是倒序遍歷所有的分類
    while (i--) {
        // 獲取一個分類
        auto cat = cats[I];
        
        // 獲取分類的實例方法列表,存進二維數(shù)組
        method_list_t *mlist = cat->methods;
        mlists[mcount++] = mlist;
        
        // 屬性
        protocol_list_t *protolist = cat->protocols;
        protolists[protocount++] = protolist;
        
        // 協(xié)議
        property_list_t *proplist = cat->properties;
        proplists[propcount++] = proplist;
    }
    
    
#pragma mark - 把這個二維數(shù)組內(nèi)所有一維數(shù)組的首地址存進methods成員變量所指向的那塊內(nèi)存空間里
    
    // 獲取當前類的數(shù)據(jù)(包括實例方法列表、屬性列表、協(xié)議列表等)
    auto classData = cls->data();
    
    // 給當前類的實例方法列表附加所有分類的實例方法列表
    classData->methods.attachLists(mlists, mcount);
    free(mlists);
    
    // 屬性
    classData->properties.attachLists(proplists, propcount);
    free(proplists);
    
    // 協(xié)議
    classData->protocols.attachLists(protolists, protocount);
    free(protolists);
}

/**
 * 給當前類的實例方法列表附加所有分類的實例方法列表
 *
 * @param addedLists 所有分類的實例方法列表(就是那個二維數(shù)組,但其實是那個二維數(shù)組的首地址)
 * @param addedCount 分類的個數(shù)
 */
void attachLists(List* const * addedLists, unsigned int addedCount) {
#pragma mark - 重新為類的methods成員變量分配內(nèi)存
    // 獲取類原來methods成員變量的元素個數(shù)(注意:一個類的methods成員變量是一個數(shù)組,存儲著若干個指針,指向相應(yīng)的方法列表,而不是直接就是個方法列表存儲方法)
    unsigned int oldCount = array()->count;
    // 加上分類的個數(shù),得到新的methods成員變量該有多少個元素
    unsigned int newCount = oldCount + addedCount;
    // 重新為methods成員變量所指向的數(shù)組分配內(nèi)存,一個指針占8個字節(jié)
    setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
    array()->count = newCount;
    
    
#pragma mark - 為類的methods成員變量重新分配完內(nèi)存后,對其內(nèi)存數(shù)據(jù)進行移動和復制操作
    //
    /*
     內(nèi)存復制:
     memmove(dst, src, len),從src所指向的內(nèi)存空間復制len個字節(jié)的數(shù)據(jù)到dst所指向的內(nèi)存空間,內(nèi)部處理了內(nèi)存覆蓋。
     memcpy(dst, src, n),從src所指向的內(nèi)存空間復制n個字節(jié)的數(shù)據(jù)到dst所指向的內(nèi)存空間,內(nèi)部沒處理內(nèi)存覆蓋。
     */
    // 把類原來的實例方法列表復制到最后面(但其實是把類原來的實例方法列表,在methods成員變量里對應(yīng)的那個指針————原來的實例方法列表的首地址————復制到最后面了)
    memmove(array()->lists + addedCount, array()->lists,
            oldCount * sizeof(array()->lists[0]));
    // 把所有分類的實例方法列表放在前面(同理,其實是把所有分類的的實例方法列表的首地址復制到前面了,因為methods成員變量里存放的是指針————即實例方法列表的地址,不過這里二維數(shù)組的內(nèi)存拷貝會拷貝它里面所有一維數(shù)組的首地址,而不僅僅這個二維數(shù)組的首地址)
    memcpy(array()->lists, addedLists,
           addedCount * sizeof(array()->lists[0]));
}

這樣就把所有分類的實例方法列表全都合并到類里面去了,最終類的方法列表結(jié)構(gòu)如下:

以上我們只是說明了分類為類擴展實例方法的底層實現(xiàn),至于分類為類擴展類方法、屬性、協(xié)議是同理的。

4、分類的+load方法和+initialize方法

調(diào)用時機 調(diào)用方式 調(diào)用順序
+load方法 +load方法是系統(tǒng)把類和分類載入內(nèi)存時調(diào)用的 +load方法是通過內(nèi)存地址直接調(diào)用的,所以分類的+load方法不會覆蓋類的+load方法,也就是說如果類和分類里面都實現(xiàn)了+load方法,那么它們都會被調(diào)用 會先調(diào)用所有類的+load方法,然后再調(diào)用所有分類的+load方法
+initialize方法 +initialize方法是類初始化的時候調(diào)用的 +initialize方法是通過消息發(fā)送機制調(diào)用的,所以分類的+initialize方法會覆蓋類的+initialize方法,也就是說如果類和分類里面都實現(xiàn)了+initialize方法,那么只有分類里面的會被調(diào)用 會優(yōu)先調(diào)用分類的+initialize方法

蘋果提供類、分類的+load方法和+initialize方法,其實就是給我們開發(fā)者暴露兩個接口,讓我們根據(jù)這倆方法的特點來合理使用。比如我們想在某個類被載入內(nèi)存時做一些事情,就可以在+load方法里做操作,想在某個類初始化時做一些事情,就可以在+initialize方法里做操作。

4.1 +load方法
  • 調(diào)用時機

假設(shè)有一個INEPerson類,并且為它創(chuàng)建了兩個分類INEEatINEDrink

-----------INEPerson.m-----------

#import "INEPerson.h"

@implementation INEPerson

+ (void)load {
    
    NSLog(@"INEPerson +load");
}

@end


-----------INEPerson+INEEat.m-----------

#import "INEPerson+INEEat.h"

@implementation INEPerson (INEEat)

+ (void)load {
    
    NSLog(@"INEPerson (INEEat) +load");
}

@end


-----------INEPerson+INEDrink.m-----------

#import "INEPerson+INEDrink.h"

@implementation INEPerson (INEDrink)

+ (void)load {
    
    NSLog(@"INEPerson (INEDrink) +load");
}

@end

我們什么都不做,不使用Person類,甚至連它的頭文件也不導入。

-----------ViewController.m-----------

#import "ViewController.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
}

@end

直接運行程序,發(fā)現(xiàn)控制臺打印如下:

INEPerson +load
INEPerson (INEEat) +load
INEPerson (INEDrink) +load

于是我們就可以得出結(jié)論:+load方法是系統(tǒng)把類和分類載入內(nèi)存時調(diào)用的,它和我們代碼里使用不使用這個類和分類無關(guān)。并且因為+load方法只會在類和分類被載入內(nèi)存時調(diào)用,所以每個類和分類的+load方法在程序的整個生命周期中肯定會被調(diào)用且只調(diào)用一次。

  • 調(diào)用方式

這里先回想一下,上面第三部分我們說過分類的方法列表會合并到類本身的方法列表里,并且分類的方法列表還會在類本身方法列表的前面,因此分類的方法會覆蓋掉類里同名的方法。

但不知道你注意沒有,上面第1小節(jié)的例子,控制臺打印了三個東西,也就是說分類的+load方法和類的+load方法都走了,這很奇怪啊,按理說應(yīng)該只走其中某一個分類的+load方法才對啊,怎么會三個都走呢?也就是說為什么分類的+load方法沒有覆蓋掉類的+load方法?

接下來我們就找找運行時(Runtime)的相關(guān)源碼(objc-runtime-new.mm文件),看看能不能得到答案:(偽代碼)

// 系統(tǒng)加載鏡像
void load_images()
{
    call_load_methods();
}

// 調(diào)用+load方法
void call_load_methods()
{
    // 1、首先調(diào)用所有類的+load方法
    call_class_loads();

    // 2、然后調(diào)用所有分類的+load方法
    call_category_loads();
}

// 調(diào)用所有類的+load方法
static void call_class_loads()
{
    // 獲取到所有的類
    struct loadable_class *classes = loadable_classes;
    
    for (int i = 0; i < loadable_classes_used; i++) {
        
        // 獲取到某個類
        Class cls = classes[i].cls;
        // 獲取到某個類+load方法的地址
        load_method_t load_method = (load_method_t)classes[i].method;
    
        // 直接調(diào)用該類的+load方法
        (*load_method)(cls, SEL_load);
    }
}

// 調(diào)用所有分類的+load方法
static void call_category_loads()
{
    // 獲取到所有的分類
    struct loadable_category *cats = loadable_categories;
    
    for (i = 0; i < loadable_categories_used; i++) {
        
        // 獲取到某個分類
        Category cat = cats[i].cat;
        // 獲取到某個分類+load方法的地址
        load_method_t load_method = (load_method_t)cats[i].method;

        // 直接調(diào)用該分類的+load方法
        (*load_method)(cls, SEL_load);
    }
}

可見+load方法是通過內(nèi)存地址直接調(diào)用的,而不像普通方法那樣走消息發(fā)送機制。因此就解釋了我們留下的疑惑,雖然說分類的方法列表在類本身方法列表的前面,但是對+load方法根本不起作用,人家不走你那一套,所以分類的+load方法不會覆蓋類的+load方法。

  • 調(diào)用順序

這里就直接給出結(jié)論了,感興趣的話,可以像第2小節(jié)那樣去看源碼(核心代碼就集中在上面那幾個方法里)并敲代碼驗證驗證。

會先調(diào)用所有類的+load方法,先編譯的類先調(diào)用;如果存在繼承關(guān)系,那么在調(diào)用子類的+load方法之前會先去調(diào)用父類的+load方法。

然后再調(diào)用所有分類的+load方法,先編譯的分類先調(diào)用。

4.2 +initialize方法
  • 調(diào)用時機

假設(shè)有一個INEPerson類和一個繼承自INEPerson類的INEStudent類,并且為INEStudent類創(chuàng)建了兩個分類INEEatINEDrink。

-----------INEPerson.m-----------

#import "INEPerson.h"

@implementation INEPerson

+ (void)initialize {
    
    NSLog(@"INEPerson +initialize");
}

@end


-----------INEStudent.m-----------

#import "INEStudent.h"

@implementation INEStudent

+ (void)initialize {
    
    NSLog(@"INEStudent +initialize");
}

@end


-----------INEStudent+INEEat.m-----------

#import "INEStudent+INEEat.h"

@implementation INEStudent (INEEat)

+ (void)initialize {
    
    NSLog(@"INEStudent (INEEat) +initialize");
}

@end


-----------INEStudent+INEDrink.m-----------

#import "INEStudent+INEDrink.h"

@implementation INEStudent (INEDrink)

+ (void)initialize {
    
    NSLog(@"INEStudent (INEDrink) +initialize");
}

@end

我們什么都不做,直接運行程序,發(fā)現(xiàn)控制臺什么都沒打印。

-----------ViewController.m-----------

#import "ViewController.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
}

@end

此時我們調(diào)用一下Student類的+alloc方法。

-----------ViewController.m-----------

#import "ViewController.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [INEStudent alloc];
}

@end

運行程序,發(fā)現(xiàn)控制臺打印如下:

INEPerson +initialize
INEStudent (INEDrink) +initialize

于是我們就可以得出結(jié)論:+initialize方法是類初始化的時候調(diào)用的,所以嚴格地來講,我們不能說“+initialize方法是第一次使用類的時候調(diào)用的”,你看上面例子中我們根本沒使用INEPerson類嘛,但它的+initialize方法照樣被調(diào)用了。如果我們壓根兒不使用這個類,它的+initialize方法被調(diào)用0次,但是我們不能說一個類的+initialize方法最多被調(diào)用1次,因為+initialize方法是通過消息發(fā)送機制來調(diào)用的,如果好幾個子類都繼承自某一個類,而這些子類都沒有實現(xiàn)自己的+initialize方法,那就都會去調(diào)用這個父類的+initialize方法,這不就是調(diào)用N次了嘛。

  • 調(diào)用方式

上面第1小節(jié)的例子,控制臺打印了一個:

INEStudent (INEDrink) +initialize

這就明顯表明:+initialize方法的調(diào)用方式不同于+load方法,它是通過消息發(fā)送機制調(diào)用的,所以才會只走分類里面的 +initialize方法,也就是說分類的+initialize方法會覆蓋類的+initialize方法。

但有一點很奇怪,因為控制臺還打印了:

INEPerson +initialize

這是父類的+initialize方法呀!既然+initialize方法是通過消息發(fā)送機制調(diào)用的,那它在自己類的內(nèi)部找到某個方法后,就不應(yīng)該再調(diào)用父類里面的方法了呀,怎么回事?

接下來我們就找找運行時(Runtime)的相關(guān)源碼(objc-runtime-new.mm文件),看看能不能得到答案:(偽代碼)

// 查找方法的實現(xiàn):類接收到消息后,會去查找這個消息的實現(xiàn)并調(diào)用,那我們就從查找這個消息的實現(xiàn)下手吧,前面的源碼沒有相關(guān)信息
IMP lookUpImpOrForward(Class cls, SEL sel)
{
    // 在查找方法的過程中,如果發(fā)現(xiàn)這個類沒被初始化過
    if (!cls->isInitialized()) {
        // 則初始化這個類
        initializeNonMetaClass(cls);
    }
}

// 初始化一個類
void initializeNonMetaClass(Class cls)
{
    // 在初始化一個類的過程中
    Class supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {// 如果發(fā)現(xiàn)這個類的父類沒被初始化過
        // 則遞歸,一層一層地先初始化父類,直到NSObject,直到nil
        initializeNonMetaClass(supercls);
        
        // 一層一層初始化完之后,才會一層一層自上而下地調(diào)用各個類的+initialize方法
        callInitialize(cls);
    } else {// 如果發(fā)現(xiàn)這個類的父類被初始化過了
        // 則直接初始化自己
        initializeNonMetaClass(cls);
        // 并調(diào)用自己的+initialize方法,
        // 如果自己沒有實現(xiàn),則會去找父類的+initialize方法調(diào)用。(因為+initialize方法是通過消息發(fā)送機制調(diào)用的嘛)
        callInitialize(cls);
    }
}

void callInitialize(Class cls)
{
    // +initialize方法確實是通過消息發(fā)送機制調(diào)用的
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
}

可見系統(tǒng)在調(diào)用一個類的+initialize方法之前,首先會看看它的父類初始化了沒有,如果沒有初始化,則初始化它的父類并調(diào)用它父類的+initialize方法,然后再初始化自己并調(diào)用自己的+initialize方法;如果它的父類初始化了,則直接初始化自己并調(diào)用自己的+initialize方法,如果自己沒有實現(xiàn),則會去找父類的+initialize方法調(diào)用。

  • 調(diào)用順序

這里就直接給出結(jié)論了。

系統(tǒng)在調(diào)用一個類的+initialize方法之前,首先會看看它的父類初始化了沒有,如果沒有初始化,則初始化它的父類并調(diào)用它父類的+initialize方法,然后再初始化自己并調(diào)用自己的+initialize方法;如果它的父類初始化了,則直接初始化自己并調(diào)用自己的+initialize方法,如果自己沒有實現(xiàn),則會去找父類的+initialize方法調(diào)用。

如果分類里也實現(xiàn)了+initialize方法,會優(yōu)先調(diào)用分類的。


temp、行文至此,我們舉個例子串一下上面的內(nèi)容


定義一個INEPerson類,并為它創(chuàng)建一個分類INEDrink,然后創(chuàng)建兩個person對象。

-----------INEPerson.h-----------

#import <Foundation/Foundation.h>

@interface INEPerson : NSObject <NSCopying>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, assign) NSInteger age;

- (void)eat;
+ (void)sleep;

@end


-----------INEPerson.m-----------

#import "INEPerson.h"

@implementation INEPerson

- (void)eat {
    NSLog(@"對象方法:吃");
}

+ (void)sleep {
    NSLog(@"類方法:睡");
}

- (id)copyWithZone:(nullable NSZone *)zone {
    // 淺拷貝一下
    return self;
}

@end
-----------INEPerson+INEDrink.h-----------

#import "INEPerson.h"

@interface INEPerson (INEDrink)

- (void)ine_drinkWater;
+ (void)ine_drinkTea;

@end


-----------INEPerson+INEDrink.m-----------

#import "INEPerson+INEDrink.h"

@implementation INEPerson (INEDrink)

- (void)ine_drinkWater {
    
    NSLog(@"%s", __func__);
}

+ (void)ine_drinkTea {
    
    NSLog(@"%s", __func__);
}

@end
-----------ViewController.m-----------

#import "ViewController.h"
#import "INEPerson.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    INEPerson *person1 = [[INEPerson alloc] init];
    person1.name = @"張三";
    person1.sex = @"男";
    person1.age = 19;
    [person1 eat];
    
    INEPerson *person2 = [[INEPerson alloc] init];
    person2.name = @"李四";
    person2.sex = @"女";
    person2.age = 18;
    
    [INEPerson sleep];
}

@end

當我們啟動App時,系統(tǒng)就會把INEPerson類、INEPerson類的元類還有INEPerson類的分類加載到內(nèi)存中,然后把分類的數(shù)據(jù)合并到類和元類里,并且這些類會被存儲到代碼區(qū),因為類只要一份就夠了嘛 + 類還得能在項目的任何地方都能訪問到,直到殺死App,這些類的內(nèi)存才會被釋放。那么INEPerson類在代碼區(qū)的那塊內(nèi)存里存儲著什么呢?isa指針存儲著一個地址,指向INEPerson類的元類,這個地址就是INEPerson類的元類在代碼區(qū)的內(nèi)存地址;superClass指針存儲著一個地址,指向NSObject類,這個地址就是NSObject類在代碼區(qū)的內(nèi)存地址;methods成員變量存儲著ine_drinkWatereat這兩個實例方法的信息,properties成員變量存儲著namesex、age這些屬性的信息,protocols成員變量存儲著NSCopying協(xié)議的信息,ivars成員變量存儲著_name_sex_age這些成員變量的信息,cache緩存著eat方法的信息。INEPerson類的元類在代碼區(qū)的那塊內(nèi)存里存儲著什么呢?isa指針存儲著一個地址,指向基類NSObject類的元類,這個地址就是基類NSObject類的元類在代碼區(qū)的內(nèi)存地址;superClass指針存儲著一個地址,同樣指向基類NSObject類的元類,這個地址就是基類NSObject類的元類在代碼區(qū)的內(nèi)存地址;methods成員變量存儲著ine_drinkTeasleep這個類方法的信息,cache緩存著sleep方法的信息。

當我們alloc init一個person對象時,就會在堆區(qū)分配一塊內(nèi)存,直到?jīng)]有強引用引用這個對象了,這塊內(nèi)存才會被釋放。那么person對象在堆區(qū)的那塊內(nèi)存里存儲著什么呢?isa指針存儲著一個地址,指向INEPerson類,這個地址就是INEPerson類在靜態(tài)全局區(qū)的內(nèi)存地址;person1對象接下來會存儲_name成員變量的值"張三",當然它存儲的也是一個常量區(qū)的地址,指向"張三"這個字符串常量,還有_sex成員變量的值"男",當然它存儲的也是一個常量區(qū)的地址,指向"男"這個字符串常量,還有_age成員變量的值“19”,當然“19”就是直接存儲了,因為它是個立即數(shù);person2對象接下來則會存儲_name成員變量的值"李四",_sex成員變量的值"女",_age成員變量的值“18”,注意對象的內(nèi)存里存儲的是成員變量的值,而類的內(nèi)存里存儲的是成員變量的信息——比如INEPerson類有一個成員變量是“_name”,它的類型是NSString這樣。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

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

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,685評論 1 32
  • 1.理解NSObject和元類 1.1 在OC中的對象和類是什么 對象是在objc.h中定義的 類是在runtim...
    HWenj閱讀 986評論 0 3
  • 本文分為4個部分 1.介紹OC和C語言之間的轉(zhuǎn)換 2.介紹運行時和相關(guān)術(shù)語 3.介紹消息發(fā)送機制已及怎樣找到函數(shù)實...
    一片楓葉隨風舞閱讀 399評論 0 1
  • Class的結(jié)構(gòu) Runtime-Demo 通過上一章中對isa本質(zhì)結(jié)構(gòu)有了新的認識,今天來回顧Class的結(jié)構(gòu),...
    二斤寂寞閱讀 601評論 0 2
  • 參加了一個充滿小驚喜的活動,一月的每一天都有小小的不同。雖然只是小小的不同,也讓枯燥的生活變得那么一點點的不一...
    YE葉囧囧閱讀 194評論 0 0

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