Objective-C的+load方法調用原理分析
Objective-C的+initialize方法調用原理分析
Category的使用場景
我個人粗淺理解,就是將一個類的實現,拆解成小的模塊,便于管理和維護。因為實際項目中,有些類的功能可能會非常復雜,導致一個類的代碼過多,這對后期修改和維護是比較不利的,所以category方便了程序員,可以根據功能,業務等形式的劃分,將類的一大堆方法分組放置以及調用。
有趣的思考
先來看一個最簡單的category結構,一下代碼定義了一個CLPerson
類 和它的一個category CLPerson+Test
// ******************** CLPerson
#import <Foundation/Foundation.h>
@interface CLPerson : NSObject
-(void)run;
@end
#import "CLPerson.h"
@implementation CLPerson
-(void)run
{
NSLog(@"CLPerson Run");
}
@end
// ******************** CLPerson+Test
#import "CLPerson.h"
@interface CLPerson (Test)
-(void)test;
@end
#import "CLPerson+Test.h"
@implementation CLPerson (Test)
-(void)test{
NSLog(@"Test");
}
@end
// ******************** CLPerson+Eat
#import "CLPerson.h"
@interface CLPerson (Eat)
-(void)eat;
@end
#import "CLPerson+Eat.h"
@implementation CLPerson (Eat)
-(void)eat{
NSLog(@"Eat");
}
@end
請問???:以下的兩個方法調用,底層到底發生了什么,它們本質是否相同?
CLPerson *person = [[CLPerson alloc]init];
[person run]; //類的實例方法調用
[person test];//分類的實例方法調用
[person eat];//分類的實例方法調用
我們都知道,[實例對象 方法]
這種寫法,經過底層轉換之后,實際上就是,objc_msgSend(類對象, @selector(實例方法))
,也就我們oc的一個基本概念,消息發送機制。因此,我們可以推定,[person run]
這句代碼,在消息發送機制下,首先會根據 person
的isa
指針找到CLPerson
的類對象,然后在類對象的方法列表(method_list_t * methods
)里面找到該方法的實現,然后進行調用。
接下來,你肯定會想
- 那么
[person test]
和[person eat]
呢?它的消息是發送給誰呢? - 是發送給
person
的類對象嗎? - 還是說,對于
CLPerson+Test.h
和CLPerson+Eat.h
來說,也有其獨立對應的分類對象呢?
帶著這些思考和問題,我們接下來一步一步地進行拆解。
Category的實現原理
底層結構——所有一切始于編譯
要想知道原理,不要猜,也不要輕易相信別人說的東西,自己驗證一下才是最靠譜的。在命令行下,進入CLPerson+Test.m
文件所在路徑執行以下命令-->
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc CLPerson+Test.m
得到編譯后的c++文件CLPerson+Test.cpp
,將其拖入xcode項目中進行查看,但是不要加入編譯列表,否則程序跑不起來。直接查看文件底部,就可以找到category相關的底層信息,請看下圖剖析
上圖比較粗糙,請諒解,但比文字描述來的更加直觀,上面基本上分析清楚了在編譯結束之后,category是以何種形式存在的,現在用文字來總結一下:
category經過編譯過程之后,系統為其定義了如下的一個結構體
//注意,編譯后的cpp文件一般比較長,會有好幾萬行,
//一般我們關注類結構相關的信息,都在最后,
//所以可以直接把文件拖到底,便可以找到這些信息
struct _category_t {
const char *name; //用來存放類名
struct _class_t *cls;
const struct _method_list_t *instance_methods;//用來存放category里面的實例方法列表
const struct _method_list_t *class_methods;//用來存放category里面的類方法列表
const struct _protocol_list_t *protocols;//用來存放category里面的協議列表
const struct _prop_list_t *properties;//用來存放category里面的屬性列表
};
這個struct _category_t
結構體,就是在程序在編譯之后,被用來存放category
的相關信息(instance methods
, class methods
,protocol
,property
)的。
反過來描述,編譯的時候,系統會給每一個category
生成一個對應的結構體變量,而且他們都是struct _category_t
類型的,然后把category
里面的信息存到這個變量里面。
在我的示例里面,這個變量的名稱叫_OBJC_$_CATEGORY_CLPerson_$_Test
,這個名字很清晰的表明,它存儲的是Objective-c
下的CLPerson
類的Test
分類的信息。
struct _category_t
中定義了六個成員變量,除去其中的第二個,我個人還沒搞明白有什么用,其他的五個作用則非常清晰了
const char *name;
上圖中的a部分,其值表示category
所對應的類的名字。
const struct _method_list_t *instance_methods;
上圖中的b部分,其值就是實例方法列表,可以看到里面正好放了我們定義的實例方法-test
const struct _method_list_t *class_methods;
上圖中的c部分,其值就是類方法列表,可以看到里面放了我們定義的類方法-classTest
const struct _protocol_list_t *protocols;
上圖中的d部分,其值就是協議列表,可以看到里面存放了NSCoping
協議
const struct _prop_list_t *properties;
上圖中的e部分,其值就是屬性,可以看到里面有我們定義的age
屬性
源碼分析
上面的篇章,我們通過查看編譯后的cpp文件,了解了category
在編譯階段完成后的存在形式,以CLPerson+Test
為例,它所對應的struct _category_t
變量中,第一個成員變量name
的值為"CLPerson"
(CLPerson+Eat
對應的name
也是"CLPerson"
,可以自行驗證),而且根據我在對象的本質(上)——OC對象的底層實現中所討論所得出的結果可以知道,一個OC類XXX
在底層都存在一個對應的C++結構體實現struct XXX_IMPL
,但我們在CLPerson+Test.cpp
文件中,并沒有發現 struct CLPerson+Test_IMPL
/struct CLPerson+Eat_IMPL
,因此,我猜想CLPerson
的category
中的信息,應該還是存儲在CLPerson
所對應的class
對象和meta-class
對象中,category
自己并沒有獨立的class
對象和meta-class
對象。CLPerson
旗下的所有category
里面的信息,應該是在某個階段被合并到了類的CLPerson
的class
對象和meta-class
對象中。從編譯的結果看,我們并沒有發現有合并的操作,僅僅是給每個category
生成了對應的struct _category_t
類型的變量,存放其信息。所以我合理懷疑,合并操作應該是發生在Runtime階段。
為了證明以上猜想,我們還是要挖掘Runtime的源碼。我們先去蘋果官網下載一份objc4的最新源碼。然后我們直接尋找objc-os.mm
文件,這個文件可以看作是Runtime進行初始化的地方。然后找到_objc_init()
方法,這個方法是Runtime被加載后執行的第一個方法,可以理解成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);
}
_objc_init()
中前面的一堆方法,跟本文的主題不相關,不入坑,且看最后一個方法_dyld_objc_notify_register(&map_images, load_images, unmap_image)
。這個函數里面的三個參數分別是另外三個函數:
-
map_images
-- Process the given images which are being mapped in by dyld.(處理那些正在被dyld映射的鏡像文件) -
load_images
-- Process +load in the given images which are being mapped in by dyld.(處理那些正在被dyld映射的鏡像文件中的+load
方法) -
unmap_image
-- Process the given image which is about to be unmapped by dyld.(處理那些將要被dyld進行去映射操作的鏡像文件)
我們查看一下map_images
方法,點進去
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
這里面看不出啥,返回了map_images_nolock(count, paths, mhdrs)
,感覺像是一層轉換,繼續點進該方法看一下。好家伙,這個方法就比較豐富了,為了節約紙張,這里就不貼完整代碼了,有興趣自己上源碼看。經過牛人指點,找到里面一個關鍵方法_read_images(hList, hCount, totalClasses, 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);
}
}
}
}
一句// Discover categories
真的是對讀者非常友好,這立馬使我明白,接下來的代碼是處理category相關內容的。這個read_images
方法從上倒下,分好幾大塊,每大塊頭部都有類似的注釋,說明該部分所做的事情,將作者的思路描述的非常清晰,不愧是蘋果的源碼。下面通過圖解來說明一下category處理部分的大致思路
這里注意我一個細節,上圖的第一部分我已經畫出來了,一開始的那個catlist
是一個二維數組,里面的成員也是一個一個的數組,也就是代碼里面的cat所指向的數組,它的類型是category_t *
,說明cat
數組里面裝的就是category_t
,(有點繞,慢慢來:-)一個cat
里面裝的就是某個class
所對應的所有category。
那么什么決定了這些category_t在cat數組中的順序呢?
答案是category文件的編譯順序決定的。先參與編譯的,就放在數組的前面,后參與編譯的,就放在數組后面。我們可以在xcode-->target-->Build Phases-->Compile Sources列表查看和調整category文件的編譯順序
在上面的category先編譯,下面的category后編譯。可以鼠標拖拽進行調整。
然后我們繼續往下看,進入remethodizeClass
方法看一看
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
然后在這里面找到一個方法attachCategories
,肯名字就知道,附著分類,也就是把分類的內容添加/合并到class里面,貌似快接近真相了,小雞動??
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
//分配一塊空間來放所有分類的方法數組,這里是一個二維數組,數組的每個成員,對應著某個分類的方法數組
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
//分配一開空間來放所有分類的屬性數組,理解同上
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
//分配一塊空間來放所有分類的協議數組,理解同上
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {//這里i--說明,是從
//取出某個分類變量
entry = cats->list[i];
//提取分類中的對象方法/類方法
/* mlists最終會是以下形式
[
[method_t, method_t],
[method_t, method_t]
]
*/
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
//提取分類中的屬性
/* proplists最終會是以下形式
[
[property_t, property_t],
[property_t, property_t]
]
*/
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
//提取分類中的協議
/* protolists最終會是以下形式
[
[protocol_t, protocol_t],
[protocol_t, protocol_t]
]
*/
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
//得到類對象里面的數據
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
//將所有分類的對象方法附加到類對象的方法列表中
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
//將所有分類的屬性附加到類對象的屬性列表中
rw->properties.attachLists(proplists, propcount);
free(proplists);
//將所有分類的協議附加到類對象的協議列表中
rw->protocols.attachLists(protolists, protocount);
free(protolists);
//搞定,結束
}
??以上這一部分代碼中的注解引用自MJ大神在騰訊平臺的相關分享??
這里注意一個地方,這里面用了while (i--) {entry = cats->list[i]; ......}
,entry
可以簡單理解成 category_t
,(里面還有一些其他內容,不影響我們的理解),那么list
里面就裝了一堆的category_t
,他們都對應著同一個class
,這些category_t
在數組中的順序,和前面我們討論的category
文件的編譯順序是相同的,也就是先編譯的category
在前,后編譯的category
在后。 在while
循環里面進行處理的時候,是從下標 cats->count-1
(也就是i--
)開始的,也就是從數組的尾部向前一個一個的處理。處理過程主要就是把category
的方法列表添加到mlists
里面,mlists[mcount++] = mlist;
,而mcount
是從0開始的,所以結果就是最終,放到mlists
里面的方法列表順序是倒過來的,最前面的方法列表,對應著最后編譯的cetegory(協議和屬性的處理過程和這里一樣)
上述方法里面的最后一個操作rw->methods.attachLists
我們再進一步分析一下,看一看,最終分類中的方法和class中的方法,最終是以怎么樣的順序合并存放到最后的方法列表里面的,進入attachLists
函數
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
這個函數的兩個參數分別代表
-
addedLists
--將要被添加的category
中的方法列表組成的的數組, -
addedCount
--addedLists
數組的元素數量。
這個方法是今天討論的問題里面最有趣的地方,將會解釋我們在使用category
中所碰到的各種現象。請看下圖分解
category的合并過程
如此看來,最終類的方法列表里面,如果class
有自己對應的category
,那么category
中的方法列表會被合并放置在class
的方法列表的前部,類本身的方法則會被往列表尾部挪,當我們通過[obj method]
的方式調用方法的時候,系統會到在類的方法列表里面,從前往后遍歷查找。
因此,如果category
里面如果重寫了class
里面的方法,那么,最終會調用category
的方法實現,就是因為它被放在了列表前面,先被找到,就被調用了,其實class
里面的同名方法還是在的,并沒有被覆蓋,只不過看起來像是覆蓋了。
另外,我們在上面分析attachCategories
方法的時候得知,該方法實際上將category的方法列表按照編譯順序倒過來存到了一個數組里,供后續方法使用。
那么過程走到這里,便可以知道,最最最終,在class的方法列表里面,最后參加編譯的
category
的方法會出現在方法列表的最前面,先參加編譯的category
的方法會出現在方法列表的后面,列表的最后存著class
自己的方法[對于meta-class也是一樣的],好,分析結束。
回答開篇的幾個問題
????????
-
[person test]
和[person eat]
的消息是發送給誰呢?
發送給
CLPerson
的類對象
- 還是說,對于
CLPerson+Test.h
來說,也有其獨立對應的分類對象呢?
不存在所謂的 分類的類對象,一個類以及它的所有分類,都只對應一個類對象,它們所有的實例方法(
-方法
),屬性(@property
),協議(@protocol
)都被合并到了這一個類對象里面,它們所有的類方法(+方法
),都被合并到了這個類的元類對象里面。上面所說的合并,都是發生在程序運行階段,運用了Objc的Runtime機制完成。
????????
*****************砍瓜切菜*****************
(1)category里面的方法存放在哪里?
- 一個類所對應的分類下的對象方法,存放在該類的類對象的方法列表里面。
- 一個類所對應的分類下的類方法,會存放在該類的元類對象的方法列表里面
(2)category里面的方法,是什么時候被放到類的類對象/元類對象的方法列表里面的?(編譯階段 or 運行階段)
- 結論:是程序運行的時候進行的。通過runtime動態地將分類的方法,合并到類對象、元類對象中。
所有的category結構是一樣的,只不過里面存儲的具體數據不同,每一個category都有自己對應的一個變量,類型為 struct _category_t
,在編譯過程中,會完成對struct _category_t
類型變量的賦值。
(3)程序運行過程中,分類中的方法是如何合并到類的方法列表中的?
面試官要問,就直接畫圖改他看吧,文字描述感覺弱爆了:)
(4)分類方法會覆蓋類里面的方法嗎?
不會
(5)如果有多個分類有同名的方法A,那么實際哪一個方法A會被調用?
最后參加編譯的category里面的A方法會被調用
(6)如何控制分類的編譯順序?
在Build Phase->Compile Sources里面調整,直接拖拽
(7)category和extension的區別是什么?
- extension的內容是在編譯完成后,就存在于類對象里面,extension只不過是將原本.h文件里面的內容挪到了.m文件里面,不讓外界看見,實質上它就是class.h的一部分,
- category的內容,是在編譯的時候,保存到了struct _category_t 結構體變量中,然后在程序運行階段(runtime機制)才動態合并到類對象當中的。