最近工作比較閑,想鞏固一下自己的iOS開發(fā)基礎(chǔ)知識(shí),就回答一下阿里、字節(jié):一套高效的iOS面試題,歡迎各位同行批評(píng)斧正!
runtime是iOS開發(fā)最核心的知識(shí)了,如果下面的問(wèn)題都解決了,那么對(duì) objc-runtime 的理解已經(jīng)很深了。
runtime已經(jīng)開源了,這有一份別人調(diào)試好可運(yùn)行的源碼objc-runtime,也可以去官網(wǎng)找objc4,以下回答用到的源碼版本是objc4-756.2。
結(jié)構(gòu)模型
1、介紹下runtime的內(nèi)存模型(isa、對(duì)象、類、metaclass、結(jié)構(gòu)體的存儲(chǔ)信息等)
isa :
1、所有繼承于NSObject類的對(duì)象,在內(nèi)存布局中,第一個(gè)變量都是isa。
2、在arm64之前,實(shí)例對(duì)象的isa指向類對(duì)象,類對(duì)象的isa指向元類對(duì)象。
3、在arm64之后,isa經(jīng)過(guò)了優(yōu)化,采取了共用體的結(jié)構(gòu),將一個(gè)64位的內(nèi)存數(shù)據(jù)分開存儲(chǔ)了很多的信息,其中的33位才是存儲(chǔ)類對(duì)象、元類對(duì)象的地址值的,可以通過(guò)一個(gè)位運(yùn)算取出instance的isa包含的class的地址,取出class的isa包含的meta-class的地址。
union isa_t {
Class cls;
#if defined(ISA_BITFIELD)
struct {
uintptr_t nonpointer : 1; //0,代表普通的指針,存儲(chǔ)著Class、Meta-Class對(duì)象的內(nèi)存地址,1,代表優(yōu)化過(guò),使用位域存儲(chǔ)更多的信息
uintptr_t has_assoc : 1; //是否有設(shè)置過(guò)關(guān)聯(lián)對(duì)象,如果沒有,釋放時(shí)會(huì)更快
uintptr_t has_cxx_dtor : 1; //是否有C++的析構(gòu)函數(shù)(.cxx_destruct),如果沒有,釋放時(shí)會(huì)更快
uintptr_t shiftcls : 33; //存儲(chǔ)著Class、Meta-Class對(duì)象的內(nèi)存地址信息
uintptr_t magic : 6; //用于在調(diào)試時(shí)分辨對(duì)象是否未完成初始化
uintptr_t weakly_referenced : 1; //是否有被弱引用指向過(guò),如果沒有,釋放時(shí)會(huì)更快
uintptr_t deallocating : 1; //對(duì)象是否正在釋放
uintptr_t has_sidetable_rc : 1; //里面存儲(chǔ)的值是引用計(jì)數(shù)器減1
uintptr_t extra_rc : 19 //引用計(jì)數(shù)器是否過(guò)大無(wú)法存儲(chǔ)在isa中,如果為1,那么引用計(jì)數(shù)會(huì)存儲(chǔ)在一個(gè)叫SideTable的類的屬性中
};
#endif
};
對(duì)象:
@interface OffcnPerson : NSObject {
@public
int _age;
int _no;
}
OffcnPerson *person = [[OffcnPerson alloc] init];
person->_age = 25;
person->_no = 77123;
以上面的person對(duì)象為例,它的內(nèi)存結(jié)構(gòu)包含它父類的變量isa指針,以及person對(duì)象自身的變量
_age、_no。實(shí)際分配的大小應(yīng)該是所有變量加起來(lái),內(nèi)存對(duì)齊后的大小,對(duì)象的底層是使用結(jié)構(gòu)體存儲(chǔ)變量的。
結(jié)構(gòu)體的大小必須是結(jié)構(gòu)體中占用內(nèi)存最大的成員變量的倍數(shù) ,上面的person對(duì)象占用內(nèi)存變量是isa指針,占用8個(gè)字節(jié),所以person內(nèi)存最小得是16個(gè)字節(jié)。如果再增加一個(gè)int _height,變量的總大小是20個(gè)字節(jié),實(shí)際上會(huì)分配24個(gè)字節(jié)。
類:
class對(duì)象在內(nèi)存中存儲(chǔ)的信息主要包括
isa指針、superclass指針、類的屬性信息(@property)、類的對(duì)象方法信息( instance method)、類的協(xié)議信息(Protocol)、類的成員變量信息 (ivar)。
metaclass:
meta-class對(duì)象和class對(duì)象的內(nèi)存結(jié)構(gòu)是一樣的,在內(nèi)存中存儲(chǔ)的信息主要包括
isa指針、superclass指針、類的屬性信息(@property)、類的對(duì)象方法信息( instance method)。
2、為什么要設(shè)計(jì)metaclass
1、如果對(duì)象方法、類方法都儲(chǔ)存在類對(duì)象的結(jié)構(gòu)體中,需要在objc_class的結(jié)構(gòu)中增加一個(gè)數(shù)組存儲(chǔ)類對(duì)象方法列表。

