Objective-C之Category的底層實作原理
Objective-C的+initialize方法調用原理分析
别人是這麼說的
- 調用時機:
方法會在Runtime加載類對象(+load
)和分類(class
)的時候調用category
- 調用頻率: 每個類對象、分類的
方法,在工程的整個生命周期中隻調用一次+load
- 調用順序:
先調用類對象(
)的class
方法:+load
- 類對象的
調用順序是按照 類檔案的編譯順序 進行先後調用;load
- 調用子類
之前會先調用父類的+load
方法+load
)的+load方法:按照編譯先後順序調用(先編譯的,先被調用)category
- 類對象的
一、load方法的調用時機和調用頻率
+load
方法是在程式一啟動運作,加載鏡像中的類對象(
class
)和分類(
category
)的時候就會調用,隻會調用一次,不論在項目中有沒有用到該類對象或者該分類,他們統統都會先被加載進記憶體,因為類的加載隻有一次,是以所有的load方法肯定都會被調用而且隻有一次。下面先上一個小demo調試看看:
上圖裡面,我建立了一個
person
類,以及它的兩個分類–
CLPerson+Test/CLPerson+Test2
,然後給它們都加上兩個類方法(
+load/+test
),
main.h
裡面先不加任何代碼跑跑看。
從日志看出,雖然整個工程都沒有import過
CLPerson
以及它的兩個分類,但是他們的
load
方法還是被調用了,并且都發生在
main
函數開始之前,而且
+test
并沒有被調用。是以該現象間接證明了,
load
方法的調用應該和類對象以及分類的加載有關。
在
main.h
裡面調一下
+test
方法
接下來通過源碼分析一下(Runtime源碼下載下傳位址)
首先,進入Runtime的初始化檔案
objc-os.mm
,找到
_objc_init
函數,該函數可以看作是Runtime的初始化函數。
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
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();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
忽略一些與本文主題關聯不太的函數,直接看最後句
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
其中很明顯,
load_images
就是加載鏡像/加載子產品的意思,應該是與我們話題相關的參數,點進去看看它的實作
/***********************************************************************
* load_images
* Process +load in the given images which are being mapped in by dyld.
*
* Locking: write-locks runtimeLock and loadMethodLock
**********************************************************************/
extern bool hasLoadMethods(const headerType *mhdr);
extern void prepare_load_methods(const headerType *mhdr);
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
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
蘋果對該函數官方給出的注釋是,處理那些正在進行映射的鏡像(images)的+load方法。該方法的實作裡面,做了兩件事情:
-
// Discover load methods – 查找并準備load方法,以供後面去調用prepare_load_methods
-
//Call +load methods – 調用這些load方法call_load_methods();
針對上面案例日志中出現的現象,先從結果出發,逆向分析,來看看load方法是如何調用的,進入
call_load_methods();
的實作
/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.
調用所有的進行中的class和category的+load方法;
* Class +load methods are called superclass-first.
class的+load方法會被先調用,并且,一個調用一個class的+load方法前,會先對其父類的+load進行調用
* Category +load methods are not called until after the parent class's +load.
category的+load方法的調用,會發生在所有的class的+load方法完成調用之後。
*
* This method must be RE-ENTRANT, because a +load could trigger
* more image mapping. In addition, the superclass-first ordering
* must be preserved in the face of re-entrant calls. Therefore,
* only the OUTERMOST call of this function will do anything, and
* that call will handle all loadable classes, even those generated
* while it was running.
*
* The sequence below preserves +load ordering in the face of
* image loading during a +load, and make sure that no
* +load method is forgotten because it was added during
* a +load call.
* Sequence:調用順序
* 1. Repeatedly call class +loads until there aren't any more
周遊所有的class對象,調用它們的+load方法,知道所有class中的+load都完成了調用
* 2. Call category +loads ONCE.
調用所有category中的+load方法
* 3. Run more +loads if:
這裡我還不太了解,感覺上面都已經把所有的+load調用完了,還不太了解哪裡會産生新的+load方法。有待繼續補充......
* (a) there are more classes to load, OR
* (b) there are some potential category +loads that have
* still never been attempted.
* Category +loads are only run once to ensure "parent class first"
* ordering, even if a category +load triggers a new loadable class
* and a new loadable category attached to that class.
*
* Locking: loadMethodLock must be held by the caller
* All other locks must not be held.
**********************************************************************/
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;
}
很明顯,核心邏輯在do-while循環裡面,循環中面做了兩件事:
- 首先調用類對象的
方法–+load
,直到可加載的類的計數器減到0 –call_class_loads();
。loadable_classes_used > 0
- 然後調用分類的
方法--+load
call_category_loads();//Call category +loads ONCE
小結A – 程式啟動之後,Runtime會在鏡像加載階段,先調用所有類對象的方法,然後在調用所有分類的
+load
方法,類對象與分類之間參與編譯順序,不會影響上面的結論。例如下圖的調試,注意編譯順序
+load
這裡産生了一個新的疑問:既然是方法調用,為什麼
category
的
+load
方法沒有“覆寫”類對象的
+load
方法呢?
有關分類( category
)中的方法對類對象中的同名方法産生的“覆寫”現象如果還不太清楚,請參考我的Objective-C之Category的底層實作原理一文。
接着上面的源碼,繼續看看Runtime對于類對象和分類
+load
到底是如何調用的。我們先檢視
call_class_loads();
,這是對所有類對象(
class
)的
+load
方法的調用邏輯
/***********************************************************************
* 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;//首先用局部變量loadable_class儲存loadable_classes清單
int used = loadable_classes_used;//在用局部變量used儲存loadable_classes_used
loadable_classes = nil;//将loadable_classes置空
loadable_classes_allocated = 0;//将loadable_classes_allocated清零
loadable_classes_used = 0;//将loadable_classes_used清零
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {//周遊classes清單
Class cls = classes[i].cls;//從清單成員裡面獲得cls
load_method_t load_method = (load_method_t)classes[i].method;//從清單成員擷取對應cls的+load 的IMP(方法實作)
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);//這裡就是對+load方法的調用,注意哦,這是直接的函數調用,不是消息機制那種哦,這裡跟類的方法清單什麼沒關系,直接就是通過+load的IMP進行調用了
}
// Destroy the detached list.
if (classes) free(classes);
}
上面實作的主要邏輯發生在for循環裡面,該for循環周遊了一個叫
classes
的清單,該清單存儲的是一堆
loadable_class
結構體,
loadable_class
的定義如下
struct loadable_class {
Class cls; // may be nil
IMP method;
};
每一個
struct loadable_class
變量,存儲的應該就是
一個類對象
+
一個與該類相關的方法實作
。從
loadable_class
這個命名,說明它内部的資訊肯定是表示一個可以被加載的類的相關資訊,是以合理推斷,它裡面的
method
應該就是類的
+load
方法,
cls
就是這個
+load
方法所對應的類對象。這個推斷是否正确,我們一會讨論。
我們再看看源碼中對于
classes
這個數組進行周遊時到底做了什麼。很簡單,就是通過函數指針
load_method
從
loadable_class
中獲得
+load
方法的
IMP
作為其參數,然後就直接對其進行調用
(*load_method)(cls, SEL_load);
,是以,類對象的
+load
方法的調用實際上就發生在這裡。這裡的for循環一旦結束,
classes
所包含的所有類對象的
+load
方法就會被依次調用,這跟一個類是否被在工程項目裡被執行個體化過,是否接受過消息,沒有半毛錢關系。
至此,Runtime對于
+load
方法是如何調用的問題我們分析了一半,弄清楚了類對象的
+load
方法的是怎麼被一個一個調用的,也就是
static void call_class_loads(void)
這個函數,接下來,還有問題的另一半–
static bool call_category_loads(void)
,也就是關于分類的
+load
方法的調用。進入其中
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.
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;
}
}
// Compact detached list (order-preserving)
shift = 0;
for (i = 0; i < used; i++) {
if (cats[i].cat) {
cats[i-shift] = cats[i];
} else {
shift++;
}
}
used -= shift;
// Copy any new +load candidates from the new list to the detached list.
new_categories_added = (loadable_categories_used > 0);
for (i = 0; i < loadable_categories_used; i++) {
if (used == allocated) {
allocated = allocated*2 + 16;
cats = (struct loadable_category *)
realloc(cats, allocated *
sizeof(struct loadable_category));
}
cats[used++] = loadable_categories[i];
}
// Destroy the new list.
if (loadable_categories) free(loadable_categories);
// Reattach the (now augmented) detached list.
// But if there's nothing left to load, destroy the list.
if (used) {
loadable_categories = cats;
loadable_categories_used = used;
loadable_categories_allocated = allocated;
} else {
if (cats) free(cats);
loadable_categories = nil;
loadable_categories_used = 0;
loadable_categories_allocated = 0;
}
if (PrintLoading) {
if (loadable_categories_used != 0) {
_objc_inform("LOAD: %d categories still waiting for +load\n",
loadable_categories_used);
}
}
return new_categories_added;
}
我們可以看到,這個方法的實作裡面,通過系統注釋,被劃分如下幾塊:
- A –
.分離可加載// Detach current loadable list
清單,也就是把可加載清單的資訊儲存到本函數的局部變量cats數組上。category
- B –
.消費// Call all +loads for the detached list
裡面的所有cats
方法(也就是調用它們)+load
- C –
清理// Compact detached list (order-preserving)
裡面已經被消費過的成員,并且更新cats
計數值used
- D –
如果又出現了新的可加載的分類,将其相關内容複制到// Copy any new +load candidates from the new list to the detached list.
清單上。cats
- E –
銷毀清單(這裡指的是外部的// Destroy the new list.
變量)loadable_categories
- F –
// Reattach the (now augmented) detached list. But if there's nothing left to load, destroy the list.
更新幾個記錄了category+load資訊的幾個全局變量。
相比較于
方法,這裡多了步驟C、D、F。關于A、B、E這三個步驟,因為跟call_class_loads
call_class_loads
方法裡面實作是一樣的,不作重複解釋。且看看多出來的這幾步
先看C
對于這個,我畫個圖示範一下,就明白了 其實我感覺消費完一輪+load方法之後,cats裡面基本上會在這個步驟被清空。
然後我們看看D步驟,如下圖
其實主要任務就是把新的可以加載的分類(
category
)資訊(如果此時發現還有的話)添加到本函數的
cats
數組上。
最後看看F步驟
小結B –對于
Runtime
方法的調用,不是走的我們熟悉的“消息發送”路線,而是直接拿到
+load
方法的
+load
,直接調用。是以不存在所謂“類的方法被
IMP
的方法覆寫”的問題,是以除了
category
的 類與分類的
結論A
方法先後調用順序外,我們看到類與它的分類的所有的
+load
全部都被調用了,沒有被覆寫。
+load
目前,我們确定了類對象的
+load
方法會先于分類的
+load
方法被調用,并且不存在覆寫現象。
- 那麼對于類于類之間
調用順序是怎樣的?+load
- 同樣的疑問對于分類(
category
)又是如何呢?
這兩個問題,我們就需要進入
方法的實作,看看prepare_load_methods
方法被調用前,Runtime是如何準備它們的。+load
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
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);
}
}
上面的實作裡,
classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count);
可以看出,利用系統提供的函數
_getObjc2NonlazyClassList
,獲得類對象的清單,因為這是系統級别的函數,應該跟編譯過程的順序有關,這裡先推測
classlist
中類的順序與類的編譯順序相同。
接下來,就是周遊
classlist
,對其每個成員通過函數
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);
/** <#注釋标題#>✈️✈️✈️✈️✈️
将cls添加到loadable_classes數組的最後面
*/
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
該函數裡面就做兩件事:
- 先遞歸調用自身(
),對目前類(也就是函數傳入的參數)的父類進行處理schedule_class_load()
- 處理完父類之後,将目前類對象加入到可加載類的相關清單當中
add_class_to_loadable_list(cls);
經過這樣的整理之後,最終整理過的裝載類對象相關資訊的數組中,父類應該排在子類前面。而不同的類對象之間在數組中的位置,就可以參考它們.m的編譯順序來看了。
每個類對象在被加入數組的時候,會通過
cls->setInfo(RW_LOADED);
設定标簽标記一下,這樣,如果該類下次被作為父類進行遞歸調用的時候,就不會重複加入到清單中,保證一個類在數組中隻出現一次。
最後再看一下
add_class_to_loadable_list(cls);
裡面的邏輯
每個步驟的作用請看圖中的注釋。請注意其中一個細節,第三句代碼
method = cls->getLoadMethod();
,進一步檢視一下這個
getLoadMethod
很明顯這個方法就是從一個類對象裡面尋找load方法實作,找到的話,就傳回load方法的IMP,指派給method。
然後把該類對象
cls
和對應的+load方法IMP
method
指派給
loadable_classes
清單最後一個成員,該成員我們前面篇章已經說了,該成員就是
loadable_class
struct loadable_class {
Class cls; // may be nil
IMP method;
};
我們之前推測的說
loadable_class
裡面存放的
IMP method;
應該就是
+load
方法的
IMP
,通過上面的分析,證明确實如此。
上面的是針對類對象的
+load
的方法所進行的調用前的整理排布。下面我們看一下分類的
+load
方法是如何處理的。回到
prepare_load_methods
方法,這裡我直接貼出相關部分代碼截圖
可以看到,并沒像類一樣,用一個
schedule
方法進行遞歸處理,而是直接通過系統函數
_getObjc2NonlazyCategoryList
拿到分類的集合
categorylist
,因為對分類來說,不存在誰是誰的父類,大家都是平級的,而且之前類對象的
+load
方法已經處理過準備好了,是以這裡,隻需将
categorylist
裡面的分類對象一個一個拿出來,通過
add_category_to_loadable_list
方法處理好,一個一個加入到我們後面調用
+load
方法時所用的
loadable_categories
數組裡面。
add_category_to_loadable_list(cat)
方法跟上面
add_class_to_loadable_list(cls);
方法裡面的邏輯完全一緻,不做重複解讀。至此,
+load
方法的調用前的前期準備工作,分析完了。
小結C
- 那麼對于類于類之間
+load
調用順序是怎樣的?
調用一個類對象的
方法之前,會先調用其父類的
+load
方法(如果存在的話),類與類之間,會按照編譯的順序,先後調用其
+load
方法。一個類對象的
+load
方法不會被重複調用,隻可能被調用一次。
+load
- 同樣的疑問對于分類(
category
)又是如何呢?
分類的
方法,會按照分類參與編譯的順序,先編譯的,先被調用。
+load
我們在通過代碼來驗證一波。在開篇案例裡面,我繼續添加幾個類和分類,
CLTeacher
(
CLPerson
子類)、
CLTree
(
NSObject
子類)、
CLRiver
(
NSObject
子類)、以及
CLTeacher
的兩個分類。
- 首先看出,類對象的
方法肯定是先與所有分類的+load
方法被調用的。+load
- 分類之間是按照編譯的順序,先後調用
。+load
-
、CLTeacher
、CLTree
也是按照編譯的順序,先後調用CLRiver
,由于+load
是CLPerson
的父類,是以會先用它調用CLTeacher
至此,完全和上面的小結C吻合。如果你有興趣,可以自己嘗試一下,變換一下源檔案的編譯順序,結果和這裡的結論都是一緻的。+load
到這裡,關于
+load
方法調用的細節應該就算分析完了~~~
PS:對于蘋果的源碼,我也在不斷的研讀和學習中,如果文中有闡述不對的地方,煩請告知指正,于此與各位共勉~~
參考來源
- Objective-C Class Loading and Initialization
- 你真的了解load方法麼
- MJ小碼哥 OC底層原理班