前言:
Category在Objc中非常重要,在平時的iOS的面試中針對Category的問題更是層出不窮,如:1)Category中的方法加載順序?2)Category中的方法“覆蓋”的原理?3)Category能添加屬性嗎?等等。更深入的可能會問Category中的方法是怎么加到方法列表中的?今天我們再來看看Category,還有沒有新的發(fā)現(xiàn)。
一、從runtime看Category的加載
_objc_init 作為OC的入口,主要的工作是注冊dyld的回調(diào),當(dāng)dyld中的image(鏡像)卸載,加載狀態(tài)(符號綁定完,靜態(tài)初始化完)發(fā)生變化時給與OC回調(diào)。
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();
// Register for unmap first, in case some +load unmaps something
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_2_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
Category的加載發(fā)生在map_2_images回調(diào)函數(shù)中,即動態(tài)鏈接器(dyld)完成符號綁定時的dyld_image_state_bound的回調(diào)。這里我們省略掉中間的調(diào)用,最后在void _read_images(header_info **hList, uint32_t hCount) 中讀?。?/p>
void _read_images(header_info **hList, uint32_t hCount)
{
//...
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
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
/* || 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);
}
}
}
}
//...
}
對于Category,_read_images做了如下事情:
1.通過_getObjc2CategoryList獲取Categorylist;
2.將Category中的實例方法,協(xié)議,屬性通過addUnattachedCategoryForClass映射到對應(yīng)的類上,然后通過remethodizeClass添加到對應(yīng)的類;
3.同上,將Category中的類方法,協(xié)議添加到類的元類上;
后續(xù)添加方法的實現(xiàn)在void attachLists(List* const * addedLists, uint32_t addedCount)中,大家應(yīng)該比較熟悉了。
我們發(fā)現(xiàn),Category是從_getObjc2CategoryList拿到的,我們看下它的實現(xiàn):
#define GETSECT(name, type, sectname) \
type *name(const headerType *mhdr, size_t *outCount) { \
return getDataSection<type>(mhdr, sectname, nil, outCount); \
} \
type *name(const header_info *hi, size_t *outCount) { \
return getDataSection<type>(hi->mhdr, sectname, nil, outCount); \
}
// function name content type section name
//...
GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
//...
很顯然,_getObjc2CategoryList的作用就是從Mach-O文件的__objc_catlist段獲取Category的數(shù)據(jù)。
好了,通過Runtime的源碼我們不難理解Category的"加載"過程:當(dāng)動態(tài)鏈接器(dyld)完成符號的綁定后,通過dyld_register_image_state_change_handler的回調(diào)到Objc,Objc通過_getObjc2CategoryList從Mach-O文件的__objc_catlist段獲取Category的數(shù)據(jù),然后先通過addUnattachedCategoryForClass把Category映射到對應(yīng)的類上,最后通過remethodizeClass添加到對應(yīng)的類。
二、就這?
從本文的題目不能猜出,Category應(yīng)該還不止這些,實際上關(guān)于Category的原理我在N年前就讀過源碼。甚至產(chǎn)生過疑慮:Category中的方法在啟動時添加明明會消耗性能,為什么不能在編譯/鏈接期間就完成呢,蘋果沒這么傻吧...直到在研究Mach-O文件格式時有了點新的發(fā)現(xiàn),下面我們通過Demo來看下,為了減少其他因素的干擾,我們創(chuàng)建一個最簡單的MacOS的控制臺程序,工程中創(chuàng)建一個Person類和它的分類。
//Person.h
@interface Person : NSObject
- (void)go;
@end
//Person+test.h
@interface Person (test)
- (void)goTest;
@end
運行下,使用MachOView看下Mach-O中的數(shù)據(jù):

_objc_catlist中居然是空的,我們新增的Category方法goTest并沒有保存在這里。Category的數(shù)據(jù)保存在哪里了呢?我們再看下保存類方法的__objc_const段:

從上圖我們發(fā)現(xiàn):Category中的方法已經(jīng)和主類中的方法合并到方法列表中了并且內(nèi)存地址連續(xù),同時Category中的方法還"規(guī)規(guī)矩矩"的放在了主類方法的前面。如果是個同名方法呢?

嗯...沒錯,和Category的特性一致...方法"覆蓋"這時已經(jīng)發(fā)生。
那么,這個優(yōu)化發(fā)生在編譯期還是靜態(tài)鏈接期間呢?很簡單,我們只需要把工程的類型Mach-O Type改成Static Libaray,因為靜態(tài)庫是經(jīng)過編譯,但還沒鏈接的中間產(chǎn)物,同樣通過MachOView看下數(shù)據(jù):

上圖可知:Category和它的主類分別生成了一個.o目標(biāo)文件,Category的信息還沒有合并。從這里我們可以看出優(yōu)化過程發(fā)生在靜態(tài)鏈接期間,了解靜態(tài)鏈接的同學(xué)可能會更容易理解,因為那時候才是真正給符號分配虛擬內(nèi)存地址的時候。
好了,既然Category的方法保存到了__objc_const段,那_objc_catlist是擺設(shè)?或者說什么情況下會保存到_objc_catlist呢?我們可以想一下:既然靜態(tài)鏈接的時候會進(jìn)行優(yōu)化,那么有沒有什么情況給類添加Category的時候已經(jīng)過了靜態(tài)鏈接的時期呢?這種情況會不會保存到_objc_catlist呢?
答案是:動態(tài)庫。
動態(tài)庫是已經(jīng)經(jīng)過鏈接(靜態(tài)鏈接)后的產(chǎn)物,如果給動態(tài)庫中的某個類添加Category應(yīng)該不能優(yōu)化了吧...這個猜想非常好驗證,我們只需要給系統(tǒng)庫中的類添加Category即可。
@interface NSString (Trim)
- (NSString *)trim;
@end
我們給NSString類添加trim方法,然后看下Mach-O文件:

果然,_objc_catlist已經(jīng)有數(shù)據(jù)了,但是_objc_catlist中沒有直接保存Category,真正的數(shù)據(jù)保存在_objc_const的Objc2 Caterory段中。
三、還有?
本以為這就結(jié)束了,多謝 @皮拉夫大王在此的提示:“分類未實現(xiàn)load和實現(xiàn)了load方法后保存是不一樣的”。我們知道在load方法的調(diào)用時會先調(diào)用當(dāng)前鏡像中的所有類的load方法,然后再調(diào)用分類的load方法,調(diào)用是分開的。再者,load方法作為特殊的存在,如果像上文所述進(jìn)行優(yōu)化的話,比較困難:如果優(yōu)化了怎么方便的調(diào)用呢?這種情況蘋果索性新增了一塊區(qū)域保存,我們看下runtime中獲取分類中l(wèi)oad方法的代碼:
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t * const *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
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
_getObjc2NonlazyCategoryList的實現(xiàn):
GETSECT(_getObjc2NonlazyCategoryList, category_t * const, "__objc_nlcatlist");
實現(xiàn)了load方法的Caterory在__objc_nlcatlist和__objc_catlist中都保存一份:

總結(jié)
從上面的分析我們可以看出蘋果對Caterory的優(yōu)化已經(jīng)非常細(xì)致:對于非動態(tài)庫中的類的Caterory中的方法會在靜態(tài)鏈接期間優(yōu)化,知道這個特性后我們可以在自己的模塊內(nèi)使用Caterory干更多的事情(比如功能解耦),不用擔(dān)心會影響啟動速度。最后在這里再拋出一個問題:蘋果既然對Caterory中的方法進(jìn)行了優(yōu)化,那其他的特性(如"屬性")呢?