2、需要在調(diào)用objc_msgSend的時(shí)候就需要額外追加一個(gè)參數(shù)去分辨該次調(diào)用的是對(duì)象方法還是類方法,而我們現(xiàn)在的objc_msgSend()只接收了(id self, SEL _cmd, ...)這三種參數(shù),第一個(gè)self就是消息的接收者,第二個(gè)就是方法,后續(xù)的...就是各式各樣的參數(shù)。
如果不加參數(shù),對(duì)象方法和類方法同名時(shí)就不知道調(diào)用的是哪一個(gè)了。
如果在objc_msgSend中再添加一個(gè)參數(shù)標(biāo)識(shí)是對(duì)象方法還是類方法,就需要在消息發(fā)送機(jī)制中進(jìn)行對(duì)象類型和方法類型判斷,影響消息發(fā)送的效率。
3、把對(duì)象方法和類方法耦合在一起不符合設(shè)計(jì)模式中的單一職責(zé)原則,通過(guò)增加一個(gè)和類對(duì)象具有相同結(jié)構(gòu)的metaclass后,實(shí)例對(duì)象就干存儲(chǔ)屬性值的事,類對(duì)象存儲(chǔ)實(shí)例方法列表,元類對(duì)象存儲(chǔ)類方法列表。
instance的isa指向class,當(dāng)調(diào)用對(duì)象方法時(shí),通過(guò)instance的isa找到class,最后找到對(duì)象方法的實(shí)現(xiàn)進(jìn)行調(diào)用,class的isa指向meta-class,當(dāng)調(diào)用類方法時(shí),通過(guò)class的isa找到meta-class,最后找到類方法的實(shí)現(xiàn)進(jìn)行調(diào)用。
3、class_copyIvarList&class_copyPropertyList區(qū)別
class_copyPropertyList返回的僅僅是對(duì)象類的屬性(@property聲明的屬性),而class_copyIvarList返回類的所有屬性(等同于getter+setter+變量)和變量(包括在@interface大括號(hào)中聲明的變量)。
@interface OffcnStudent ()
{
int _age;
int _no;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *department;
@end
- (void)invokeClass_copyPropertyList {
unsigned int count =0;
objc_property_t *properties = class_copyPropertyList(OffcnStudent.class, &count);
for (int i =0; i<count;i++) {
objc_property_t property = properties[i];
//獲取屬性的名字
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
NSLog(@"propertyName:%@",propertyName);
}
}
//打印
/*
propertyName:name
propertyName:department
*/
- (void)invokeClass_copyIvarList {
unsigned int count =0;
Ivar *ivars = class_copyIvarList(OffcnStudent.class, &count);
for (int i =0; i<count;i++) {
//獲取屬性的名字
NSString *ivarName = [[NSString alloc] initWithCString:ivar_getName(ivars[i]) encoding:NSUTF8StringEncoding];
NSLog(@"ivarName:%@",ivarName);
}
}
//打印
/*
ivarName:_age
ivarName:_no
ivarName:_name
ivarName:_department
*/
4、class_rw_t 和 class_ro_t 的區(qū)別
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
//由此可以看出bits是采用位域的方式存儲(chǔ)數(shù)據(jù)的,其中的某一些內(nèi)存空間存放的是class_rw_t
class_ro_t存放的是編譯期間就確定的;而class_rw_t是在runtime時(shí)才確定。
調(diào)用realizeClassWithoutSwift方法時(shí),如果當(dāng)前的class沒有實(shí)現(xiàn)初始化,會(huì)對(duì)rw進(jìn)行內(nèi)存分配、將class_ro_t的內(nèi)容拷貝過(guò)去,把rw設(shè)置給class的data、設(shè)置nextSiblingClass和 firstSubclass屬性、然后再將當(dāng)前類的分類的方法拷貝到rw的methods方法列表中。
當(dāng)然實(shí)際訪問(wèn)類的方法、屬性等也都是訪問(wèn)的class_rw_t中的內(nèi)容。
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
class_rw_t *rw;
Class supercls;
Class metacls;
if (!cls) return nil;
if (cls->isRealized()) return cls;
auto ro = (const class_ro_t *)cls->data();
auto isMeta = ro->flags & RO_META;
//初始化rw,把ro設(shè)置給rw
// Normal class. Allocate writeable class data.
rw = objc::zalloc<class_rw_t>();
rw->set_ro(ro);
rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
cls->setData(rw); //把rw設(shè)置給class的data
// Realize superclass and metaclass, if they aren't already.
// This needs to be done after RW_REALIZED is set above, for root classes.
// This needs to be done after class index is chosen, for root metaclasses.
supercls = realizeClassWithoutSwift(remapClass(cls->superclass), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
// Update superclass and metaclass in case of remapping
cls->superclass = supercls;
cls->initClassIsa(metacls);
// Reconcile instance variable offsets / layout.
// This may reallocate class_ro_t, updating our ro variable.
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);
// Set fastInstanceSize if it wasn't set already.
cls->setInstanceSize(ro->instanceSize);
// Connect this class to its superclass's subclass lists
if (supercls) {
addSubclass(supercls, cls); //設(shè)置nextSiblingClass和 firstSubclass屬性
} else {
addRootClass(cls);
}
// Attach categories 追加分類
methodizeClass(cls, previously);
return cls;
}
5、category如何被加載的,兩個(gè)category的load方法的加載順序,兩個(gè)category的同名方法的加載順序
實(shí)現(xiàn)思路:
1、每個(gè)分類中有多個(gè)方法,根據(jù)分類中方法的總大小 + 原來(lái)類對(duì)象中方法列表的大小,realloc重新分配數(shù)組內(nèi)存空間
2、往后挪動(dòng)原來(lái)類對(duì)象方法(類方法)列表的數(shù)據(jù),挪動(dòng)的大小為分類中方法的總大小
3、根據(jù)編譯順序把分類中的方法倒序添加的數(shù)組中,依次加入到新分配數(shù)組的前面
4、根據(jù)objc_msgsend去調(diào)用方法,先找類對(duì)象的方法列表,再通過(guò)superclass找到父類的方法列表進(jìn)行調(diào)用
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
bool isMeta = cls->isMetaClass();
// 為方法列表的二維數(shù)組分配內(nèi)存,大小為分類方法的總大小
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int i = cats->count;
while (i--) {
//從分類方法的末尾取出方法
auto& entry = cats->list[i];
//根據(jù)當(dāng)前cls是類對(duì)象還是元類對(duì)象,對(duì)應(yīng)取出對(duì)象方法和類方法
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist; //依次把分類中的方法加入方法列表中
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
}
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount; //分類方法與類中的方法總和
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
//把類中的方法向后挪(分類的方法總數(shù))個(gè)位置
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
//把分類中的方法列表拷貝到方法數(shù)組的前面
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
1、category如何被加載的?
從上面的兩段代碼可以看出,分類中的方法會(huì)被添加到方法列表的前面。
分類中的方法是按照倒序的方法添加到方法的列表前面的。
2、兩個(gè)category的load方法的加載順序?
1、首先要搞清楚load方法什么時(shí)候調(diào)用?load方法是在程序啟動(dòng)runtime加載類、分類的時(shí)候就會(huì)調(diào)用。
2、先調(diào)用類的load方法,再調(diào)用分類的load方法,load方法的調(diào)用不是通過(guò)消息發(fā)送機(jī)制,而是找到函數(shù)地址直接調(diào)用。
3、分類中的load方法是按照編譯的先后順序加載調(diào)用的,具體可以看下面call_category_loads中的實(shí)現(xiàn)。
4、先調(diào)用父類,再調(diào)用子類,先編譯先調(diào)用,具體可以看下面schedule_class_load的實(shí)現(xiàn)。
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(重復(fù)的調(diào)用類的+load方法)
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE(然后調(diào)用分類的+load)
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
//找到load方法的地址直接調(diào)用
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
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.(按照正序遍歷分類中的load方法,然后調(diào)用)
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;
}
}
}
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);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
3、兩個(gè)category的同名方法的加載順序?
由于分類的方法是倒序添加到方法列表的前面的,所以后編譯的先調(diào)用。
在Build Phases 的Compile Sources中可以調(diào)整編譯的順序。
6、category & extension區(qū)別,能給NSObject添加Extension嗎,結(jié)果如何?
1、category & extension區(qū)別?
category 與 extension的主要區(qū)別是extension相當(dāng)于把變量屬性的訪問(wèn)權(quán)限改為私有了,編譯后就己經(jīng)合并到底層的C++代碼中了,category是在運(yùn)行時(shí)才合并到類的方法列表中。
2、能給NSObject添加Extension嗎,結(jié)果如何?
不能給NSObject添加Extension。Extension里面的變量和屬性都是私有的,需要在.m文件中添加,現(xiàn)在拿不到NSObject的源文件,所以無(wú)法給NSObject添加Extension。
7、OC的消息轉(zhuǎn)發(fā)機(jī)制和其他語(yǔ)言的消息機(jī)制優(yōu)劣對(duì)比?
源代碼 -> 編譯鏈接 -> 運(yùn)行。
對(duì)于C語(yǔ)言來(lái)說(shuō),編譯完之后生成的二進(jìn)制文件就是當(dāng)初源代碼那個(gè)樣子。C語(yǔ)言就是當(dāng)初寫的是什么,編譯的就是什么,運(yùn)行的結(jié)果也就是什么。運(yùn)行結(jié)果和當(dāng)初的編譯時(shí)保持一致的。
OC是一門動(dòng)態(tài)性比較強(qiáng)的編程語(yǔ)言,允許很多操作推遲到程序運(yùn)行時(shí)再進(jìn)行。OC能夠在程序的運(yùn)行中修改之前編譯好的一些東西,可以在運(yùn)行的過(guò)程中添加一些方法。
OC的動(dòng)態(tài)性就是由Runtime來(lái)支撐和實(shí)現(xiàn)的,Runtime是一套C語(yǔ)言的API,封裝了很多動(dòng)態(tài)性相關(guān)的函數(shù)
平時(shí)編寫的OC代碼,底層都是轉(zhuǎn)換成了Runtime API進(jìn)行調(diào)用。
8、在方法調(diào)用的時(shí)候,方法查詢-> 動(dòng)態(tài)解析-> 消息轉(zhuǎn)發(fā) 之前做了什么
OC中的方法調(diào)用其實(shí)都是轉(zhuǎn)成了objc_msgSend函數(shù)的調(diào)用,給receiver(方法調(diào)用者)發(fā)送了一條消息(selector方法名)。
1、方法查詢(消息發(fā)送)
1、在方法查詢之前會(huì)判斷receiver(方法調(diào)用者)是否為nil,如果為nil就直接退出。
2、receiver通過(guò)isa指針找到receiverClass,從receiverClass的cache方法列表中查找selector方法名,找到方法后調(diào)用,結(jié)束查找。
3、在cache方法列表沒找到方法,就去receiverClass的class_rw_t中查找方法,找到方法后調(diào)用,結(jié)束查找。并將該方法緩存到receiverClass的cache方法列表中。
4、在receiverClass的class_rw_t沒找到方法,就從receiverClass的superClass的cache方法列表查找,找到方法后調(diào)用,結(jié)束查找。并將該方法緩存到receiverClass的cache方法列表中。
5、在receiverClass的superClass的cache中沒找到方法,就去receiverClass的superClass的class_rw_t中查找,找到方法后調(diào)用,結(jié)束查找。并將該方法緩存到receiverClass的cache方法列表中。
6、找不到就去receiverClass的superClass的superClass中重復(fù)以上過(guò)程,如果上層已沒有superClass就進(jìn)入下一階段,動(dòng)態(tài)方法解析。
2、動(dòng)態(tài)方法解析
1、判斷是否曾經(jīng)有動(dòng)態(tài)解析,如果有動(dòng)態(tài)解析,直接走第3步中的消息轉(zhuǎn)發(fā)
2、如果沒有動(dòng)態(tài)方法解析過(guò),可以通過(guò)+resolveInstanceMethod:、+resolveClassMethod:來(lái)動(dòng)態(tài)添加方法實(shí)現(xiàn)。
3、添加過(guò)方法后,標(biāo)記為已經(jīng)動(dòng)態(tài)解析,然后重新走消息發(fā)送的流程,“從receiverClass的cache中查找方法”這一步開始執(zhí)行。
void other(id self,SEL _cmd) {
}
+(BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(test)) {
// 動(dòng)態(tài)添加test方法的實(shí)現(xiàn)
class_addMethod(self, sel, (IMP)other, "v@:");
// 返回YES代表有動(dòng)態(tài)添加方法
return YES;
}
return [super resolveClassMethod:sel];
}
3、消息轉(zhuǎn)發(fā)
1、調(diào)用forwardingTargetForSelector:方法,返回值不為nil,調(diào)用objc_msgSend
2、返回值為nil,調(diào)用methodSignatureForSelector:方法,返回值不為nil,調(diào)用forwardInvocation:方法
3、返回值為nil,調(diào)用doesNotRecognizeSelector:方法
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) {
// objc_msgSend([[MJCat alloc] init], aSelector)
return [[MJCat alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
//方法簽名:返回值類型、參數(shù)類型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) {
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
// NSInvocation封裝了一個(gè)方法調(diào)用,包括:方法調(diào)用者、方法名、方法參數(shù)
// anInvocation.target 方法調(diào)用者
// anInvocation.selector 方法名
// [anInvocation getArgument:NULL atIndex:0]
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
anInvocation.target = [[MJCat alloc] init];
[anInvocation invoke];
[anInvocation invokeWithTarget:[[MJCat alloc] init]];
}
9、IMP、SEL、Method的區(qū)別和使用場(chǎng)景?
1、IMP代表函數(shù)的具體實(shí)現(xiàn)
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
2、SEL代表函數(shù)名,一般叫做選擇器,底層結(jié)構(gòu)跟char *類似
可以通過(guò)@selector()和sel_registerName()獲得
可以通過(guò)sel_getName()和NSStringFromSelector()轉(zhuǎn)成字符串
不同類中相同名字的方法,所對(duì)應(yīng)的方法選擇器是相同的
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
method_t是對(duì)函數(shù)的封裝。
typedef struct method_t *Method;
struct method_t {
SEL name; //函數(shù)名
const char *types; //編碼 (返回值類型、參數(shù)類型)
MethodListIMP imp; //指向函數(shù)的指針 (函數(shù)地址)
};
iOS中提供了一個(gè)叫做@encode的指令,可以將具體的類型表示成字符串編碼,具體可以查看Type Encoding
10、load、initialize方法的區(qū)別什么?在繼承關(guān)系中他們有什么區(qū)別
調(diào)用方式不同:
load是根據(jù)函數(shù)地址直接調(diào)用,initialize是通過(guò)objc_msgSend調(diào)用。
調(diào)用時(shí)機(jī)不同:
load是runtime加載類、分類的時(shí)候調(diào)用(只會(huì)調(diào)用1次)
initialize是類第一次接收到消息的時(shí)候調(diào)用,每一個(gè)類只會(huì)initialize一次(父類的initialize方法可能會(huì)被調(diào)用多次)。
load、initialize的調(diào)用順序?
load
1> 先調(diào)用類的load
a) 先編譯的類,優(yōu)先調(diào)用load
b) 調(diào)用子類的load之前,會(huì)先調(diào)用父類的load
2> 再調(diào)用分類的load
a) 先編譯的分類,優(yōu)先調(diào)用load
initialize
1> 先初始化父類
2> 再初始化子類(可能最終調(diào)用的是父類的initialize方法)