Objective-c 類的結(jié)構(gòu)
Objective-c 作為一種動態(tài)語言,除了需要一個編譯器來編譯之外,還需要有一套運行時環(huán)境來動態(tài)的創(chuàng)建類和對象。這就是我們要了解的Runtime。Runtime的核心是消息傳遞(Messaging),了解這一核心能夠更好的利用語言的特點,適當?shù)臅r候進行擴展。
與靜態(tài)語言不同的是,Objective-c在調(diào)用方法的時候并不是直接跳轉(zhuǎn)到編譯時生成的函數(shù)地址執(zhí)行代碼,而是會把這個方法以消息的形式發(fā)送給object。能否由object執(zhí)行或者轉(zhuǎn)發(fā)給別的對象處理或者不做處理,都是在運行時決定的。
打開objc/runtime.h可以看到,系統(tǒng)聲明了一個objc_class的結(jié)構(gòu)體
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
從結(jié)構(gòu)體中可以看出,一個運行時類關(guān)聯(lián)了它的父類指針,類名,版本號,成員變量,方法列表(實例方法),緩存,協(xié)議。
其中成員變量,方法列表都是一個結(jié)構(gòu)體類型
//成員變量和成員變量列表
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//方法和方法列表
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
struct objc_method_list {
struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
objc_ivar_list和objc_method_list是分別用來存儲成員變量和方法的列表,這兩個結(jié)構(gòu)體內(nèi)部有objc_ivar,和objc_method這兩個結(jié)構(gòu)體,這兩個結(jié)構(gòu)體存儲的是對應(yīng)的成員變量和方法。其中objc_method這個結(jié)構(gòu)體有SEL method_name 和 IMP method_imp這兩個變量,分別對應(yīng)方法的名稱和實現(xiàn)。
SEL是selector在oc中的表示,本質(zhì)是一個映射到方法的一個C字符串。
IMP是一個函數(shù)指針,指向一個函數(shù)的實現(xiàn)
這里還要再了解一個c結(jié)構(gòu)體objc_object
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
這個結(jié)構(gòu)體對應(yīng)的是我們類的實例,結(jié)構(gòu)體內(nèi)部只有一個isa屬性,指向的正是objc_class這個結(jié)構(gòu)體,而objc_class這個結(jié)構(gòu)體中也有一個isa指針說明,類本身也是一種對象,這個isa指向的是類對象的元類Meta Class,Meta Class 表述了類對象本身所具備的元數(shù)據(jù)。那既然類本身作為一個對象存在,那類方法是不是就相當于是類對象的實例方法那?我們通過下面的代碼來測試一下:
Test *test = [[Test alloc] init];
unsigned int count = 0;
Method *meths = class_copyMethodList(object_getClass([test class]), &count);
for (NSInteger i = 0; i < count; i++) {
Method meth = meths[i];
SEL sel = method_getName(meth);
const char *name = sel_getName(sel);
NSLog(@"%s", name);
}
free(meths);
runtime.h中提供了很多方便的方法使我們可以輕易的獲取方法列表或者成員變量,其中以class_開頭的是跟objc_class相關(guān)的方法,以object_開頭的是跟objc_object相關(guān)的方法,分別對應(yīng)我們的類對象和實例對象。通過上面的object_getClass方法可以獲取需要的類,傳入的一個obj對象,如果傳入的是test則打印出來的方法名是實例方法,如果傳入的是test的類則打印出來的是類方法,通過這個測試也驗證了上面的猜測。
了解了類的構(gòu)成之后,那么在調(diào)用方法的時候是怎么處理的那?oc的運行時特性又是怎么體現(xiàn)出來的那?
消息發(fā)送
事實上,在編譯時,oc的調(diào)用方法會被翻譯成一個c的函數(shù)調(diào)用,objc_msgSend(receiver, selector, arg1, arg2, ...).
從上面的分析可以看出,發(fā)送一條消息應(yīng)該執(zhí)行下列步驟:
1. 通過obj的isa找到實例的class
2. 在class的方法列表中找對應(yīng)的方法
3. 如果沒找到就去superclass里繼續(xù)找
4. 一旦找到就去執(zhí)行它的實現(xiàn)IMP
一個正常的方法通常都是這樣被調(diào)用的,而且由于不同的方法調(diào)用頻率是不一樣的,所以系統(tǒng)提供了一個cache也就是上面看到的objc_cache來做緩存,提高查詢效率。
動態(tài)方法解析,消息轉(zhuǎn)發(fā)和方法簽名
上面的例子只是針對正常的情況,那如果該方法沒找到怎么辦,這時就會拋出unrecognized selector sent to … 的異常,這也是我們在開發(fā)中經(jīng)常遇到的情況。那怎么避免這種情況那,這就用到了oc的動態(tài)特性,在一場拋出前,runtime會給三次補救的機會:
1.動態(tài)方法解析
查看NSObject的API發(fā)現(xiàn)有下面這兩個方法+resolveInstanceMethod:和+resolveClassMethod:分別對應(yīng)實例方法和類方法。在沒有找到方法的情況下,如果實現(xiàn)了這兩個方法中的一個,那么系統(tǒng)就會重新啟動一次消息發(fā)送的過程。
以上面的Test類為例,如果一個Test的實例調(diào)用一個method1:方法,而Test類沒有實現(xiàn)的話使用下面的代碼可以避免程序閃退:
void method2(id obj, SEL _cmd)
{
NSLog(@"do something");
}
+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(method1:))
{
class_addMethod([self class],aSEL,(IMP)method2,@"v@:");
return YES;
}
return [super resloveInstanceMethod];
}
2.如果resolve方法返回NO,就會進行下一步,消息轉(zhuǎn)發(fā)(Message Forwarding)
同樣runtime提供了一個方法,-forwardingTargetForSelector: ,用這個方法可以獲得把消息轉(zhuǎn)發(fā)給其他對象的機會,該方法返回一個實現(xiàn)了被調(diào)用的方法的實例:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(method1:))
{
return xxxObject;
}
return [super forwardingTargetForSelector:aSelector];
}
3.如果這個方法也返回nil,則會啟動最后一步,方法簽名,首先runtime會發(fā)送一個-methodSignatureForSelector:消息來獲取參數(shù)和返回值,如果返回nil則發(fā)送doesNotRecognizeSelector:消息,這是程序也就崩潰了。如果返回了一個函數(shù)簽名,runtime就會創(chuàng)建一個NSInvocation對象,發(fā)送-forwardInvocation:消息給當前對象,并傳入NSInvocation:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
if(!signature) {
if([xxxObj respondsToSelector:selector]) {
return [xxxObj methodSignatureForSelector:selector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation*)invocation
{
if ([xxxObj respondsToSelector:[invocation selector]]) {
[invocation invokeWithTarget:xxxObj];
}else {
[self doesNotRecognizeSelector:sel];
}
}
這就是Objective-c關(guān)于消息發(fā)送的流程,利用這個流程和運行時的特性可以對語音進行擴展和解決一些問題。
Refer: