??首先我們都知道在iOS應(yīng)用啟動(dòng)的時(shí)候會(huì)調(diào)用所有類和其分類的+load方法。子類的load方法會(huì)在父類方法執(zhí)行完成之后執(zhí)行,分類的+load會(huì)在主類執(zhí)行之后執(zhí)行。不可繼承,子類沒(méi)有實(shí)現(xiàn)的時(shí)候,文件加載的時(shí)候是不會(huì)調(diào)用父類的load方法的。那么為什么+load的方法會(huì)有這樣的特性,runtime又有哪些巧妙的處理呢。今天我們來(lái)刨根問(wèn)底一下+load方法。
??首先解釋兩個(gè)變量和兩個(gè)結(jié)構(gòu)體,可以先看一眼有個(gè)印象,后面遇到再回來(lái)仔細(xì)察看
//每次調(diào)用add_class_to_loadable_list()方法都會(huì)++,記錄這個(gè)方法的調(diào)用次數(shù),也相當(dāng)于Class中+load方法列表的個(gè)數(shù)
static int loadable_classes_used = 0;
//同上,只不過(guò)對(duì)應(yīng)的是add_category_to_loadable_list()方法和Category中的+load方法列表
static int loadable_categories_used = 0;
//存儲(chǔ)了+load方法所屬的Class和+load方法的IMP
struct loadable_class {
Class cls; // may be nil
IMP method;
};
//同上,只不過(guò)對(duì)應(yīng)的是Category
struct loadable_category {
Category cat; // may be nil
IMP method;
};
//存儲(chǔ)Class和categoty這兩個(gè)struct數(shù)據(jù)的是數(shù)組
1. Category方法列表的裝載
1.1 調(diào)用棧
??我們還是通過(guò)runtime的源碼來(lái)分析這個(gè)問(wèn)題首先看分類的方法是什么時(shí)候加到主類的methed_list_t列表中的。下圖為調(diào)用棧:
??可以看到調(diào)用次序是這樣的,我們重點(diǎn)看_read_images方法。
map_images() —>map_images_nolock()—>_read_images()
1.2 _read_images()
??源碼較長(zhǎng),這里我只貼對(duì)我們有用的部分
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
//前面的都省略
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
//后面的也省略
}
??注意到里面的addUnattachedCategoryForClass方法了嗎,OC里面都喜歡這種見(jiàn)文知義的命名方法,直譯過(guò)來(lái)就是為類添加未附加的類別。在這個(gè)方法里面,會(huì)找到未添加的類別列表對(duì)Class和Category做一個(gè)映射關(guān)聯(lián)。而在remethodizeClass()方法里面會(huì)調(diào)用attachCategories把Category中的方法列表加到Class的methed_list_t里面去。而且是插入到Class方法列表的前面(這就是Category中重寫主類的方法導(dǎo)致的方法覆蓋的原因)
2. +load方法的調(diào)用
2.1 調(diào)用棧
??我們接著來(lái)看+load的方法的調(diào)用,如下圖所示,這些會(huì)在Category方法裝載之后!下圖為調(diào)用棧

