OC調(diào)用方法的機(jī)制其實就是消息轉(zhuǎn)發(fā)機(jī)制,最終都是轉(zhuǎn)成objc_msgSend
的函數(shù)調(diào)用赔退。接下來我們就一起
我們先來看下類的底層結(jié)構(gòu)![
消息發(fā)送
第一步: 查詢接收者是否為空,如果為空钢颂,直接退出经窖,如果不為空走第二步
第二步: 查詢緩存cash
中是否有該方法,如果有則調(diào)用方法,如果沒有走第三步彭沼。值的注意的是緩存中存儲方法使用散列表
列表的方式存儲的缔逛。
第三步: 查詢class_rw_t
中methods
中是否有該方法,如果有則調(diào)用方法,并且添加到緩存中姓惑,如果沒有走第四步褐奴。值的注意的是,這個查詢方法是根據(jù)該方法列表是否已經(jīng)排好序了于毙,如果已經(jīng)排序則用二分法查找敦冬,如果沒有則是遍歷查找
第四步: 通過superclass
查詢父類否有該方法,如果有則調(diào)用方法,并且添加到自己的緩存中唯沮,如果沒有繼續(xù)調(diào)用第四步脖旱,如果查找到基類都沒有這個方法,那么就執(zhí)行第二階段動態(tài)解析
介蛉。值的注意的是萌庆,這一步的查找方式是執(zhí)行的父類中第二步跟第三步。
動態(tài)解析
一旦消息發(fā)送階段沒有找到方法币旧,那么就會執(zhí)行動態(tài)解析階段践险,會調(diào)用兩個方法
對象方法
調(diào)用 + (BOOL)resolveInstanceMethod:(SEL)sel
類方法
調(diào)用+ (BOOL)resolveClassMethod:(SEL)sel
這兩個方法基本一致,這里我們介紹一下對象方法
.h
@interface DDPerson : NSObject
- (void)eat;
@end
.m
#import "DDPerson.h"
#import <objc/runtime.h>
@implementation DDPerson
- (void)test{
NSLog(@"調(diào)用了test方法");
}
// 這個是動態(tài)添加對象方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(eat)) {
// 獲取要動態(tài)添加的方法
Method mothod = class_getInstanceMethod(self, @selector(test));
// 獲取添加方法的Imp也就是方法地址
IMP imp = method_getImplementation(mothod);
// 獲取字符串編碼 typeEconding
const char *types = method_getTypeEncoding(mothod);
// 添加方法
class_addMethod(self, sel, imp, types);
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
DDPerson *person = [[DDPerson alloc] init];
[person eat];
}
return 0;
}
代碼分析
.h
文件中我們聲明了eat
對象方法吹菱,但是在.m
文件中我們沒有實現(xiàn)巍虫,我們只加入了test
方法。當(dāng)程序自動調(diào)用resolveInstanceMethod
時,我們可以通過調(diào)用class_addMethod
像對象添加一個動態(tài)方法鳍刷,也就是說真正要調(diào)用的test
方法占遥。class_addMethod
參數(shù)介紹:
cls
:第一個參數(shù),給誰發(fā)送消息输瓜。如果是對象方法瓦胎,那么這里傳入接收者的類對象,如果是類方法前痘,那么這里傳入的是接收者的元類對象凛捏。
SEL
:第二個參數(shù),動態(tài)添加的方法名稱
IMP
:第三個參數(shù)芹缔,動態(tài)添加的方法地址
types
:第四個參數(shù)坯癣,動態(tài)添加的方法的字符串編碼
當(dāng)調(diào)用eat
方法時的執(zhí)行結(jié)果:
2021-05-26 09:29:30.976106+0800 消息轉(zhuǎn)發(fā)[19393:703271] 調(diào)用了test方法
Program ended with exit code: 0
??????
1.
這里動態(tài)添加方法是添加到了類或者是元類中的class_rw_t
中的methods
方法列表中,而沒有存到方法的緩存中最欠。添加之后會重新走消息發(fā)送階段
2.
動態(tài)解析階段示罗,允許用戶調(diào)用其他類的方法惩猫,我們只需把消息接收者改變?yōu)閷?yīng)類就行了,比如我們想要調(diào)用Student的study方法蚜点,我們只要把消息接收這改為Student方法改為study就可以了Method mothod = class_getInstanceMethod([DDStudent class], @selector(study));
3.
動態(tài)解析過后轧房,程序把這個方法標(biāo)記為已經(jīng)動態(tài)解析,之后又會重新走第一個階段(消息發(fā)送)
消息轉(zhuǎn)發(fā)
如果沒有實現(xiàn)第二階段的任何方法绍绘,也就是沒有添加任何新方法奶镶,那么方法調(diào)用來到第三階段,消息轉(zhuǎn)發(fā)階段陪拘。
第一步: 調(diào)用 forwardingTargetForSelector
- (id)forwardingTargetForSelector:(SEL)aSelector {
// 這里返回的是一個對象厂镇,就是想要把消息轉(zhuǎn)發(fā)給的對象
// 比如有一個對象Student中 也有一個同樣的調(diào)用方法 那么我們這里就可以直接返回這個對象就可以了
if (aSelector == @selector(eat)) {
return [[DDStudent alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
?????? 返回對象中必須有一個跟你調(diào)用方法一樣的方法,才能返回這個對象否則程序會調(diào)用
doesNotRecognizeSelector
方法左刽,終止程序運行捺信,并且提示unrecognized selector sent to instance 0x100615cc0'
這個常見提示
第二步: 調(diào)用 methodSignatureForSelector
如果在第一步?jīng)]有調(diào)用forwardingTargetForSelector
或者返回值是nil
那么就執(zhí)行第二步
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(eat)) {
// 創(chuàng)建NSMethodSignature實例
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:i"];
return signatrure;
}
return [super methodSignatureForSelector:aSelector];
}
這里需要返回一個方法的簽名,方法簽名需要傳遞一個char
類型字符串欠痴,
字符串中第一個字符是返回值v
代表void
迄靠,接下來依次是方法傳遞的參數(shù)@
表示對象,:
代表地址喇辽,i
代表int
類型
這三步: 如果執(zhí)行了第二步就必須實現(xiàn) forwardInvocation
掌挚,并且最終執(zhí)行也是在這個方法中,NSInvocation
這個對象中包含方法調(diào)用者 方法名 方法參數(shù)
// anInvocation 包含方法調(diào)用者 方法名 方法參數(shù)
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"喲呵呵呵 呦呵喲呵");
// 切換方法調(diào)用者
[anInvocation invokeWithTarget:[[DDStudent alloc] init]];
// 獲取指定參數(shù) 注意這里
int age;
[anInvocation getArgument:&age atIndex:2];
}
獲取參數(shù):調(diào)用獲取參數(shù)方法的是后茵臭,索引值就是方法傳遞參數(shù)的索引疫诽,由于每一個方法都包含兩個默認(rèn)參數(shù)
id
類型 還有一個SEL
方法名舅世,所以我們自己傳入的參數(shù)索引只是從2開始的旦委。還有一點需要注意,索引值應(yīng)該小于參數(shù)個數(shù)雏亚,否則會報數(shù)組越界錯誤缨硝。
拋出異常
如果上述三個階段都沒有找到調(diào)用方法那么程序執(zhí)行doesNotRecognizeSelector
方法,終止程序運行罢低,并且提示unrecognized selector sent to instance 0x100615cc0'
這個常見提示
點擊這里查看總結(jié)查辩。 大家加油!M帧宜岛!