參考apple開發(fā)文檔文檔地址我們可以看到load方法定義如下:
Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.
// 當一個類/分類被加入到runtime時調(diào)用,另外實現(xiàn)這個方法可以在加載時做一些特殊操作
該方法的具體描述和一些需要特別注意的特性如下:
The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.
// load方法在類和分類中都會被動態(tài)執(zhí)行和靜態(tài)鏈接,但是只有在類/分類中實現(xiàn)load方法該方法才能被調(diào)用
The order of initialization is as follows:
// 具體的執(zhí)行順序如下:
All initializers in any framework you link to.
// 調(diào)用所有你鏈接的framework
All +load methods in your image.
// 調(diào)用所有l(wèi)oad方法
All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.
// 調(diào)用C++的靜態(tài)初始化方及C/C++中的構(gòu)造方法
All initializers in frameworks that link to you.
// 調(diào)用所有鏈接到目標文件的framework中的初始化方法
In addition:
A class’s +load method is called after all of its superclasses’ +load methods.
// 一個類的load方法在父類的load方法調(diào)用之后才會被調(diào)用
A category +load method is called after the class’s own +load method.
// 一個分類的load方法只有在被分類的原來的類的load方法被調(diào)用之后調(diào)用
In a custom implementation of load you can therefore safely message other unrelated classes from the same image, but any load methods implemented by those classes may not have run yet.
// 在自定義load方法中,可以安全地向同一二進制包中(其他庫中的類)的其它無關(guān)的類發(fā)送消息,但接收消息的類中的load方法可能尚未被調(diào)用。
下面我們就來驗證一下上述描述中標注的特性。
我在項目中創(chuàng)建了Catamount類(父類),Cat類(子類1)和Tiger類(子類2)均繼承自Catamount類,另外創(chuàng)建了Catamount (Test1),Catamount (Test2),Cat (Test1),Tiger (Test1)四個分類,其每個類的具體實現(xiàn)如下:
@implementation Catamount
- (void)load {
NSLog(@"Catamount -- load");
}
@end
@implementation Cat
+ (void)load {
NSLog(@"Cat -- load");
}
@end
@implementation Tiger
@end
@implementation Catamount (Test1)
+ (void)load {
NSLog(@"Catamount-Test1 -- load");
}
@end
@implementation Catamount (Test2)
+ (void)load {
NSLog(@"Catamount-Test2 -- load");
}
@end
@implementation Cat (Test1)
@end
@implementation Tiger (Test1)
+ (void)load {
NSLog(@"Tiger-Test1 -- load");
}
@end
運行項目,執(zhí)行結(jié)果如下:
2018-05-23 11:11:25.820888+0800 debug-objc[1904:186605] Catamount -- load
2018-05-23 11:11:27.116243+0800 debug-objc[1904:186605] Cat -- load
2018-05-23 11:11:28.244515+0800 debug-objc[1904:186605] Catamount-Test2 -- load
2018-05-23 11:11:29.860360+0800 debug-objc[1904:186605] Catamount-Test1 -- load
2018-05-23 11:11:33.083241+0800 debug-objc[1904:186605] Tiger-Test1 -- load
通過上述實踐可知:
1.對比cat類和Catamount類中l(wèi)oad的執(zhí)行順序可知一個類的load方法在父類的load方法調(diào)用之后才會被調(diào)用
2.對比Tiger類和Cat (Test1)類的實現(xiàn)可知只有在類/分類中實現(xiàn)load方法該方法才能被調(diào)用
3.對比Catamount (Test1)類和Catamount (Test2)類 與Catamount類中的load的執(zhí)行書序可知一個分類的load方法只有在被分類的原來的類的load方法被調(diào)用之后調(diào)用
4.對比Catamount (Test1)類和Catamount (Test2)類中l(wèi)oad的執(zhí)行順序可知分類的load的執(zhí)行順序不確定(網(wǎng)上有說法是一個類的多個分類的load執(zhí)行順序與其在Compile Sources中出現(xiàn)的順序一致,并為深究,讀者可自行驗證)
所以回到我們剛開始提出的問題:
【Q1】普通類/Category,load方法執(zhí)行邏輯。多個分類中有l(wèi)oad方法是怎么處理的?
【A1】load方法在類/子類/分類中的執(zhí)行順序由上述描述已可知。
而對于Tiger (Test1)類,該類的被分類的原來的類Tiger類并未實現(xiàn)load方法,但是Tiger (Test1)類中的load方法正常執(zhí)行,所以本文認為一個分類的load方法是否執(zhí)行,與其原本類是否有實現(xiàn)load方法并無關(guān)系,因為load方法的描述本就為:Invoked whenever a class or category is added to the Objective-C runtime,所以也就意味著一個類/分類被鏈接到項目中的時候,只要該類/分類實現(xiàn)了load方法那么該方法就會被調(diào)用(The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.)
【Q2】為什么系統(tǒng)在調(diào)用load方法的時候不存在方法覆蓋問題
【A2】查閱源碼可以發(fā)現(xiàn)系統(tǒng)調(diào)用load方法的具體方法體如下:
void call_load_methods(void)
{
loadMethodLock.assertLocked();// 加鎖
...
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);
...
}
其中主要調(diào)用了call_class_loads()和call_category_loads()方法,而這兩個方法的執(zhí)行l(wèi)oad方法的核心代碼如下:
?
// 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;
// PrintLoading, OBJC_PRINT_LOAD_METHODS, "log calls to class and category +load methods"
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}
可以看出call_load_methods里是通過load方法的地址直接調(diào)用的load方法,而不是通過消息機制來調(diào)用的,所以分類中的load方法并不會覆蓋主類以及其他同主類的分類里的load 方法實現(xiàn)。
Tip
【1】當類被引用進項目的時候就會執(zhí)行l(wèi)oad函數(shù)(在main函數(shù)開始執(zhí)行之前),與這個類是否被用到無關(guān),所以其運行環(huán)境有不確定因素,可能并不能保證所有類都加載完成且可用。然而也正因為load方法調(diào)用在main函數(shù)之前所以我們不應該在load方法中做耗時操作,以避免程序啟動時間過長的情況
【2】load方法使用了鎖來保證線程安全,所以應該避免線程阻塞在load方法.
Category/Extension/Protocol的實現(xiàn)原理
Category
Category的主要作用是它可以在不改變原來類的基礎(chǔ)上,為類動態(tài)的添加方法。分類的使用應該注意以下特點:
1.分類中只能增加方法,不能增加屬性
2.在分類方法中可以訪問原來類的.h文件中聲明的屬性和方法
3.分類可以重新實現(xiàn)原來類中的方法,但是會覆蓋掉原來方法,所以在開發(fā)中盡量避免同名方法覆蓋
4.方法調(diào)用的優(yōu)先級:分類>原來的類>父類,如包含多個分類,則調(diào)用優(yōu)先級與編譯順序有關(guān),最后參與編譯的分類優(yōu)先
那么Category具體實現(xiàn)原理是什么呢?其實很簡單,就是系統(tǒng)在編譯時將Category的方法列表加入到了原有類的方法列表之上。
我們先來看一下Category的結(jié)構(gòu)體聲明:
struct category_t {
const char *name;// 名稱
classref_t cls;// 類
struct method_list_t *instanceMethods;// 實例方法列表
struct method_list_t *classMethods;// 類方法列表
struct protocol_list_t *protocols;//協(xié)議列表
struct property_list_t *instanceProperties;//實例屬性列表
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;//類屬性列表
// 如果是元類,就返回類方法列表;否則返回實例方法列表
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
// 如果不是元類,則返回實例屬性列表;若hi參數(shù)有分類類屬性,返回類屬性列表;否則即為元類返回 nil,因為元類沒有屬性
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
其中就包括了方法等列表項
在OC代碼運行時,系統(tǒng)經(jīng)歷了一下幾個方法
objc-os.mm類中的_objc_init方法
——>objc-os.mm類中的map_images方法
————>objc-os.mm類中的map_images_nolock方法
——————>objc-runtime-new.mm類中的_read_images方法
————————>objc-runtime-new.mm類中的remethodizeClass方法
——————————>objc-runtime-new.mm類中的attachCategories方法
然后實現(xiàn)了將所有category的實例方法列表拼成了一個大的實例方法列表,所以最終結(jié)果是category的方法被放到了新方法列表的前面,而原來類的方法被放到了新方法列表的后面。需要注意:
1.category的方法沒有“完全替換掉”原來類已經(jīng)有的方法,也就是說如果category和原來類都有methodA,那么category附加完成之后,類的方法列表里會有兩個methodA
2.我們平常所說的category的方法會“覆蓋”掉原來類的同名方法,實際上由一可知:這是因為運行時在查找方法的時候是順著方法列表的順序查找的,它只要一找到對應名字的方法即返回imp,也就實現(xiàn)了所謂的“方法覆蓋”。
Extension
Extension一般用來隱藏類的私有信息,其定義與Category相似,但是它不僅可以擴展原有類的方法,也可以擴充原有類的屬性。此外需注意:定義在 .m 文件中的類擴展方法為私有的,定義在 .h 文件(頭文件)中的類擴展方法為公有的。類擴展是在 .m 文件中聲明私有方法的非常好的方式。
Protocol
Protocol是一種特殊的程序設(shè)計結(jié)構(gòu),專門用來聲明被別的類實現(xiàn)的方法,也就提供了一個可被其他類實現(xiàn)的通信接口,它具有以下特點:
1.一個類可以同時遵循多個協(xié)議
2.協(xié)議本身也可以遵循其他協(xié)議
3.只要父類遵守了某個協(xié)議,那么子類也遵守
需要注意的是delegate一般用weak修飾,而不用strong修飾,是為了避免對象一直被持有導致無法釋放,也就是為了防止循環(huán)引用。
而實際上Protocol也是一個結(jié)構(gòu)體:struct protocol_t,Runtime提供了Protocol的一系列函數(shù)操作,如下所示:
// 返回指定的協(xié)議
Protocol * objc_getProtocol ( const char *name );
// 獲取運行時所知道的所有協(xié)議的數(shù)組
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 創(chuàng)建新的協(xié)議實例
Protocol * objc_allocateProtocol ( const char *name );
// 在運行時中注冊新創(chuàng)建的協(xié)議
void objc_registerProtocol ( Protocol *proto ); //創(chuàng)建一個新協(xié)議后必須使用這個進行注冊這個新協(xié)議,但是注冊后不能夠再修改和添加新方法。
// 為協(xié)議添加方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 添加一個已注冊的協(xié)議到協(xié)議中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 為協(xié)議添加屬性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 返回協(xié)議名
const char * protocol_getName ( Protocol *p );
// 測試兩個協(xié)議是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
// 獲取協(xié)議中指定條件的方法的方法描述數(shù)組
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );
// 獲取協(xié)議中指定方法的方法描述
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 獲取協(xié)議中的屬性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 獲取協(xié)議的指定屬性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 獲取協(xié)議采用的協(xié)議
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
// 查看協(xié)議是否采用了另一個協(xié)議
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );
參考鏈接:
NSObject的load和initialize方法
iOS類方法load和initialize詳解
iOS category內(nèi)部實現(xiàn)原理
Objc Runtime 深入學習類,對象,Method,消息,Protocol,Category和Block的底層結(jié)構(gòu)和運行時操作函數(shù)