在swift這門(mén)優(yōu)雅的語(yǔ)言還沒(méi)誕生之前从祝,iPhone開(kāi)發(fā)主要使用的是Object-C這門(mén)面向?qū)ο笳Z(yǔ)言牛曹,OC是由C實(shí)現(xiàn)的超集(大部分的OC庫(kù)都有對(duì)應(yīng)的C版本的實(shí)現(xiàn)例如Foundation和CoreFoundation)甸怕,并不需要像JAVA那樣運(yùn)行在虛擬機(jī)中样勃,而且可以很好的結(jié)合C和C++代碼提高程序的性能单绑,除了面向?qū)ο蟮奶匦酝獗也蓿琌C這門(mén)語(yǔ)言還具備了smalltalk的消息機(jī)制,當(dāng)我們調(diào)用了一個(gè)對(duì)象的方法或者說(shuō)函數(shù)時(shí)殷蛇,其實(shí)是向那個(gè)對(duì)象發(fā)送了一條消息实夹。
OC是一門(mén)動(dòng)態(tài)語(yǔ)言,也就是說(shuō)在OC運(yùn)行時(shí)粒梦,有一個(gè)運(yùn)行時(shí)系統(tǒng)亮航,運(yùn)行時(shí)系統(tǒng)的作用就是執(zhí)行編譯后的代碼,動(dòng)態(tài)的加載類匀们,向?qū)ο蟀l(fā)送消息缴淋,運(yùn)行時(shí)系統(tǒng)更像是OC的操作系統(tǒng)。
那么什么是動(dòng)態(tài)呢泄朴?我們來(lái)看看下面這段代碼:
Person *p = [[Person alloc] initWithName:@"Tom" andAge:15];
[p performSelector:@selector(sayHello)]; //雖然Person類中并沒(méi)有這個(gè)sayHello方法重抖,依然可以編譯通過(guò)
這段代碼在編譯階段并不能夠判斷出Person對(duì)象是否存在sayHello這個(gè)方法(盡管會(huì)給出警告,但并不報(bào)錯(cuò))祖灰,可以通過(guò)編譯階段钟沛,但是會(huì)在運(yùn)行時(shí)崩潰。也就是說(shuō)OC語(yǔ)言的動(dòng)態(tài)特性使得類型信息在運(yùn)行時(shí)被檢查局扶,而不是編譯時(shí)恨统。同時(shí)Class也是動(dòng)態(tài)創(chuàng)建的叁扫,也就是說(shuō)你可以在程序運(yùn)行的時(shí)候?yàn)槌绦蛐略鲱悺?duì)象畜埋、以及方法和方法體等莫绣。本文將介紹runtime原理和實(shí)際應(yīng)用:
1. 消息機(jī)制
2. 消息轉(zhuǎn)發(fā)
3. 屬性定義
4. 實(shí)際使用
在了解Runtime機(jī)制之前,先來(lái)簡(jiǎn)單了解一下NSObject這個(gè)公共父類(并不是所有的類都繼承自NSObject悠鞍,例如NSProxy):
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
+ (void)load;
+ (void)initialize;
- (instancetype)init
#if NS_ENFORCE_NSOBJECT_DESIGNATED_INITIALIZER
NS_DESIGNATED_INITIALIZER
#endif
.
.
.
可以看到对室,NSObject有一個(gè)成員變量叫做isa,它是Class類型的咖祭,這個(gè)Class其實(shí)是一個(gè)結(jié)構(gòu)體:typedef struct objc_class *Class
掩宜,來(lái)研究一下這個(gè)結(jié)構(gòu)體:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#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
} OBJC2_UNAVAILABLE;
結(jié)構(gòu)體的成員中包含了另一個(gè)isa的引用,其它的結(jié)構(gòu)成員在OC2.0之后不可用么翰,但是锭亏,依然可以從中獲取重要的一些信息,例如父類硬鞍、類名慧瘤、對(duì)象大小、變量列表固该、方法列表锅减、該類遵循的協(xié)議等都是以列表的形式保存在objc_class中,其中還有一個(gè)緩存cache伐坏,用于緩存最近使用到的消息怔匣,該文件中還包括對(duì)其它結(jié)構(gòu)體的定義,例如方法桦沉、類目每瞒、屬性等。(可以通過(guò)#import<objc/runtime.h>查看)基礎(chǔ)知識(shí)先說(shuō)到這里纯露,來(lái)看看消息機(jī)制剿骨。
1.消息機(jī)制
什么是消息機(jī)制,舉例來(lái)說(shuō):
Math m = [[Math alloc]init];
[m sum:5 y:6];
通常會(huì)說(shuō)調(diào)用了m對(duì)象的sum方法埠褪,但編譯器會(huì)將函數(shù)調(diào)用轉(zhuǎn)變?yōu)橄驅(qū)ο蟀l(fā)送一條消息浓利,:
objc_msgSend(m , sum ,5 , 6);
現(xiàn)在應(yīng)該說(shuō)是像m對(duì)象發(fā)送了一條sum消息更合適。
首先應(yīng)該了解SEL和IMP钞速,我們暫且可以這么區(qū)分贷掖,一個(gè)方法有方法名和方法體,SEL指的是方法名(可以這么理解但是實(shí)際叫做<b>選擇器</b>渴语,下文會(huì)提到)苹威,而IMP指的是方法體也就是對(duì)應(yīng)的實(shí)現(xiàn),在C語(yǔ)言中例如調(diào)用一個(gè)方法的話驾凶,編譯器會(huì)將方法調(diào)用轉(zhuǎn)換為匯編指令call 并帶一個(gè)地址操作數(shù)牙甫,程序計(jì)數(shù)器會(huì)將下一條要執(zhí)行的指令地址設(shè)為這個(gè)操作數(shù)潮改,并將返回地址壓入棧中。在OC中SEL的定義為:
typedef struct objc_selector *SEL;
而IMP的定義為:typedef void (*IMP)(void /* id, SEL, ... */ );
在運(yùn)行時(shí)腹暖,消息會(huì)綁定到對(duì)應(yīng)的實(shí)現(xiàn)上:
首先會(huì)根據(jù)對(duì)象m的選擇器sum查找對(duì)應(yīng)的方法實(shí)現(xiàn)(IMP)
傳遞參數(shù)(包括消息的接受對(duì)象、選擇器)翰萨,執(zhí)行方法
-
將函數(shù)的返回結(jié)果返回
當(dāng)一個(gè)新的對(duì)象被創(chuàng)建時(shí)脏答,系統(tǒng)要為該對(duì)象分配對(duì)應(yīng)的內(nèi)存,實(shí)例變量被初始化亩鬼,還記得上面說(shuō)的isa么殖告,isa這個(gè)指針變量將被指向該對(duì)象的<b>類結(jié)構(gòu)</b>,之后通過(guò)super_class可以獲取該對(duì)象的父類雳锋,進(jìn)而整個(gè)繼承鏈的類結(jié)構(gòu)信息就都可以獲取了黄绩。編譯器負(fù)責(zé)將類、對(duì)象構(gòu)建為具有運(yùn)行時(shí)信息的結(jié)構(gòu)(包括isa玷过、super_class爽丹、選擇器轉(zhuǎn)發(fā)表等)。另一個(gè)重要的信息就是轉(zhuǎn)發(fā)表辛蚊,可以看做是一個(gè)以SEL為鍵以IMP地址為值的映射粤蝎。
當(dāng)向一個(gè)對(duì)象發(fā)送消息時(shí),objc_msgSend會(huì)去該對(duì)象的類結(jié)構(gòu)體(isa指針指向的結(jié)構(gòu))中查找轉(zhuǎn)發(fā)表袋马,如果能夠定位指定的選擇器的話初澎,就會(huì)執(zhí)行對(duì)應(yīng)地址處的方法體,如果找不到虑凛,會(huì)沿著繼承鏈一層層去查找每一個(gè)父類的轉(zhuǎn)發(fā)表直到NSObject類碑宴。如下圖所示:
這樣一層層查找會(huì)有損程序效率,于是就有了上面提到的緩存桑谍,當(dāng)?shù)谝淮握{(diào)用了某個(gè)方法延柠,系統(tǒng)便會(huì)將方法和對(duì)應(yīng)的實(shí)現(xiàn)地址緩存起來(lái)(系統(tǒng)就是如此霸道,一旦第一次使用了某個(gè)方法锣披,系統(tǒng)會(huì)認(rèn)為你還想繼續(xù)使用)捕仔,在查找轉(zhuǎn)發(fā)表之前,會(huì)搜一搜這個(gè)緩存盈罐。
用上面的Math來(lái)說(shuō)明這個(gè)過(guò)程榜跌,當(dāng)我們創(chuàng)建m對(duì)象時(shí),系統(tǒng)會(huì)為m 對(duì)象分配內(nèi)存盅粪,將isa 指針指向Math 類結(jié)構(gòu)钓葫,并配置轉(zhuǎn)發(fā)表,當(dāng)我們向m 發(fā)送sum 消息時(shí)票顾,objc_msgSend 會(huì)去isa 所指的結(jié)構(gòu)體中查找轉(zhuǎn)發(fā)表础浮,去找啥帆调?去找selector 為sum 的地址,如果找到豆同,就執(zhí)行對(duì)應(yīng)地址的方法體番刊,然后將selector 緩存起來(lái),如果找不到影锈,就沿著superClass 鏈向上查找轉(zhuǎn)發(fā)表芹务,如果到了NSObject 這一層還沒(méi)有找到,對(duì)不起鸭廷,程序就拋異常了枣抱。
objc_msgSend至少需要兩個(gè)參數(shù):接收消息的對(duì)象和選擇器,這兩個(gè)參數(shù)是在編譯的時(shí)候被插入的辆床,在OC中我們定一個(gè)方法佳晶,并不需要顯示的指定這兩個(gè)參數(shù)。
剛才說(shuō)了那么多selector和IMP地址讼载,那么如果我們不想通過(guò)消息機(jī)制來(lái)調(diào)用一個(gè)函數(shù)應(yīng)該怎么辦轿秧,可以通過(guò)methodForSelector:SEL來(lái)將方法的實(shí)現(xiàn)取出來(lái):
Student *p = [[Student alloc] initWithName:@"Tom" andAge:15];
typedef NSInteger(*sum)(id ,SEL , NSInteger , NSInteger);
sum s = (sum)[p methodForSelector:@selector(sum:y:)];
NSLog(@"%d",s(p , @selector(sum:y:) , 10 , 5));
輸出結(jié)果為15。注意methodForSelector返回的是IMP結(jié)構(gòu)體咨堤,需要轉(zhuǎn)換為指定函數(shù)指針類型淤刃,并保證前兩個(gè)參數(shù)依然為接受對(duì)象和方法選擇器。
2. 消息轉(zhuǎn)發(fā)
有沒(méi)有過(guò)這樣的經(jīng)歷吱型,當(dāng)我們?cè)噲D調(diào)用一個(gè)不存在的方法是逸贾,會(huì)報(bào)以下的錯(cuò)誤:unrecognized selector sent to instance 0x1004001a0
,很多iOS初學(xué)者不知道這句話是什么意思津滞,是說(shuō)地址為0x1004001a0的對(duì)象沒(méi)有定義相關(guān)的方法選擇器铝侵,因此不能夠被識(shí)別,通過(guò)查看該地址處的對(duì)象就可以找到出錯(cuò)的原因触徐,也可以根據(jù)debug的crash信息定位出錯(cuò)的對(duì)象和信息咪鲜。那么如果一個(gè)對(duì)象無(wú)法響應(yīng)某個(gè)消息(就是上面說(shuō)的沒(méi)有定義某個(gè)函數(shù)),運(yùn)行時(shí)向該對(duì)象發(fā)送了這個(gè)未定義的消息撞鹉,程序就一定被判死刑了么疟丙,其實(shí)不一定,當(dāng)一個(gè)對(duì)象無(wú)法響應(yīng)某個(gè)message的時(shí)候鸟雏,系統(tǒng)會(huì)給你三次機(jī)會(huì)來(lái)動(dòng)態(tài)的為一個(gè)對(duì)象增加一個(gè)處理消息的實(shí)現(xiàn)或者實(shí)現(xiàn)消息的轉(zhuǎn)發(fā)享郊,讓我們來(lái)看看第一種方式:
動(dòng)態(tài)決議
+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel
這兩個(gè)方法都是動(dòng)態(tài)的為方法選擇器添加方法實(shí)現(xiàn)又叫做<b>動(dòng)態(tài)方法決議</b>,當(dāng)調(diào)用了對(duì)象的一個(gè)不存在的方法選擇器或者該方法選擇器沒(méi)有對(duì)應(yīng)的方法體孝鹊,消息機(jī)制會(huì)調(diào)用這兩個(gè)方法來(lái)決議(注意:如果消息機(jī)制沿著繼承連找不到對(duì)應(yīng)的selector和IMP之間的映射時(shí)才會(huì)調(diào)用炊琉,也就是說(shuō)只有調(diào)用了類或者對(duì)象不存在的方法體時(shí)才會(huì)嘗試決議),例如:
//Person 類
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic , strong)NSString *name;
@property(nonatomic , assign)NSInteger age;
- (id)initWithName:(NSString *)name andAge:(NSInteger)age;
- (void)say;
- (NSInteger)sum:(NSInteger)x y:(NSInteger)y;
+ (void)sayHello;
@end
對(duì)應(yīng)的implement為:
// implement
#import "Person.h"
@implementation Person
- (id)initWithName:(NSString *)name andAge:(NSInteger)age {
if(self = [super init]){
self.name = name;
self.age = age;
}
return self;
}
+ (void)sayHello {} //1
+ (BOOL)resolveClassMethod:(SEL)sel { //2
NSLog(@"%@",NSStringFromSelector(sel));
if(sel == @selector(sayHello)) {
return YES;
}
return [Person resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%@",NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
@end
以類方法sayHello 為例,此時(shí)已經(jīng)實(shí)現(xiàn)了sayHello苔咪,所以并不會(huì)調(diào)用resolve方法锰悼,當(dāng)我們把1處的代碼刪除后,程序會(huì)崩潰团赏,如何動(dòng)態(tài)為類方法sayHello添加方法體呢箕般?
我們將2處的方法修改為:
+ (BOOL)resolveClassMethod:(SEL)sel {
if(sel == @selector(sayHello)) {
class_addMethod([NSObject class], @selector(sayHello), (IMP)sayHelloDynamic, "v@:");//①
return YES;
}
return [super resolveClassMethod:sel];
}
然后添加如下代碼:
void sayHelloDynamic(id target , SEL sel) {
printf("Hello world\n");
}
執(zhí)行結(jié)果為:Hello world。(此處有疑問(wèn):上面代碼的①處舔清,必須指定為NSObject的類對(duì)象丝里,如果是Person的話,通過(guò)class_getClassMethod得到的結(jié)果為nil鸠踪,也就是說(shuō)添加類方法不成功,這里還需要繼續(xù)調(diào)查复斥,如果有知道的讀者可以留言营密。)
resolveClassMethod動(dòng)態(tài)添加類方法,而resolveInstanceMethod動(dòng)態(tài)添加實(shí)例方法目锭,網(wǎng)上大多數(shù)教程都在解釋后面這個(gè)方法评汰,想必也是因?yàn)閞esolveClassMethod添加類方法失敗。
消息轉(zhuǎn)發(fā)
當(dāng)通過(guò)繼承連定位不到對(duì)象相關(guān)消息的實(shí)現(xiàn)痢虹,同時(shí)resolveInstanceMethod對(duì)應(yīng)的selector返回NO的話被去,系統(tǒng)會(huì)嘗試消息轉(zhuǎn)發(fā)(按照文檔的說(shuō)法,決議優(yōu)先消息轉(zhuǎn)發(fā)奖唯,決議與消息轉(zhuǎn)發(fā)正交惨缆,也就是如過(guò)對(duì)應(yīng)的selector在決議方法中返回true,消息轉(zhuǎn)發(fā)不會(huì)被調(diào)用)丰捷∨髂可以將消息轉(zhuǎn)發(fā)看做是處理不存在消息的第二層保護(hù)。如果向一個(gè)對(duì)象發(fā)送了一個(gè)它不能處理的消息時(shí)病往,運(yùn)行時(shí)系統(tǒng)會(huì)向forwardInvocation:發(fā)送一個(gè)消息捣染,并傳遞NSInvocation對(duì)象,該對(duì)象可以看做是一個(gè)方法調(diào)用的包裝(消息的響應(yīng)對(duì)象停巷、selector耍攘、參數(shù)、返回值)畔勤,通過(guò)重寫(xiě)該方法就可以獲得一個(gè)消息轉(zhuǎn)發(fā)的機(jī)會(huì)蕾各。
還是上面的Person類,如果我們現(xiàn)在調(diào)用Person對(duì)象的say方法庆揪,程序一定崩潰示损,讓我們?cè)趇mplement中加入以下代碼:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if(![self respondsToSelector:anInvocation.selector]){
return;
}
}
但是單單是重寫(xiě)了forwardInvocation方法還是不夠的,還需要重寫(xiě)methodSignatureForSelector:方法來(lái)為forwardInvocation:的anInvocation參數(shù)提供必要的信息嚷硫,代碼如下:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"----%@",NSStringFromSelector(aSelector));
if([self respondsToSelector:aSelector]) {
return [super methodSignatureForSelector:aSelector];
} else {
[self noMessage:aSelector];
return [NSMethodSignature signatureWithObjCTypes:"v@:"]; }
}
- (void)noMessage:(SEL) sel{
NSLog(@"No this function %@",NSStringFromSelector(sel));
}
我們使用signatureWithObjCTypes創(chuàng)建NSMethodSignature對(duì)象检访,這里需要傳一個(gè)參數(shù)始鱼,就是函數(shù)的編碼類型,由返回值類型脆贵、參數(shù)類型決定医清,可以參看官方的圖解:
例如我們定一個(gè)函數(shù)void sum(int x , int y)那么這個(gè)函數(shù)的編碼就為"vii",如果是void msgHand(id target , SEL selector)這個(gè)函數(shù)的編碼為"v@:"這種格式是消息實(shí)現(xiàn)體IMP常使用的格式。
以上代碼我們并沒(méi)有轉(zhuǎn)發(fā)消息卖氨,而是將不能處理的消息打印出來(lái)并swallow掉会烙,如果想實(shí)現(xiàn)轉(zhuǎn)發(fā)的話,可以轉(zhuǎn)換為如下代碼筒捺,只需要修改forwardInvocation的代碼就可以了:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(say)){
Student *stu = [[Student alloc] init];
[anInvocation invokeWithTarget:stu];
}
}
這樣就把消息轉(zhuǎn)發(fā)給了stu對(duì)象了柏腻,可以看出來(lái),OC的對(duì)象可以作為消息轉(zhuǎn)發(fā)的中心系吭,也可作為錯(cuò)誤消息的垃圾站五嫂,如上述實(shí)現(xiàn)。
3. 屬性定義
當(dāng)編譯器遇到屬性聲明時(shí)(@property)肯尺,將會(huì)生成一個(gè)關(guān)于此屬性的原型數(shù)據(jù)沃缘,我們可以通過(guò)系統(tǒng)的api獲取一個(gè)類、協(xié)議中的屬性以及其對(duì)應(yīng)的原型则吟。
typedef struct objc_property *objc_property_t;
屬性也是結(jié)構(gòu)體指針槐臀,但是該結(jié)構(gòu)體不可見(jiàn),只能通過(guò)相關(guān)函數(shù)獲取內(nèi)部的信息氓仲。
獲取一個(gè)類和協(xié)議的全部屬性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)//獲取一個(gè)類的全部屬性
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)//獲取協(xié)議中的全部屬性
const char *property_getName(objc_property_t property) //返回屬性名
const char *property_getAttributes(objc_property_t property) //返回屬性的編碼類型信息
以下函數(shù)獲取Person類的全部屬性和屬性的類型信息:
- (void)demo {
unsigned int num;
objc_property_t *properties = class_copyPropertyList([self class], &num); //1
for (int i = 0 ; i < num; i++) { //2
objc_property_t one = properties[i];
NSString *attrName = [NSString stringWithCString:property_getName(one) encoding:NSUTF8StringEncoding]; //3
NSString *typeString = [NSString stringWithCString:property_getAttributes(one) encoding:NSUTF8StringEncoding]; //4
NSLog(@"attr is %@ , type is %@",attrName , typeString);
}
free(properties);
}
1 處獲取Person類的全部屬性并保存在properties數(shù)組中水慨,并將數(shù)組長(zhǎng)度保存在num中。
2 循環(huán)遍歷properties
3 4 獲取屬性名和屬性的編碼類型信息
輸出結(jié)果:
attr is name , type is T@"NSString",&,N,Gname,V_name
attr is age , type is Tq,N,V_age
當(dāng)然我們可以在程序運(yùn)行的時(shí)候動(dòng)態(tài)地添加屬性:
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
屬性的類型信息:
以T開(kāi)始后接類型的@類型加一個(gè)','敬扛,由V_屬性名結(jié)束讥巡,中間就是該屬性的描述符,用,號(hào)隔開(kāi)舔哪。附贈(zèng)一張?zhí)O果官方的屬性類型編碼:
4. 實(shí)際使用
Runtime的使用比較多樣也比較靈活欢顷,但是比較流行的用法就是method swizzling,也就是互換方法體捉蚤,如下圖所示在進(jìn)行method swizzling前后selector和IMP之間的關(guān)系:
交換方法體之后的對(duì)應(yīng)關(guān)系抬驴,在實(shí)際中有什么作用呢,例如項(xiàng)目開(kāi)發(fā)了一大半缆巧,突然有一個(gè)采集數(shù)據(jù)的需求布持,需要每次用戶進(jìn)入頁(yè)面都要統(tǒng)計(jì)每個(gè)頁(yè)面進(jìn)入的次數(shù),由于項(xiàng)目已經(jīng)接近尾聲陕悬,再假如說(shuō)沒(méi)有做關(guān)于VC的同一調(diào)用接口题暖,一個(gè)個(gè)頁(yè)面修改起來(lái)就會(huì)很麻煩,如果我們能夠在所有的VC執(zhí)行viewDidAppear 時(shí)做一個(gè)其它的事情還不用每個(gè)頁(yè)面都修改這是最好的辦法,那么method swizzling就很適合你:
#import <UIKit/UIKit.h>
@interface UIViewController (Swizzle)
@end
#import "UIViewController+Swizzle.h"
#import <objc/runtime.h>
@implementation UIViewController (Swizzle)
+ (void)load{
SEL selDidAppear = @selector(viewDidAppear:);
Method impDidAppear = class_getInstanceMethod([self class], selDidAppear);
SEL selLog = @selector(logVC:);
Method impLog = class_getInstanceMethod([self class], selLog);
method_exchangeImplementations(impDidAppear, impLog);
}
- (void)logVC:(BOOL) nouse {
NSLog(@"進(jìn)入了頁(yè)面 %@", NSStringFromClass([self class]));
[self logVC:nouse];
}
@end
這樣VC的viewDidAppear和logVC的方法實(shí)現(xiàn)就交換了胧卤,當(dāng)系統(tǒng)調(diào)用viewDidAppear實(shí)際調(diào)用的是logVC的方法體唯绍,而調(diào)用logVC實(shí)際走的是viewDidAppear。在實(shí)際中的應(yīng)用還有很多例如數(shù)據(jù)統(tǒng)計(jì)枝誊、動(dòng)態(tài)加載代碼况芒、為類目添加屬性等。
項(xiàng)目代碼:<a >gitHub-iOSRuntime</a>