2.2 load_images()
??load_images()方法會(huì)多次調(diào)用(每個(gè)類都會(huì)調(diào)用一次)。我們來(lái)看load_images()方法的具體內(nèi)容
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
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
??里面有prepare_load_methods()和call_load_methods()這兩個(gè)主要方法,先來(lái)看第一個(gè)
2.3 prepare_load_methods()
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertWriting();
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
#pragma mark - 我是分割線—————————————————————————————————
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());
add_category_to_loadable_list(cat);
}
}
??這里面又分兩個(gè)重要的部分,上面分割線之前是Class中的+load方法加入到對(duì)應(yīng)的list中去,那怎么保證的父類先調(diào)用呢
2.3.1 schedule_class_load() & add_class_to_loadable_list()
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);
}
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
//往數(shù)組中添加loadable_classes結(jié)構(gòu)體,并賦值
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
??schedule_class_load()方法做了遞歸調(diào)用一直調(diào)用到superclass為空,在schedule_class_load()方法中會(huì)調(diào)用add_class_to_loadable_list。這樣就保證了父類的+load方法是加載到list前面的,從父類到子類依次往數(shù)組中添加。執(zhí)行的時(shí)候也是從前往后遍歷數(shù)組調(diào)用。
??add_class_to_loadable_list()方法的實(shí)現(xiàn),我們可以很清楚的看到loadable_classes的初始化策略與溢出時(shí)的擴(kuò)容策略。每次需要擴(kuò)容都會(huì)在原來(lái)的基礎(chǔ)之上*2+16。
2.3.2 add_category_to_loadable_list()
void add_category_to_loadable_list(Category cat)
{
IMP method;
loadMethodLock.assertLocked();
method = _category_getLoadMethod(cat);
// Don't bother if cat has no +load method
if (!method) return;
if (PrintLoading) {
_objc_inform("LOAD: category '%s(%s)' scheduled for +load",
_category_getClassName(cat), _category_getName(cat));
}
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
//往數(shù)組中添加loadable_categories結(jié)構(gòu)體,并賦值
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
??我們來(lái)看分割線之后的部分,在Class的+load方法處理完成時(shí)候才會(huì)來(lái)處理Category中的+load方法,這里用到了add_category_to_loadable_list()可以看到與add_class_to_loadable_list()方法的內(nèi)容基本一樣只是標(biāo)志位和數(shù)組的名字不同而已。add_category_to_loadable_list()在Category中的+load方法加入到對(duì)應(yīng)的列表中。至此prepare_load_methods()方法執(zhí)行完畢。
2.4 call_load_methods()
??接下來(lái)我們來(lái)看call_load_methods()方法的具體內(nèi)容
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();
}
// 2. Call category +loads ONCE
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;
}
??看代碼這里還用到了aotureleasePool,這個(gè)是題外話了,撇開(kāi)不談。我們來(lái)看今天的重點(diǎn),首先是一個(gè)do while循環(huán),循環(huán)條件有兩個(gè)。第一個(gè)loadable_classes_used(查看文章開(kāi)頭),第二個(gè)是call_category_loads()方法的返回值,這個(gè)返回值是什么呢,其實(shí)就是loadable_categories_used>0。在這里先后調(diào)用所有Class列表中的+load方法,Category列表中的+load方法call_class_loads()和call_category_loads()兩個(gè)方法的代碼我就不貼了。代碼有點(diǎn)多,影響閱讀,有興趣可以去看runtime的源碼。
3. 總結(jié)與問(wèn)題
3.1總結(jié)
??分析下來(lái),基本回答了文章開(kāi)頭所述的+load方法特征的原因??偨Y(jié)如下:
??1、Category方法列表的裝載是在_read_images的時(shí)候發(fā)生的,這個(gè)調(diào)用比較早在map_images之后,load_images前。
??2、+load方法的調(diào)用是在load_images是發(fā)生的,而且load_image會(huì)重復(fù)調(diào)用(每個(gè)類都會(huì)調(diào)用)。而load_images調(diào)在_read_images之后,也就是說(shuō)是在Category中的方法插入到Class中的方法列表之后調(diào)用的。
??3、+load方法的調(diào)用是又專門的方法負(fù)責(zé)的,Class和Category分別有一個(gè)數(shù)組保存+load方法(數(shù)組內(nèi)保存的是loadable_class,和loadable_category結(jié)構(gòu)體參考文章開(kāi)頭的介紹)。所以不會(huì)被Category方法的裝載導(dǎo)致方法覆蓋。
??4、Class的數(shù)組,加入的次序也是有保證的,從最高級(jí)的父類到子類一次加入,調(diào)用的時(shí)候能保證先父類后子類。
??5、Category數(shù)組,調(diào)用是在Class中的方法列表調(diào)用完成之后,保證了次序。
3.2問(wèn)題
??call_class_loads()的實(shí)現(xiàn)比較簡(jiǎn)單,就是一個(gè)for循環(huán)依次調(diào)用+load。call_category_loads()內(nèi)部的實(shí)現(xiàn)就復(fù)雜了不少,還有許多復(fù)雜的代碼,筆者也是沒(méi)有太看懂內(nèi)部的那些判斷和循環(huán),也不敢在這里誤人子弟。如果有哪位明白的請(qǐng)不吝指教。