OC- +load
和 +initialize
方法調(diào)用原理
-
load
方法什么時(shí)候調(diào)用的? -
load
方法和initialize
方法的區(qū)別是什么?他們?cè)?code>category中的調(diào)用順序.
Objective-C為我們提供了兩種方法去運(yùn)行對(duì)類進(jìn)行相關(guān)設(shè)置的代碼账胧。
-
+load
:該方法會(huì)在很早階段(同時(shí)也是比較危險(xiǎn)的階段携取,可能導(dǎo)致崩潰)被調(diào)用,一旦某個(gè)類被Runtime加載处坪,該類的+load
方法就會(huì)被調(diào)用。我們可以在這個(gè)方法里面寫一些必須要在程序運(yùn)行非常早期階段就需要運(yùn)行的代碼丝蹭。 -
+initialize
:該方法可以比較安全的處理大部分情況下的設(shè)置任務(wù)代碼秤掌,因?yàn)闀?huì)在一個(gè)更加安全的環(huán)境下被調(diào)用。你幾乎可以在這個(gè)方法里面做任何事情再来,除非蒙兰,你的代碼
需要等到外部實(shí)體向這個(gè)類發(fā)消息之后磷瘤,才能運(yùn)行,那么將你的代碼
放在+initialize
方法里面將是不合適的
+load
方法
load方法的調(diào)用時(shí)機(jī)和調(diào)用頻率
首先創(chuàng)建一個(gè)Person
類,和Person+Test1
,Person+Test2
兩個(gè)分類,然后再創(chuàng)建一個(gè)Student
類繼承自Person
類,以及Student
的分類Student+Test1
,重寫他們的+ load
方法,并添加一個(gè) + test
方法:
@interface MJPerson : NSObject
+ (void)test;
@end
#import "MJPerson.h"
@implementation MJPerson
+ (void)load
{
NSLog(@"MJPerson +load");
}
+ (void)test
{
NSLog(@"MJPerson +test");
}
@end
MJPerson+Test1
#import "MJPerson.h"
@interface MJPerson (Test1)
@end
#import "MJPerson+Test1.h"
@implementation MJPerson (Test1)
+ (void)load
{
NSLog(@"MJPerson (Test1) +load");
}
+ (void)test
{
NSLog(@"MJPerson (Test1) +test");
}
@end
MJPerson+Test2
#import "MJPerson.h"
@interface MJPerson (Test2)
@end
#import "MJPerson+Test2.h"
@implementation MJPerson (Test2)
+ (void)load
{
NSLog(@"MJPerson (Test2) +load");
}
+ (void)test
{
NSLog(@"MJPerson (Test2) +test");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
RUN>
============打印輸出============ 2021-04-19 14:06:32.404129+0800 Interview01-load[2599:117222] MJPerson +load 2021-04-19 14:06:32.404641+0800 Interview01-load[2599:117222] MJPerson (Test1) +load 2021-04-19 14:06:32.404692+0800 Interview01-load[2599:117222] MJPerson (Test2) +load
從日志看出搜变,雖然整個(gè)工程都沒有import過
MJPerson
以及它的兩個(gè)分類采缚,但是他們的load
方法還是被調(diào)用了,并且都發(fā)生在main
函數(shù)開始之前挠他,而且+test
并沒有被調(diào)用扳抽。所以該現(xiàn)象間接證明了,load
方法的調(diào)用應(yīng)該和類對(duì)象以及分類的加載有關(guān)殖侵。
void printMethodNamesOfClass(Class cls)
{
unsigned int count;
// 獲得方法數(shù)組
Method *methodList = class_copyMethodList(cls, &count);
// 存儲(chǔ)方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍歷所有的方法
for (int i = 0; i < count; i++) {
// 獲得方法
Method method = methodList[i];
// 獲得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 釋放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
printMethodNamesOfClass(object_getClass([MJPerson class]));
}
return 0;
}
RUN>
============打印輸出============ 2021-04-19 14:13:02.099579+0800 Interview01-load[2668:121765] MJPerson +load 2021-04-19 14:13:02.099997+0800 Interview01-load[2668:121765] MJPerson (Test1) +load 2021-04-19 14:13:02.100038+0800 Interview01-load[2668:121765] MJPerson (Test2) +load 2021-04-19 14:13:02.100215+0800 Interview01-load[2668:121765] MJPerson load, test, load, test, load, test,
從打印結(jié)果可以看出,
load
方法和test
方法都已經(jīng)附加到了本類中.從結(jié)果上可以看到
test
方法的確如我們之前所說,被附加到了本類中并且優(yōu)先調(diào)用,那為什么每個(gè)類中load
方法都會(huì)調(diào)用呢?
接下來通過源碼分析一下(Runtime源碼下載地址)
我們從runtime
源碼中尋找答案,查看源碼步驟如下:
打開objc-os.mm
文件->找到_objc_init()
方法->進(jìn)入load_images
->進(jìn)入call_load_methods()
方法
首先贸呢,進(jìn)入Runtime的初始化文件objc-os.mm
,找到_objc_init
函數(shù)愉耙,該函數(shù)可以看作是Runtime的初始化函數(shù)贮尉。
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
直接看最后句_dyld_objc_notify_register(&map_images, load_images, unmap_image);
其中很明顯,load_images
就是加載鏡像/加載模塊的意思朴沿,應(yīng)該是與我們?cè)掝}相關(guān)的參數(shù)猜谚,點(diǎn)進(jìn)去看看它的實(shí)現(xiàn)
/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
蘋果對(duì)該函數(shù)官方給出的注釋是,處理那些正在進(jìn)行映射的鏡像(images)的+load方法赌渣。該方法的實(shí)現(xiàn)里面魏铅,做了兩件事情:
-
prepare_load_methods
// Discover load methods -- 查找并準(zhǔn)備load方法,以供后面去調(diào)用 -
call_load_methods();
//Call +load methods -- 調(diào)用這些load方法
針對(duì)上面案例日志中出現(xiàn)的現(xiàn)象坚芜,先從結(jié)果出發(fā)览芳,逆向分析,來看看load方法是如何調(diào)用的鸿竖,進(jìn)入call_load_methods();
的實(shí)現(xiàn)
/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.
調(diào)用所有的處理中的class和category的+load方法沧竟;
* Class +load methods are called superclass-first.
class的+load方法會(huì)被先調(diào)用,并且缚忧,一個(gè)調(diào)用一個(gè)class的+load方法前悟泵,會(huì)先對(duì)其父類的+load進(jìn)行調(diào)用
* Category +load methods are not called until after the parent class's +load.
category的+load方法的調(diào)用,會(huì)發(fā)生在所有的class的+load方法完成調(diào)用之后闪水。
*
* This method must be RE-ENTRANT, because a +load could trigger
* more image mapping. In addition, the superclass-first ordering
* must be preserved in the face of re-entrant calls. Therefore,
* only the OUTERMOST call of this function will do anything, and
* that call will handle all loadable classes, even those generated
* while it was running.
*
* The sequence below preserves +load ordering in the face of
* image loading during a +load, and make sure that no
* +load method is forgotten because it was added during
* a +load call.
* Sequence:調(diào)用順序
* 1. Repeatedly call class +loads until there aren't any more
遍歷所有的class對(duì)象糕非,調(diào)用它們的+load方法,知道所有class中的+load都完成了調(diào)用
* 2. Call category +loads ONCE.
調(diào)用所有category中的+load方法
* 3. Run more +loads if:
* (a) there are more classes to load, OR
* (b) there are some potential category +loads that have
* still never been attempted.
* Category +loads are only run once to ensure "parent class first"
* ordering, even if a category +load triggers a new loadable class
* and a new loadable category attached to that class.
*
* Locking: loadMethodLock must be held by the caller
* All other locks must not be held.
**********************************************************************/
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();//先調(diào)用類的load方法
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();//再調(diào)用分類的load方法
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
很明顯球榆,核心邏輯在do-while循環(huán)里面朽肥,循環(huán)中面做了兩件事:
- 首先調(diào)用類對(duì)象的
+load
方法--call_class_loads();
,直到可加載的類的計(jì)數(shù)器減到0 --loadable_classes_used > 0
。 - 然后調(diào)用分類的
+load
方法--call_category_loads();//Call category +loads ONCE
我們進(jìn)入call_class_loads()
方法內(nèi)部:這是對(duì)所有類對(duì)象(class
)的+load
方法的調(diào)用邏輯
/***********************************************************************
* call_class_loads
* Call all pending class +load methods.
* If new classes become loadable, +load is NOT called for them.
*
* Called only by call_load_methods().
**********************************************************************/
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;//首先用局部變量loadable_class保存loadable_classes列表
int used = loadable_classes_used;//在用局部變量used保存loadable_classes_used
loadable_classes = nil;//將loadable_classes置空
loadable_classes_allocated = 0;//將loadable_classes_allocated清零
loadable_classes_used = 0;//將loadable_classes_used清零
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {//遍歷classes列表
Class cls = classes[i].cls;//從列表成員里面獲得cls
load_method_t load_method = (load_method_t)classes[i].method;//從列表成員獲取對(duì)應(yīng)cls的+load 的IMP(方法實(shí)現(xiàn))
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);//這里就是對(duì)+load方法的調(diào)用持钉,注意哦衡招,這是直接的函數(shù)調(diào)用,不是消息機(jī)制那種哦每强,這里跟類的方法列表什么沒關(guān)系蚁吝,直接就是通過+load的IMP進(jìn)行調(diào)用了
}
// Destroy the detached list.
if (classes) free(classes);
}
上面實(shí)現(xiàn)的主要邏輯發(fā)生在for循環(huán)里面旱爆,該for循環(huán)遍歷了一個(gè)叫classes
的列表,該列表存儲(chǔ)的是一堆loadable_class
結(jié)構(gòu)體窘茁,loadable_class
的定義如下
struct loadable_class {
Class cls; // may be nil
IMP method;
};
每一個(gè)struct loadable_class
變量怀伦,存儲(chǔ)的應(yīng)該就是 一個(gè)類對(duì)象
+ 一個(gè)與該類相關(guān)的方法實(shí)現(xiàn)
。從loadable_class
這個(gè)命名山林,說明它內(nèi)部的信息肯定是表示一個(gè)可以被加載的類的相關(guān)信息房待,因此合理推斷,它里面的method
應(yīng)該就是類的+load
方法驼抹,cls
就是這個(gè)+load
方法所對(duì)應(yīng)的類對(duì)象桑孩。
我們?cè)倏纯丛创a中對(duì)于classes
這個(gè)數(shù)組進(jìn)行遍歷時(shí)到底做了什么。很簡(jiǎn)單框冀,就是通過函數(shù)指針load_method
從loadable_class
中獲得+load
方法的IMP
作為其參數(shù)流椒,然后就直接對(duì)其進(jìn)行調(diào)用(*load_method)(cls, SEL_load);
,所以明也,<font color='red'>類對(duì)象的+load
方法的調(diào)用實(shí)際上就發(fā)生在這里</font>宣虾。這里的for循環(huán)一旦結(jié)束,classes
所包含的所有類對(duì)象的+load
方法就會(huì)被依次調(diào)用温数,這跟一個(gè)類是否被在工程項(xiàng)目里被實(shí)例化過绣硝,是否接受過消息,沒有關(guān)系撑刺。
至此鹉胖,Runtime對(duì)于+load
方法是如何調(diào)用的問題我們分析了一半,弄清楚了類對(duì)象的+load
方法的是怎么被一個(gè)一個(gè)調(diào)用的够傍,也就是static void call_class_loads(void)
這個(gè)函數(shù)甫菠,接下來,還有問題的另一半--static bool call_category_loads(void)
冕屯,也就是關(guān)于分類的+load
方法的調(diào)用淑蔚。進(jìn)入其中
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.--------->A 分離可加載categor`列表
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list. -------->B 調(diào)用detached list 里面的所有+load方法
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}
// Compact detached list (order-preserving) --------->C 清理cats 里面已經(jīng)被消費(fèi)過的成員,并且更新used計(jì)數(shù)值
shift = 0;
for (i = 0; i < used; i++) {
if (cats[i].cat) {
cats[i-shift] = cats[i];
} else {
shift++;
}
}
used -= shift;
// Copy any new +load candidates from the new list to the detached list. ---->D 如果又出現(xiàn)了新的可加載的分類愕撰,將其相關(guān)內(nèi)容復(fù)制到`cats`列表上
new_categories_added = (loadable_categories_used > 0);
for (i = 0; i < loadable_categories_used; i++) {
if (used == allocated) {
allocated = allocated*2 + 16;
cats = (struct loadable_category *)
realloc(cats, allocated *
sizeof(struct loadable_category));
}
cats[used++] = loadable_categories[i];
}
// Destroy the new list.------->E 銷毀列表 loadable_categories
if (loadable_categories) free(loadable_categories);
// Reattach the (now augmented) detached list.
// But if there's nothing left to load, destroy the list.
if (used) {
loadable_categories = cats;
loadable_categories_used = used;
loadable_categories_allocated = allocated;
} else {
if (cats) free(cats);
loadable_categories = nil;
loadable_categories_used = 0;
loadable_categories_allocated = 0;
}
if (PrintLoading) {
if (loadable_categories_used != 0) {
_objc_inform("LOAD: %d categories still waiting for +load\n",
loadable_categories_used);
}
}
return new_categories_added;
}
我們可以看到,這個(gè)方法的實(shí)現(xiàn)里面醋寝,通過系統(tǒng)注釋搞挣,被劃分如下幾塊:
- A --
// Detach current loadable list
.分離可加載category
列表,也就是把可加載列表的信息保存到本函數(shù)的局部變量cats數(shù)組上音羞。 - B --
// Call all +loads for the detached list
.消費(fèi)cats
里面的所有+load
方法(也就是調(diào)用它們) - C --
// Compact detached list (order-preserving)
清理cats
里面已經(jīng)被消費(fèi)過的成員囱桨,并且更新used
計(jì)數(shù)值 - D --
// Copy any new +load candidates from the new list to the detached list.
如果又出現(xiàn)了新的可加載的分類,將其相關(guān)內(nèi)容復(fù)制到cats
列表上嗅绰。 - E --
// Destroy the new list.
銷毀列表(這里指的是外部的loadable_categories
變量) - F --
// Reattach the (now augmented) detached list. But if there's nothing left to load, destroy the list.
更新幾個(gè)記錄了category+load信息的幾個(gè)全局變量舍肠。
分類的處理方法call_category_loads()
和類的處理方法同理.所以我們現(xiàn)在明白了,為什么每個(gè)load
方法都會(huì)調(diào)用,<font color=FF0000>因?yàn)?code>load方法是直接拿到每個(gè)類load
方法的地址,直接調(diào)用,并不是像test()
方法那樣通過消息發(fā)送機(jī)制去查找.</font>
小結(jié)
Runtime
對(duì)于+load
方法的調(diào)用搀继,不是走的我們熟悉的“消息發(fā)送”路線,而是直接拿到+load
方法的IMP
,直接調(diào)用。因此不存在所謂“類的方法被category
的方法覆蓋”的問題.
目前泻蚊,我們確定了<font color=FF0000>類對(duì)象的+load
方法會(huì)先于分類的+load
方法被調(diào)用</font>轧抗,并且不存在覆蓋現(xiàn)象。
- 那么對(duì)于類于類之間
+load
調(diào)用順序是怎樣的刨肃? -
同樣的疑問對(duì)于分類(
category
)又是如何呢?
這兩個(gè)問題,我們就需要進(jìn)入prepare_load_methods
方法的實(shí)現(xiàn)黑滴,看看+load
方法被調(diào)用前,Runtime是如何準(zhǔn)備它們的紧索。
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
//根據(jù)編譯順序把類存放到 classlist 中
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
//定制任務(wù),規(guī)劃任務(wù),處理類的 load 方法.
schedule_class_load(remapClass(classlist[i]));
}
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
realizeClass(cls);
assert(cls->ISA()->isRealized());
//把分類的load方法添加到 loadable_classes 列表中
add_category_to_loadable_list(cat);
}
}
classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count);
可以看出袁辈,利用系統(tǒng)提供的函數(shù)_getObjc2NonlazyClassList
,獲得類對(duì)象的列表珠漂,因?yàn)檫@是系統(tǒng)級(jí)別的函數(shù)晚缩,應(yīng)該跟編譯過程的順序有關(guān),這里先推測(cè)classlist
中類的順序與類的編譯順序相同甘磨。
接下來橡羞,就是遍歷classlist
,對(duì)其每個(gè)成員通過函數(shù)schedule_class_load()
進(jìn)行處理
/***********************************************************************
* prepare_load_methods
* Schedule +load for classes in this image, any un-+load-ed
* superclasses in other images, and any categories in this image.
**********************************************************************/
// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);//遞歸查找济舆,優(yōu)先調(diào)用父類卿泽。
/**
將cls添加到loadable_classes數(shù)組的最后面
*/
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
- 先遞歸調(diào)用自身(
schedule_class_load()
),對(duì)當(dāng)前類(也就是函數(shù)傳入的參數(shù))的父類進(jìn)行處理 - 處理完父類之后滋觉,將當(dāng)前類對(duì)象加入到可加載類的相關(guān)列表當(dāng)中
add_class_to_loadable_list(cls);
<font color=FF0000>經(jīng)過這樣的整理之后签夭,最終整理過的裝載類對(duì)象相關(guān)信息的數(shù)組中,父類應(yīng)該排在子類前面椎侠。而不同的類對(duì)象之間在數(shù)組中的位置第租,就可以參考它們.m的編譯順序來看了</font>,load方法的加載順序是,優(yōu)先調(diào)用父類的laod
方法,再調(diào)用子類的laod
方法
那如果沒有子類關(guān)系,有很多同級(jí)別的類,laod
方法是怎樣調(diào)用的呢?我們可以試一下,新增Cat
和Dog
類,然后運(yùn)行:
可以看到,Cat
和Dog
方法線運(yùn)行,打印順序和編譯順序是大致一致的
優(yōu)先加載類的laod
方法,再加載分類的load
方法嘛
小結(jié)
- 那么對(duì)于類于類之間
+load
調(diào)用順序是怎樣的?
調(diào)用一個(gè)類對(duì)象的+load
方法之前我纪,會(huì)先調(diào)用其父類的+load
方法(如果存在的話)慎宾,類與類之間,會(huì)按照編譯的順序浅悉,先后調(diào)用其+load
方法趟据。一個(gè)類對(duì)象的+load
方法不會(huì)被重復(fù)調(diào)用,只可能被調(diào)用一次术健。- 同樣的疑問對(duì)于分類(
category
)又是如何呢汹碱?
分類的+load
方法,會(huì)按照分類參與編譯的順序荞估,先編譯的咳促,先被調(diào)用稚新。
+initialize
方法
initialize
方法和load
方法很多人一直傻傻分不清楚,這兩個(gè)方法的確也很容易搞混淆,下面我們將研究一下initialize
方法,搞清楚他們之間的區(qū)別.
關(guān)于+initialize方法的一些結(jié)論
-
+initialize
方法會(huì)在類第一次接收到消息的時(shí)候調(diào)用 -
+initialize
方法是通過objc_msgSend()
進(jìn)行調(diào)用的
#import "MJPerson.h"
@implementation MJPerson
+ (void)initialize
{
NSLog(@"MJPerson +initialize");
}
@end
#import "MJPerson+Test1.h"
@implementation MJPerson (Test1)
+ (void)initialize
{
NSLog(@"MJPerson (Test1) +initialize");
}
@end
#import "MJPerson+Test2.h"
@implementation MJPerson (Test2)
+ (void)initialize
{
NSLog(@"MJPerson (Test2) +initialize");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
RUN>
沒有任何輸出
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MJPerson alloc];
}
return 0;
}
RUN
2021-04-19 15:19:39.887208+0800 Interview01-load[3169:158165] MJPerson (Test2) +initialize
會(huì)發(fā)現(xiàn)調(diào)用了分類
Person + Test2
的initialize
方法,這說明initialize
方法是通過msgSend(target,sel)
消息發(fā)送來調(diào)用方法的.如果分類有相同的方法,會(huì)優(yōu)先調(diào)用分類的方法.
那么含有繼承關(guān)系的類調(diào)用initialize
方法的順序是怎樣的呢?我們?cè)賱?chuàng)建一個(gè)Teacher
類繼承Person
類,所以Person
現(xiàn)在就有兩個(gè)子類:Student
,Teacher
.
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MJStudent alloc];
[MJTeacher alloc];
}
return 0;
}
RUN>
2021-04-19 15:40:11.670509+0800 Interview01-load[3315:167613] MJPerson (Test2) +initialize 2021-04-19 15:40:11.671034+0800 Interview01-load[3315:167613] MJStudent +initialize 2021-04-19 15:40:11.671094+0800 Interview01-load[3315:167613] MJTeacher +initialize
會(huì)發(fā)現(xiàn)
Person , Student , Teacher
三個(gè)類的initialize
方法都調(diào)用了,這是為什么呢?剛才我們說過,initialize
方法是通過消息發(fā)送機(jī)制調(diào)用的,<font color=FF0000>按理說它只會(huì)調(diào)用子類的中的方法,為什么父類的方法也會(huì)調(diào)用?</font>
我們?cè)侔?code>Student , Teacher中的initialize
方法注釋掉,再運(yùn)行一下:
2021-04-19 15:44:36.106119+0800 Interview01-load[3343:169965] MJPerson (Test2) +initialize
2021-04-19 15:44:36.106554+0800 Interview01-load[3343:169965] MJPerson (Test2) +initialize
2021-04-19 15:44:36.106601+0800 Interview01-load[3343:169965] MJPerson (Test2) +initialize
從上圖多次調(diào)用的運(yùn)行結(jié)果我們也可以猜測(cè),一個(gè)類的initialize
方法只會(huì)調(diào)用一次,那么msgSend(cls,sel)
方法內(nèi)在執(zhí)行initialize
方法之前很可能會(huì)判斷父類的的是否已經(jīng)初始化,如果父類沒有初始化,則初始化父類.
那么到底是不是如我們猜測(cè)的那樣呢?還是從runtime
源碼中找尋答案.
我們?cè)?code>runtime源碼中搜索objc_msgSend(
,會(huì)發(fā)現(xiàn)objc_msgSend()
方法的源碼都是匯編代碼:
后面流程略,從MJ老師課程直接進(jìn)入objc_msgLookup
等價(jià)的方法跪腹,它就是Method class_getInstanceMethod(Class cls, SEL sel)
褂删。進(jìn)入該方法查看一下lookUpImpOrNil
-> 進(jìn)入lookUpImpOrForward
:
/***********************************************************************
* lookUpImpOrNil.
* Like lookUpImpOrForward, but returns nil instead of _objc_msgForward_impcache
**********************************************************************/
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
最后再進(jìn)入lookUpImpOrForward
會(huì)發(fā)現(xiàn)我們要找的重點(diǎn):
......
//initialize是否需要初始化 !cls->isInitialized這個(gè)類沒有初始化
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
......
上面會(huì)判斷如果需要初始化并且這個(gè)類沒有初始化,就進(jìn)入_class_initialize方法進(jìn)行初始化尺迂,驗(yàn)證了笤妙,一個(gè)類只初始化一次。
進(jìn)入_class_initialize
:
void _class_initialize(Class cls)
{
assert(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
//如果有父類噪裕,并且父類沒有初始化就遞歸調(diào)用蹲盘,初始化父類
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
......
//沒有父類或者父類已經(jīng)初始化,開始初始化子類
callInitialize(cls); //初始化子類
......
上面會(huì)先判斷如果有父類并且父類沒有初始化就遞歸調(diào)用膳音,初始化父類召衔,如果沒有父類或者父類已經(jīng)初始化,就開始初始化子類祭陷。驗(yàn)證了苍凛,先初始化父類,再初始化子類兵志。
進(jìn)入callInitialize醇蝴, 開始初始化這個(gè)類
void callInitialize(Class cls)
{
//第一個(gè)參數(shù)是類,第二個(gè)參數(shù)是SEL_initialize消息
//就是給某個(gè)類發(fā)送SEL_initialize消息
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
通過上面的源碼分析想罕,可以知道悠栓,的確是先調(diào)用父類的Initialize再調(diào)用子類的Initialize,并且一個(gè)類只會(huì)初始化一次按价。
經(jīng)過一系列的調(diào)用過程惭适,會(huì)發(fā)現(xiàn)正如我們猜測(cè)的一樣:在_class_initialize
內(nèi)部會(huì)先判斷父類是否已經(jīng)初始化,如果父類未初始化,則先初始化父類再初始化子類.
回到剛才的問題,為何Student , Teacher
中的initialize
方法都注釋掉后,仍然打印3次initialize
?
結(jié)合剛才的源碼,我們可以大致分析一下[Student alloc] , [Teacher alloc]
底層偽代碼大致如下:
bool personIsInItializer = NO;
bool studentIsInItializer = NO;
bool teacherIsInItializer = NO;
//調(diào)用 [Student alloc]
if (Student 未被初始化){
if (Student 的父類 Person 未被初始化) {
1: 初始化 Person 類
2: personIsInItializer = YES
}
1: 初始化 Student 類
2: studentIsInItializer = YES;
}
// 調(diào)用 [Teacher alloc]
if (Teacher 未被初始化){
if (Teacher 的父類 Person 未被初始化) {
1: 初始化 Person 類
2: personIsInItializer = YES
}
1: 初始化 Teacher 類
2: teacherIsInItializer = YES;
}
initialize
總結(jié)
+initialize
方法會(huì)在類對(duì)象 *第一次* 接收到消息的時(shí)候調(diào)用調(diào)用順序:調(diào)用某個(gè)類的
+initialize
之前,會(huì)先調(diào)用其父類的+initialize
(前提是父類的+initialize
從來沒有被調(diào)用過)由于
+initialize
的調(diào)用楼镐,是通過消息機(jī)制癞志,也就是objc_msgSend()
,因此如果子類的+initialize
沒有實(shí)現(xiàn)框产,就會(huì)去調(diào)用父類的+initialize
基于同樣的原因凄杯,如果分類實(shí)現(xiàn)的
+initialize
,那么就會(huì)“覆蓋”類對(duì)象本身的+initialize
方法而被調(diào)用秉宿。
+ load
和 + initialize
方法的區(qū)別:
- 調(diào)用方式:
load
是直接拿到函數(shù)地址,直接調(diào)用;initialize
是通過消息機(jī)制調(diào)用. - 調(diào)用時(shí)機(jī):
load
是runtime
加載類或者分類的時(shí)候調(diào)用,不管有沒有使用這個(gè)類,都會(huì)調(diào)用,也就是<font color=FF0000>說load
方法是肯定會(huì)執(zhí)行的</font>;initialize
是類第一次接收到消息的時(shí)候調(diào)用,如果沒有向這個(gè)類發(fā)送消息,則不會(huì)調(diào)用.
面試題:
問題一:+load方法和+ Initialize方法的區(qū)別是什么戒突?
- 調(diào)用時(shí)機(jī):load是在Runtime加載類、分類的時(shí)候調(diào)用蘸鲸,只會(huì)調(diào)用一次,Initialize是在類第一次接收到消息時(shí)調(diào)用窿锉,每一個(gè)類只會(huì)初始化一次酌摇。
- 調(diào)用方式:load是根據(jù)函數(shù)地址直接調(diào)用膝舅,Initialize是通過objc_msgSend調(diào)用。
問題二:說一下load和Initialize的調(diào)用順序窑多?
對(duì)于load:先調(diào)用父類的+load仍稀,后調(diào)用子類的+load,再調(diào)用分類的+load埂息,并且先編譯的先調(diào)用技潘。
對(duì)于Initialize:先調(diào)用父類的+initialize,再調(diào)用子類的+initialize(先初始化父類千康,再初始化子類)
特別備注
本系列文章總結(jié)自MJ老師在騰訊課堂iOS底層原理班(下)/OC對(duì)象/關(guān)聯(lián)對(duì)象/多線程/內(nèi)存管理/性能優(yōu)化享幽,相關(guān)圖片素材均取自課程中的課件。如有侵權(quán)拾弃,請(qǐng)聯(lián)系我刪除值桩,謝謝!