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

目錄

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

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


一、對(duì)象的本質(zhì)


1、OC1.0

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

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

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

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

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

2、OC2.0

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

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

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

// 共用體isa_t
//
// 共用體也是C語言的一種數(shù)據(jù)類型,和結(jié)構(gòu)體差不多,
// 都可以定義很多的成員變量,但兩者的主要區(qū)別就在于內(nèi)存的使用。
//
// 一個(gè)結(jié)構(gòu)體占用的內(nèi)存等于它所有成員變量占用內(nèi)存之和,而且要遵守內(nèi)存對(duì)齊規(guī)則,而一個(gè)共用體占用的內(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個(gè)字節(jié),64位
    struct { // 其實(shí)所有的數(shù)據(jù)都存儲(chǔ)在成員變量bits里面,因?yàn)橥饨缰辉L問它,而這個(gè)結(jié)構(gòu)體則僅僅是用位域來增加代碼的可讀性,讓我們看到bits里面相應(yīng)的位上存儲(chǔ)著什么數(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; // 當(dāng)前對(duì)象所屬類的地址信息
        unsigned long magic             : 6;
        unsigned long weakly_referenced : 1; // 當(dāng)前對(duì)象是否有弱引用
        unsigned long deallocating      : 1;
        unsigned long has_sidetable_rc  : 1; // 引用計(jì)數(shù)表里是否有當(dāng)前對(duì)象的引用計(jì)數(shù)
        unsigned long extra_rc          : 19; // 對(duì)象的引用計(jì)數(shù) - 1,存不下了就會(huì)放到引用計(jì)數(shù)表里
# endif
    };
};

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

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

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

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

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

2、OC2.0

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

struct objc_class : objc_object {
//    isa_t isa; // objc_class繼承自objc_object,所以不考慮內(nèi)存對(duì)齊的前提下,可以直接把isa成員變量搬過來
    Class superclass;
    
    class_data_bits_t bits; // 存儲(chǔ)著該類的具體信息,按位與掩碼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; // 存儲(chǔ)著該類所有的實(shí)例方法信息,包括分類的
    property_array_t properties; // 存儲(chǔ)著該類所有的屬性信息,包括分類的
    protocol_array_t protocols; // 存儲(chǔ)著該類所有遵守的協(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; // 存儲(chǔ)著該類本身的實(shí)例方法信息
    protocol_list_t * baseProtocols; // 存儲(chǔ)著該類本身遵守的協(xié)議信息
    const ivar_list_t * ivars; // 存儲(chǔ)著該類本身的成員變量信息

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

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

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


三、元類的本質(zhì)


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

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


四、分類的本質(zhì)


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

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

舉個(gè)簡(jiǎn)單例子:

比如我們給NSObject類擴(kuò)展一個(gè)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

比如我們有一個(gè)INEPerson類,保持它的主體,然后把它“吃”、“喝”的功能分散到不同的模塊里去實(shí)現(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)的類或三方庫的類擴(kuò)展方法、屬性和協(xié)議,或者把一個(gè)類不同的功能分散到不同的模塊里去實(shí)現(xiàn);而延展一般用來給我們自定義的類添加私有屬性。
  • 分類的數(shù)據(jù)不是在編譯時(shí)就合并到類里面的,而是在運(yùn)行時(shí);而延展的數(shù)據(jù)是在編譯時(shí)就合并到類里面的。

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ì)是一個(gè)category_t類型的結(jié)構(gòu)體,該結(jié)構(gòu)體內(nèi)部有若干個(gè)成員變量,其中有幾個(gè)是我們重點(diǎn)關(guān)注的:

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

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

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

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

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

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

  • 系統(tǒng)不是在編譯時(shí)讓編譯器把分類的數(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:"], // 所有的實(shí)例方法
    ["age"], // 所有的屬性
    [], // 所有遵循的協(xié)議
    
    // 只讀的
    "INEPerson", // 類名
    ["_age"], // 所有的成員變量
    16, // 實(shí)例對(duì)象的實(shí)際大小
};

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

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

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

運(yùn)行時(shí),系統(tǒng)讀取鏡像階段,會(huì)讀取所有的類,并且如果發(fā)現(xiàn)有分類,也會(huì)讀取所有的分類,然后遍歷所有的分類,根據(jù)分類的cls指針找到它所屬的類,重新組織一下這個(gè)類的內(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++) {
        // 讀取某一個(gè)分類
        category_t *cat = catlist[I];
        
        // 根據(jù)分類的cls指針找到它所屬的類
        Class cls = cat->cls;
        // 重新組織一下這個(gè)類的內(nèi)部結(jié)構(gòu)——即合并分類的數(shù)據(jù)
        remethodizeClass(cls);
    }
}

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

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

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

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

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

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

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

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

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

4.1 +load方法
  • 調(diào)用時(shí)機(jī)

假設(shè)有一個(gè)INEPerson類,并且為它創(chuàng)建了兩個(gè)分類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類,甚至連它的頭文件也不導(dǎo)入。

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

#import "ViewController.h"

@implementation ViewController

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

@end

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

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

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

  • 調(diào)用方式

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

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

接下來我們就找找運(yùn)行時(shí)(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++) {
        
        // 獲取到某個(gè)類
        Class cls = classes[i].cls;
        // 獲取到某個(gè)類+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++) {
        
        // 獲取到某個(gè)分類
        Category cat = cats[i].cat;
        // 獲取到某個(gè)分類+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ā)送機(jī)制。因此就解釋了我們留下的疑惑,雖然說分類的方法列表在類本身方法列表的前面,但是對(duì)+load方法根本不起作用,人家不走你那一套,所以分類的+load方法不會(huì)覆蓋類的+load方法。

  • 調(diào)用順序

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

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

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

4.2 +initialize方法
  • 調(diào)用時(shí)機(jī)

假設(shè)有一個(gè)INEPerson類和一個(gè)繼承自INEPerson類的INEStudent類,并且為INEStudent類創(chuàng)建了兩個(gè)分類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

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

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

#import "ViewController.h"

@implementation ViewController

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

@end

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

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

#import "ViewController.h"

@implementation ViewController

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

@end

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

INEPerson +initialize
INEStudent (INEDrink) +initialize

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

  • 調(diào)用方式

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

INEStudent (INEDrink) +initialize

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

但有一點(diǎn)很奇怪,因?yàn)榭刂婆_(tái)還打印了:

INEPerson +initialize

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

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

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

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

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

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

  • 調(diào)用順序

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

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

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


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


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

-----------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(@"對(duì)象方法:吃");
}

+ (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

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

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

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

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

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

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