C語(yǔ)言:調(diào)用函數(shù)的語(yǔ)言在聲明完函數(shù)后,如果沒有實(shí)現(xiàn)函數(shù),程序是無法編譯通過的属提。
OC:程序是可以編譯通過的,但是會(huì)有一個(gè)黃色的警告权逗。只有當(dāng)程序運(yùn)行之后才會(huì)出現(xiàn)如下的崩潰信息:
+[Function max::]: unrecognized selector sent to class 0x1000010f8***
Terminating app due to uncaught exception 'NSInvalidArgumentException'...
消息傳遞和調(diào)用函數(shù)對(duì)于程序員來說最大的區(qū)別就在于源代碼編譯的過程中是否能夠編譯通過.
解釋消息傳遞機(jī)制的原理就要用到OC語(yǔ)言中的運(yùn)行時(shí)系統(tǒng)(Runtime)了.
什么是Runtime
runtime是一個(gè)c和匯編寫的動(dòng)態(tài)庫(kù),它就像一個(gè)小小的系統(tǒng),將OC和C緊密關(guān)聯(lián)斟薇。這個(gè)系統(tǒng)主要做兩件事 :
1火惊、封裝C語(yǔ)言的結(jié)構(gòu)體和函數(shù),讓開發(fā)者在運(yùn)行時(shí)創(chuàng)建奔垦、檢查或者修改類、對(duì)象和方法等等尸疆。
2椿猎、傳遞消息,找出方法的最終執(zhí)行代碼寿弱。
那么我們可以利用這些方法干點(diǎn)什么犯眠?
1、遍歷對(duì)象的屬性
2症革、動(dòng)態(tài)添加/修改屬性筐咧,動(dòng)態(tài)添加/修改/替換方法
3、動(dòng)態(tài)創(chuàng)建類/對(duì)象/協(xié)議等等
4噪矛、方法攔截調(diào)用
運(yùn)行時(shí)系統(tǒng)是一個(gè)提供一系列公開函數(shù)接口以及數(shù)據(jù)結(jié)構(gòu)的動(dòng)態(tài)鏈接庫(kù)量蕊,這些頭文件位于/usr/include/objc。許多這些函數(shù)允許你使用純C語(yǔ)言重寫當(dāng)你寫OC代碼后編譯器做的事情艇挨。其他形式的接口則是通過NSObject類中定義的一些方法残炮。這些方法是可以用來實(shí)現(xiàn)其他的運(yùn)行時(shí)接口來提高運(yùn)行效率。但是重寫運(yùn)行時(shí)的代碼對(duì)于使用OC語(yǔ)言進(jìn)行編程并非是必須的缩滨,但是势就,少數(shù)的運(yùn)行時(shí)函數(shù)在一些特殊情況下,對(duì)于OC程序還是很有用途的脉漏。
我們寫的代碼在程序運(yùn)行過程中都會(huì)被轉(zhuǎn)化成runtime的C代碼執(zhí)行苞冯,例如
[target doSomething];
會(huì)被轉(zhuǎn)化成objc_msgSend(target, @selector(doSomething));
。
OC中一切都被設(shè)計(jì)成了對(duì)象侧巨,我們都知道一個(gè)類被初始化成一個(gè)實(shí)例舅锄,這個(gè)實(shí)例是一個(gè)對(duì)象。實(shí)際上一個(gè)類本質(zhì)上也是一個(gè)對(duì)象刃泡,在runtime中用結(jié)構(gòu)體表示巧娱。
如果消息的接收者能夠找到對(duì)應(yīng)的selector,那么就相當(dāng)于直接執(zhí)行了接收者這個(gè)對(duì)象的特定方法烘贴;否則禁添,消息要么被轉(zhuǎn)發(fā),或是臨時(shí)向接收者動(dòng)態(tài)添加這個(gè)selector對(duì)應(yīng)的實(shí)現(xiàn)內(nèi)容桨踪,要么就干脆玩完崩潰掉老翘。
現(xiàn)在可以看出[receiver message]真的不是一個(gè)簡(jiǎn)簡(jiǎn)單單的方法調(diào)用。因?yàn)檫@只是在編譯階段確定了要向接收者發(fā)送message這條消息,而receive將要如何響應(yīng)這條消息铺峭,那就要看運(yùn)行時(shí)發(fā)生的情況來決定了墓怀。
Objective-C 的 Runtime 鑄就了它動(dòng)態(tài)語(yǔ)言的特性。Objc Runtime使得C具有了面向?qū)ο竽芰ξ兰诔绦蜻\(yùn)行時(shí)創(chuàng)建傀履,檢查,修改類莉炉、對(duì)象和它們的方法钓账。可以使用runtime的一系列方法實(shí)現(xiàn)絮宁。
相關(guān)的定義:
/// 描述類中的一個(gè)方法
typedef struct objc_method *Method;
/// 實(shí)例變量
typedef struct objc_ivar *Ivar;
/// 類別Category
typedef struct objc_category *Category;
/// 類中聲明的屬性
typedef struct objc_property *objc_property_t;
類在runtime中的表示
//類在runtime中的表示
structobjc_class {
Class isa;//指針梆暮,顧名思義,表示是一個(gè)什么绍昂,實(shí)例的isa指向類對(duì)象啦粹,類對(duì)象的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;//緩存: 一種優(yōu)化,調(diào)用過的方法存入緩存列表窘游,下次調(diào)用先找緩存
struct objc_protocol_list *protocols//協(xié)議列表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
向object發(fā)送消息時(shí)唠椭,Runtime庫(kù)會(huì)根據(jù)object的isa指針找到這個(gè)實(shí)例object所屬于的類,然后在類的方法列表以及父類方法列表尋找對(duì)應(yīng)的方法運(yùn)行忍饰。id是一個(gè)objc_object結(jié)構(gòu)類型的指針泪蔫,這個(gè)類型的對(duì)象能夠轉(zhuǎn)換成任何一種對(duì)象。
然后再來看看消息發(fā)送的函數(shù):objc_msgSend函數(shù)
在引言中已經(jīng)對(duì)objc_msgSend進(jìn)行了一點(diǎn)介紹喘批,看起來像是objc_msgSend返回了數(shù)據(jù)撩荣,其實(shí)objc_msgSend從不返回?cái)?shù)據(jù)而是你的方法被調(diào)用后返回了數(shù)據(jù)。下面詳細(xì)敘述下消息發(fā)送步驟:
檢測(cè)這個(gè) selector 是不是要忽略的饶深。比如 Mac OS X 開發(fā)餐曹,有了垃圾回收就不理會(huì) retain,release 這些函數(shù)了。
檢測(cè)這個(gè) target 是不是 nil 對(duì)象敌厘。ObjC 的特性是允許對(duì)一個(gè) nil 對(duì)象執(zhí)行任何一個(gè)方法不會(huì) Crash台猴,因?yàn)闀?huì)被忽略掉。
如果上面兩個(gè)都過了俱两,那就開始查找這個(gè)類的 IMP饱狂,先從 cache 里面找,完了找得到就跳到對(duì)應(yīng)的函數(shù)去執(zhí)行宪彩。
如果 cache 找不到就找一下方法分發(fā)表休讳。
如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找尿孔,直到找到NSObject類為止俊柔。
如果還找不到就要開始進(jìn)入動(dòng)態(tài)方法解析了筹麸,后面會(huì)提到。
方法調(diào)用
讓我們看一下方法調(diào)用在運(yùn)行時(shí)的過程
如果用實(shí)例對(duì)象調(diào)用實(shí)例方法雏婶,會(huì)到實(shí)例的isa指針指向的對(duì)象(也就是類對(duì)象)操作物赶。
如果調(diào)用的是類方法,就會(huì)到類對(duì)象的isa指針指向的對(duì)象(也就是元類對(duì)象)中操作留晚。
- 首先酵紫,在相應(yīng)操作的對(duì)象中的緩存方法列表中找調(diào)用的方法,如果找到错维,轉(zhuǎn)向相應(yīng)實(shí)現(xiàn)并執(zhí)行憨闰。
- 如果沒找到,在相應(yīng)操作的對(duì)象中的方法列表中找調(diào)用的方法需五,如果找到,轉(zhuǎn)向相應(yīng)實(shí)現(xiàn)執(zhí)行
- 如果沒找到轧坎,去父類指針?biāo)赶虻膶?duì)象中執(zhí)行1宏邮,2.
- 以此類推,如果一直到根類還沒找到缸血,轉(zhuǎn)向攔截調(diào)用蜜氨。
- 如果沒有重寫攔截調(diào)用的方法,程序報(bào)錯(cuò)捎泻。
以上的過程給我?guī)淼膯l(fā):
- 重寫父類的方法飒炎,并沒有覆蓋掉父類的方法,只是在當(dāng)前類對(duì)象中找到了這個(gè)方法后就不會(huì)再去父類中找了笆豁。
- 如果想調(diào)用已經(jīng)重寫過的方法的父類的實(shí)現(xiàn)郎汪,只需使用super這個(gè)編譯器標(biāo)識(shí),它會(huì)在運(yùn)行時(shí)跳過在當(dāng)前的類對(duì)象中尋找方法的過程闯狱。
攔截調(diào)用
在方法調(diào)用中說到了煞赢,如果沒有找到方法就會(huì)轉(zhuǎn)向攔截調(diào)用。
那么什么是攔截調(diào)用呢哄孤。
攔截調(diào)用就是照筑,在找不到調(diào)用的方法程序崩潰之前,你有機(jī)會(huì)通過重寫NSObject的四個(gè)方法來處理瘦陈。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后兩個(gè)方法需要轉(zhuǎn)發(fā)到其他的類處理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation*)anInvocation;
- 第一個(gè)方法是當(dāng)你調(diào)用一個(gè)不存在的類方法的時(shí)候凝危,會(huì)調(diào)用這個(gè)方法,默認(rèn)返回NO晨逝,你可以加上自己的處理然后返回YES蛾默。
- 第二個(gè)方法和第一個(gè)方法相似,只不過處理的是實(shí)例方法捉貌。
- 第三個(gè)方法是將你調(diào)用的不存在的方法重定向到一個(gè)其他聲明了這個(gè)方法的類趴生,只需要你返回一個(gè)有這個(gè)方法的target阀趴。
- 第四個(gè)方法是將你調(diào)用的不存在的方法打包成
NSInvocation
傳給你。做完你自己的處理后苍匆,調(diào)用invokeWithTarget:
方法讓某個(gè)target觸發(fā)這個(gè)方法刘急。
動(dòng)態(tài)添加方法
重寫了攔截調(diào)用的方法并且返回了YES,我們要怎么處理呢浸踩?有一個(gè)辦法是根據(jù)傳進(jìn)來的SEL
類型的selector動(dòng)態(tài)添加一個(gè)方法叔汁。
首先從外部隱式調(diào)用一個(gè)不存在的方法:
//隱式調(diào)用方法
[target performSelector:@selector(resolveAdd:) withObject:@"test"];
然后,在target對(duì)象內(nèi)部重寫攔截調(diào)用的方法检碗,動(dòng)態(tài)添加方法据块。
void runAddMethod(id self, SEL _cmd, NSString *string){
NSLog(@"add C IMP ", string);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
//給本類動(dòng)態(tài)添加一個(gè)方法
if ([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]) {
class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
}
return YES;
}
其中class_addMethod
的四個(gè)參數(shù)分別是:
-
Class cls
給哪個(gè)類添加方法,本例中是self -
SEL name
添加的方法折剃,本例中是重寫的攔截調(diào)用傳進(jìn)來的selector另假。 -
IMP imp
方法的實(shí)現(xiàn),C方法的方法實(shí)現(xiàn)可以直接獲得怕犁。如果是OC方法边篮,可以用+(IMP)instanceMethodForSelector:(SEL)aSelector;
獲得方法的實(shí)現(xiàn)。 -
"v@:*"
方法的簽名奏甫,代表有一個(gè)參數(shù)的方法戈轿。
關(guān)聯(lián)對(duì)象
現(xiàn)在你準(zhǔn)備用一個(gè)系統(tǒng)的類,但是系統(tǒng)的類并不能滿足你的需求阵子,你需要額外添加一個(gè)屬性思杯。
這種情況的一般解決辦法就是繼承。
但是挠进,只增加一個(gè)屬性色乾,就去繼承一個(gè)類,總是覺得太麻煩類领突。
這個(gè)時(shí)候杈湾,runtime的關(guān)聯(lián)屬性就發(fā)揮它的作用了。
你還可以把添加和獲取關(guān)聯(lián)對(duì)象的方法寫在你需要用到這個(gè)功能的類的類別中攘须,方便使用漆撞。
方法交換
方法交換,顧名思義于宙,就是將兩個(gè)方法的實(shí)現(xiàn)交換浮驳。例如,將A方法和B方法交換捞魁,調(diào)用A方法的時(shí)候至会,就會(huì)執(zhí)行B方法中的代碼,反之亦然谱俭。
方法交換應(yīng)該被保證奉件,在程序中只會(huì)執(zhí)行一次
應(yīng)用實(shí)例
1宵蛀、Json到Model的轉(zhuǎn)化
在開發(fā)中相信最常用的就是接口數(shù)據(jù)需要轉(zhuǎn)化成Model了(當(dāng)然如果你是直接從Dict取值的話。县貌。术陶。),很多開發(fā)者也都使用著名的第三方庫(kù)如JsonModel煤痕、Mantle或MJExtension等梧宫,下面我們使用runtime去解析json來給Model賦值。
原理描述:用runtime提供的函數(shù)遍歷Model自身所有屬性摆碉,如果屬性在json中有對(duì)應(yīng)的值塘匣,則將其賦值。
核心方法:在NSObject的分類中添加方法:
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
//(1)獲取類的屬性及屬性對(duì)應(yīng)的類型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通過property_getName函數(shù)獲得屬性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通過property_getAttributes函數(shù)可以獲得屬性的名字和@encode編碼
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute]; }
//立即釋放properties指向的內(nèi)存
free(properties);
//(2)根據(jù)類型給屬性賦值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;
}
2巷帝、快速歸檔
有時(shí)候我們要對(duì)一些信息進(jìn)行歸檔忌卤,如用戶信息類UserInfo,這將需要重寫initWithCoder和encodeWithCoder方法楞泼,并對(duì)每個(gè)屬性進(jìn)行encode和decode操作驰徊。那么問題來了:當(dāng)屬性只有幾個(gè)的時(shí)候可以輕松寫完,如果有幾十個(gè)屬性呢现拒?
原理描述:用runtime提供的函數(shù)遍歷Model自身所有屬性,并對(duì)屬性進(jìn)行encode和decode操作望侈。
核心方法:在Model的基類中重寫方法:
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
3印蔬、訪問私有變量
我們知道,OC中沒有真正意義上的私有變量和方法脱衙,要讓成員變量私有侥猬,要放在m文件中聲明,不對(duì)外暴露捐韩。如果我們知道這個(gè)成員變量的名稱退唠,可以通過runtime獲取成員變量,再通過getIvar來獲取它的值荤胁。
方法:
Ivar ivar = class_getInstanceVariable([Model class], "_str1");
NSString * str1 = object_getIvar(model, ivar);