動態語言
OC是一門不折不扣的動態語言,所以它的很多機制都是動態運行時決定的。這點和C語言不一樣,C語言是靜態綁定,也就是編譯后所有的一切都已經決定了。這一點和C語言的函數指針有些類似,很多時候函數指針在編譯的時候并不知道會指向哪個函數,所以此時就是動態綁定。
舉幾個OC動態類型的例子,最為直接的就是id類型了、還有關聯對象、動態綁定、消息轉發、方法調配、這些技術都是動態類型很好的證明。
OC對象結構
在介紹動動態性之前,我們先來看看OC對象的一些結構。
#import<objc/runtime>這是OC運行時函數庫,里面定義了很多結構體。
首先看對象的結構:
typdef struct objc_object { Class isa; } *id;
對象結構中非常簡單,只有一個isa指針,isa指針后面我們會介紹。
接下來我們看類的結構體
struct objc_class { Class isa#if !__OBJC2__ Class super_class const char *name long version long info long instance_size struct objc_ivar_list *ivars struct objc_method_list **methodLists struct objc_cache *cache struct objc_PRotocol_list *protocols #endif}typedef struct objc_class *Class;
可以看到很多信息都在Class中定義著,里面信息如下
字段 | 含義 |
isa | isa指針 |
super_class | 父類指針 |
name | 類名 |
version | 類的版本信息,默認為0 |
info | 供運行期使用的一些位標識 |
instance_size | 實例的大小 |
ivars | 實例變量列表 |
methodLists | 方法列表 |
cache | 指向最近調用的方法,用于優化調用方法的速度 |
protocols | 協議列表 |
接下來我們逐個介紹一下:
isa指針和super_class
在OC中,嚴格意義上講是沒有類這種概念的,每一個類都是一個對象,只不過類對象是一個單例。
isa指針存在于每一個對象中,類普通實例的isa指針指向類,類的isa指針指向它的元類(類方法全部都在元類中存放)。元類的isa指針指向根元類,也就是NSObject的isa所指向的元類。
super_class只有類和元類才有,它們分別指向自己的父類和父元類,而為了讓NSObject成為所有類的根類,讓NSObject的元類的父類指針也指向了NSObject。
這樣說可能也不是很好理解,看下面這張圖應該就很快理解了。
等下我們說到消息傳遞的時候還會在說到isa和super_class。
ivars屬性列表
struct objc_ivar_list { int ivar_count #ifdef __LP64__ int space #endif /* variable length structure */ struct objc_ivar ivar_list[1] }
space作用還不太清楚...求指教啊。
下面是實例變量結構
struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE; char *ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE;#ifdef __LP64__ int space OBJC2_UNAVAILABLE;#endif}
可以看到有一個ivar_offset,這個是實例變量在編譯時的偏移量,是由編譯時決定的。
methodLists方法列表
struct objc_method_list { struct objc_method_list *obsolete int method_count #ifdef __LP64__ int space #endif /* variable length structure */ struct objc_method method_list[1] }
該結構有方法鏈表和方法總數。
struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE;}
里面有函數名,返回值類型和函數實現,接下來看看SEL和IMP定義
typedef struct objc_selector *SEL;typedef id (*IMP)(id, SEL, ...);
可以看到SEL是objc_selector,(*IMP)(id,SEL,...)是id,我沒有找到objc_selector的結構所以這里也沒法說什么...
cache緩存列表
typedef struct objc_cache *Cache #define CACHE_BUCKET_NAME(B) ((B)->method_name)#define CACHE_BUCKET_IMP(B) ((B)->method_imp)#define CACHE_BUCKET_VALID(B) (B)#ifndef __LP64__#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))#else#define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>3)) & (mask))#endifstruct objc_cache { unsigned int mask /* total = mask + 1 */ unsigned int occupied Method buckets[1] };
緩存列表里面包含了已緩存的方法,用于快速的調用,不需要在去方法列表里面查詢了。
protocol協議列表
struct objc_protocol_list { struct objc_protocol_list *next; long count; Protocol *list[1];};
協議列表包含了協議數量和協議的指針。
id
id類型實際上就是一個objc_object的typedef,是一個對象實例,而且還是一個指針,所以用id來定義對象的時候就不需要加*號了。
id類型往往需要我們使用”自省“機制來保證使用安全,所謂自省其實就是看看這個對象是不是某個類的實例,或者是不是其子類。
自省用一下兩個方法:
isKindOfClass:(Class)class 判斷是不是其類族對象
isMemberOfClass:(Class)class 判斷是不是類本身對象
關聯對象
關聯對象在我之前的博客中已經有介紹了,這里就不再說了。
OC消息機制和消息轉發
別的語言調用函數,OC則叫做發送消息,這是因為所有OC的方法調用實際上底層都是通過
objc_msgSend(id self , SEL cmd,...)來發送的。
該函數的作用就是傳遞給一個對象某個方法,后面的不定參數列表是方法所需要的參數。
這里說一下OC的消息傳遞機制,首先對一個對象發送消息,它會先檢查自己的類中有沒有該方法,如果沒有就找他的父類中有沒有,如果還沒有則會進行消息轉發。
在看例子之前先說一下,Xcode6貌似默認行為不讓我們使用objc_msgSend了,所以需要先設置一下
把這一項設置為No就可以了。
這里看個例子,
EqualObject *object1 = [EqualObject new]; EqualObject *object2 = [EqualObject new]; object1.name = @"xiaoming"; object2.name = @"xiaoming"; BOOL isEqual = objc_msgSend(object1,@selector(isEqualToEqualObject:),object2); if(isEqual) { NSLog(@"equal"); }
EqualObject是我們自己實現的類,它有一個判斷是否相等的方法isEqualToEqualObject:,如果name相等就人為兩個對象相等。
這里我們直接傳遞消息,不通過OC語法,運行程序可以看到equal被打印了出來。
然后我們再看看消息轉發機制。
當該對象包括其父類都沒有這個方法的時候會啟動,消息轉發機制分為兩大階段。
第一階段先看對象所屬類是否有能力動態添加方法,已處理這個位置的選擇子,這叫做動態解析(dynamic method resolution)。
第二階段設計“完整的消息轉發機制”。如果運行期系統已經把第一階段執行完了,那么接受者自己就沒法再以動態新增方法的手段來處理與消息相關的方法調用。這又分為兩個小步。
首先,請接受者看看有沒有其他對象能處理這條消息。若有,則在運行時轉給那個對象,于是消息轉發過程結束。若沒有“備用的接受者”,則啟動完成的消息轉發機制,運行起系統會把與消息有關的全部細節都封裝到NSInvocation對象中,再給接受者最后一次機會,令其設法解決當前還未處理的這條消息。
動態方法解析
遇到無法解析的信息后,首先將調用其所屬類的下列類方法:
+(BOOL)resolveInstanceMethod:(SEL)selector
該方法參數就是未知的選擇子,返回BOOL類型那個,表示這個類是否能新增一個實力方法已處理這個選擇子。假如是類方法,那么會調用
+(BOOL)resolveClassMethod:(SEL)selector
使用這種方法的前提是相關的實現已經寫好了,只等運行時動態的插入就行,比如CoreData中NSManagedObjects對象的屬性時就可以這么做,因為實現這些屬性所需的存取方法在編譯期就能確定。
下面我們看個例子
void showMessage(id self, SEL _cmd, id value){ if([value isKindOfClass:[NSString class]]) { NSLog(@"%@",(NSString *)value); }}+(BOOL)resolveInstanceMethod:(SEL)sel{ NSString *selString = NSStringFromSelector(sel); if([selString isEqualToString:@"showMessage:"]) { class_addMethod(self, sel, (IMP)showMessage, "v@:@"); return YES; } else { return [super resolveInstanceMethod:sel]; }}
向剛才EqualObject添加以上實現帶代碼,然后在客戶端調用:
objc_msgSend(object1,@selector(showMessage:),@"Hello");
能夠看到程序并沒有報錯,而且還打印出了Hello!
備援接受者
當沒有使用動態方法解析后,還是出發備用接受者,該步驟會觸發該方法
-(id)forwardingTargetForSelector:(SEL)selector
方法參數是未知的選擇子,如果找到備用對象返回對象,否則返回nil。
可以利用該方法來模擬多重繼承機制(實際為組合),因為外部看不到,所以感覺上就像是本身處理該消息。
下面看一個例子:
把剛才添加的代碼注釋掉,添加一個新類OtherObject,添加showMessage方法,然后在EqualObject中添加以下代碼
-(id)forwardingTargetForSelector:(SEL)aSelector{ NSString *selectString = NSStringFromSelector(aSelector); if([selectString isEqualToString:@"showMessage:"]) { return other; } return nil;}
會發現Hello依舊出現了!,而且如果你在OtherObject中的showMessage方法中打上斷點,會發現方法執行到了OtherObject中...
完整的消息轉發
如果消息沒有轉發,那么回來到這一步,首先創建NSInvocation對象,然后把未處理的信息細節全部都封裝于其中。
此對象包含選擇子、目標(target)和參數。在觸發NSInvocation對象時,“消息派發系統”會把消息派給目標。
此步驟會調用以下方法:
-(void)forwardInvocation:(NSInvocation *)invocation
這個方法實現簡單,只要改變調用目標,使消息在新目標上得以調用即可。但是這樣和備用接受者實現就一樣了,所以一般都不會這樣寫。
比較有用的實現是再出發消息前,先以某種方式改變消息內容,比如追加另一個參數,或是改裝選擇子。
如果發現不該由該類執行,那么需要調用超類的該方法,繼承體系中每個類都有機會處理此調用請求,如果到NSObject還不能處理會調用doesNotRecognizerSelector拋出異常。
實現上面那個方法的時候需要同時實現。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
需要先對NSInvocation簽名然后才能使用NSInvocation
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature *sig; sig = [other methodSignatureForSelector:aSelector]; return sig;}-(void)forwardInvocation:(NSInvocation *)anInvocation{ [anInvocation invokeWithTarget:other];}
方法調配
在運行時,我們還可以使用方法調配技術來改變SEL指向的IMP,比如說目前方法名和對應IMP如下
方法 | IMP |
methodA | IMPA |
methodB | IMPB |
但是當我們使用方法調配后,就可以出現一下情況
方法 | IMP |
methodA | IMPB |
methodB | IMPA |
其實我們修改的本質是方發表的映射,修改了選擇子的指向
該技術主要用到的方法如下
void method_exchangeImplementations(Method m1, Method m2)
該函數的兩個參數表示待交換的兩個方法實現,而方法實現則可通過下列函數獲得:
Method class_getInstanceMethod(Class aClass , SEL aSelector)
現在我們向EqualObject中添加methodA和methodB
-(void)methodA{ NSLog(@"methodA");}-(void)methodB{ NSLog(@"methodB");}
然后客戶端這樣寫
EqualObject *object1 = [EqualObject new]; Method methodA = class_getInstanceMethod([EqualObject class], @selector(methodA)); Method methodB = class_getInstanceMethod([EqualObject class], @selector(methodB)); method_exchangeImplementations(methodA, methodB); [object1 methodA];
運行后就會打印出methodB。
利用該技術進行黑盒調試
要進行黑盒調試,主要用到的就是該技術和category
下面來看看具體該如何編寫:
首先添加一個EqualObject的category,然后添加一個新方法plusMethodA
-(void)plusMethodA{ [self plusMethodA]; NSLog(@"this is plus version");}
這里看著像是會無限遞歸,但是實際上plusMethodA選擇子已經指向了methodA的IMP,所以并不會出現無限調用的情況。
客戶端代碼
EqualObject *object1 = [EqualObject new]; Method methodA = class_getInstanceMethod([EqualObject class], @selector(methodA)); Method methodB = class_getInstanceMethod([EqualObject class], @selector(plusMethodA)); method_exchangeImplementations(methodA, methodB); [object1 methodA]
輸出結果為
2015-08-13 09:06:01.672 Equal[8282:5413229] methodA2015-08-13 09:06:01.673 Equal[8282:5413229] this is plus version
可以看到我們沒有繼承一個類就做到了擴展某個方法,用于調試打印一些輸出信息會很有用!
常用Runtime總結
關聯對象:
設置一個關聯對象
void objc_setAssociatedObject(id object, void *key ,id value, objc_AssociationPolicy policy)
獲取關聯對象
void objc_getAssociatedObject(id object, void *key)
刪除該對象所有的關聯對象
void objc_removeAssociatedObjects(id object)
消息傳遞
向某個對象/父類 發送消息
objc_msgSend(Super)
方法調配
交換兩個方法的實現
void method_exchangeImplementation(Method m1, Method m2)
得到該法的指針
Method class_getInstanceMethod(Class aClass, SEL aSelector)
動態創建對象
創建新的類
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
給類增加新的方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
注冊新的類
void objc_registerClassPair(Class cls)
獲得對象的isa指針所指向的對象
Class object_getClass(id obj)
新聞熱點
疑難解答