
目錄
弄明白對(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)套了兩層rw和ro,我們先說一下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_eat和ine_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è)分類INEEat和INEDrink。
-----------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è)分類INEEat和INEDrink。
-----------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_drinkWater和eat這兩個(gè)實(shí)例方法的信息,properties成員變量存儲(chǔ)著name、sex、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_drinkTea和sleep這個(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這樣。