一、前言
在iOS的開發(fā)中,Runtime的方法交換都是寫在+load
之中硫痰,為什么不是+initialize
中呢?可能不少朋友對此或多或少有一點(diǎn)點(diǎn)的疑問窜护。 我們知道:OC中幾乎所有的類都繼承自NSObject效斑,而+load
和+initialize
用于類的初始化,這兩者的區(qū)別和聯(lián)系到底何在呢柱徙?接下來我們一起來看看這二者的區(qū)別缓屠。
二、+load
根據(jù)官方文檔的描述:
-
+load
是當(dāng)一個類或者分類被添加到Objective-C運(yùn)行時調(diào)用的护侮。 - 本類
+load
的調(diào)用在其所有的父類+load
調(diào)用之后敌完。 - 分類的
+load
在類的調(diào)用之后。
也就是說調(diào)用順序:父類 > 類 > 分類
但是不同類之間的+load方法的調(diào)用順序是不確定的羊初。
由于+load
是在類第一次加載進(jìn)Runtime運(yùn)行時才調(diào)用滨溉,由此我們可以知道:
- 它只會調(diào)用一次,這也是為什么么方法交換寫在
+load
的原因长赞。 - 它的調(diào)用時機(jī)在
main
函數(shù)之前晦攒。
三、+load的實現(xiàn)
在Runtime源碼的objc-runtime-new.mm
和objc-runtime-old.mm
中的load_images
方法中都存在這關(guān)鍵代碼:
prepare_load_methods((const headerType *)mh);
和
call_load_methods();
3.1 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]));
}
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);
}
}
這里就是準(zhǔn)備好滿足 +load 方法調(diào)用條件的類和分類得哆,而對class和category分開做了處理脯颜。
- 在處理class的時候,調(diào)用了
schedule_class_load
:
/***********************************************************************
* 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);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
從倒數(shù)第三句代碼看出這里對參數(shù)class的父類進(jìn)行了遞歸調(diào)用贩据,以此確保父類的優(yōu)先級
然后調(diào)用了add_class_to_loadable_list
,把class加到了loadable_classes中:
/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
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));
}
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
- 對于分類則是調(diào)用
add_category_to_loadable_list
把category加入到loadable_categories之中:
/***********************************************************************
* add_category_to_loadable_list
* Category cat's parent class exists and the category has been attached
* to its class. Schedule this category for +load after its parent class
* becomes connected and has its own +load method called.
**********************************************************************/
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));
}
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
3.2 call_ load_methods
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;
}
從注釋我們可以明確地看出:
- 重復(fù)地調(diào)用class里面的
+load
方法 - 一次調(diào)用category里面的
+load
方法
還是看一下調(diào)用具體實現(xiàn)栋操,以call_class_loads
為例:
/***********************************************************************
* 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;
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_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
這就是真正負(fù)責(zé)調(diào)用類的 +load 方法了。它從上一步獲取到的全局變量 loadable_classes 中取出所有可供調(diào)用的類乐设,并進(jìn)行清零操作讼庇。
-
loadable_classes
指向用于保存類信息的內(nèi)存的首地址, -
loadable_classes_allocated
標(biāo)識已分配的內(nèi)存空間大小近尚, -
loadable_classes_used
則標(biāo)識已使用的內(nèi)存空間大小蠕啄。
然后,循環(huán)調(diào)用所有類的 +load 方法。注意歼跟,這里是(調(diào)用分類的 +load 方法也是如此)直接使用函數(shù)內(nèi)存地址的方式(*load_method)(cls, SEL_load)
; 對 +load 方法進(jìn)行調(diào)用的和媳,而不是使用發(fā)送消息 objc_msgSend 的方式。
這樣的調(diào)用方式就使得 +load 方法擁有了一個非常有趣的特性哈街,那就是子類留瞳、父類和分類中的 +load 方法的實現(xiàn)是被區(qū)別對待的。也就是說如果子類沒有實現(xiàn) +load 方法骚秦,那么當(dāng)它被加載時 runtime 是不會去調(diào)用父類的 +load 方法的她倘。同理,當(dāng)一個類和它的分類都實現(xiàn)了 +load 方法時作箍,兩個方法都會被調(diào)用硬梁。因此,我們常嘲茫可以利用這個特性做一些“有趣”的事情荧止,比如說方法交換method-swizzling。
四阶剑、+initialize
根據(jù)官方文檔initialize的描述:
- Runtime發(fā)送
+initialize
消息是在類或者其子類第一次收到消息時跃巡,而且父類會在類之前接收到消息 -
+initialize
的實現(xiàn)是線程安全的,多線程下會有線程等待 - 父類的
+initialize
可能會被調(diào)用多次
也就是說 +initialize 方法是以懶加載的方式被調(diào)用的牧愁,如果程序一直沒有給某個類或它的子類發(fā)送消息素邪,那么這個類的 +initialize 方法是永遠(yuǎn)不會被調(diào)用的。那這樣設(shè)計有什么好處呢递宅?好處是顯而易見的娘香,那就是節(jié)省系統(tǒng)資源,避免浪費(fèi)办龄。
五、+initialize的實現(xiàn)
在runtime源碼objc-runtime-new.mm
的方法lookUpImpOrForward
中有如下代碼片段:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
···
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
}
}
可以知道:在方法調(diào)用過程中淋昭,如果類沒有被初始化的時候俐填,會調(diào)用_class_initialize
對類進(jìn)行初始化,方法細(xì)節(jié)如下:
/***********************************************************************
* class_initialize. Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
void _class_initialize(Class cls)
{
assert(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
// Make sure super is done initializing BEFORE beginning to initialize cls.
// See note about deadlock above.
* supercls = cls->superclass;
* if (supercls && !supercls->isInitialized()) {
* _class_initialize(supercls);
* }
// Try to atomically set CLS_INITIALIZING.
{
monitor_locker_t lock(classInitLock);
if (!cls->isInitialized() && !cls->isInitializing()) {
cls->setInitializing();
reallyInitialize = YES;
}
}
if (reallyInitialize) {
// We successfully set the CLS_INITIALIZING bit. Initialize the class.
// Record that we're initializing this class so we can message it.
_setThisThreadIsInitializingClass(cls);
if (MultithreadedForkChild) {
// LOL JK we don't really call +initialize methods after fork().
performForkChildInitialize(cls, supercls);
return;
}
// Send the +initialize message.
// Note that +initialize is sent to the superclass (again) if
// this class doesn't implement +initialize. 2157218
if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: calling +[%s initialize]",
pthread_self(), cls->nameForLogging());
}
// Exceptions: A +initialize call that throws an exception
// is deemed to be a complete and successful +initialize.
//
// Only __OBJC2__ adds these handlers. !__OBJC2__ has a
// bootstrapping problem of this versus CF's call to
// objc_exception_set_functions().
// Exceptions: A +initialize call that throws an exception
// is deemed to be a complete and successful +initialize.
//
// Only __OBJC2__ adds these handlers. !__OBJC2__ has a
// bootstrapping problem of this versus CF's call to
// objc_exception_set_functions().
#if __OBJC2__
@try
#endif
{
callInitialize(cls);
if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
pthread_self(), cls->nameForLogging());
}
}
...
}
這源碼我們可以可出結(jié)論:
- 從前面*的行數(shù)知道翔忽,_class_initialize方法會對class的父類進(jìn)行遞歸調(diào)用英融,由此可以確保父類優(yōu)先于子類初始化。
- 在截出的代碼末尾有著如下方法:
callInitialize(cls);
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
這里指明了+initialize
的調(diào)用方式是objc_msgSend,它和普通方法一樣是由Runtime通過發(fā)消息的形式歇式,調(diào)用走的都是發(fā)送消息的流程驶悟。換言之,如果子類沒有實現(xiàn) +initialize 方法材失,那么繼承自父類的實現(xiàn)會被調(diào)用痕鳍;如果一個類的分類實現(xiàn)了 +initialize 方法,那么就會對這個類中的實現(xiàn)造成覆蓋。
因此笼呆,如果一個子類沒有實現(xiàn) +initialize 方法熊响,那么父類的實現(xiàn)是會被執(zhí)行多次的。有時候诗赌,這可能是你想要的汗茄;但如果我們想確保自己的 +initialize 方法只執(zhí)行一次,避免多次執(zhí)行可能帶來的副作用時铭若,我們可以使用下面的代碼來實現(xiàn):
+ (void)initialize {
if (self == [ClassName self]) {
// ... do the initialization ...
}
}
總結(jié)
+load | +initialize | |
---|---|---|
調(diào)用時機(jī) | 加載到runtime時 | 收到第一條消息時洪碳,可能永遠(yuǎn)不調(diào)用 |
調(diào)用方式(本質(zhì)) | 函數(shù)調(diào)用 | runtime調(diào)度(和普通的方法一樣,通過objc_msgSend) |
調(diào)用順序 | 父類 > 類 > 分類 | 父類 > 類 |
調(diào)用次數(shù) | 一次 | 不定叼屠,可能多次可能不調(diào)用 |
是否沿用父類的實現(xiàn) | 否 | 是 |
分類的中實現(xiàn) | 類和分類都執(zhí)行 | "覆蓋"類中的方法偶宫,只執(zhí)行分類的實現(xiàn) |
后記
應(yīng)該盡可能減少initialize的調(diào)用,節(jié)省資源环鲤,截取官方原文的供大家參考:
Because initialize is called in a blocking manner, it’s important to limit method implementations to the minimum amount of work necessary possible. Specifically, any code that takes locks that might be required by other classes in their initialize methods is liable to lead to deadlocks. Therefore, you should not rely on initialize for complex initialization, and should instead limit it to straightforward, class local initialization.