iOS高性能OC三:Runtime Message

1.消息發(fā)送objc_msgSend
OC中在運行期決定調(diào)用什么方法,方法的調(diào)用轉換成C函數(shù)

//#import <objc/message.h>
    objc_msgSend(obj, @selector(messageName:), parameter);

這個函數(shù)的參數(shù)個數(shù)可變,第一個是接收消息的對象,第二個是方法名,后面的是參數(shù),順序和轉換前的OC方法一樣.

  • 接收到消息之后,這個函數(shù)會在類的方法列表中尋找,找不到會依據(jù)繼承體系向上尋找.找到之后,就會跳轉到方法的實現(xiàn)中.
    objc_msgSend還會把匹配到的結果緩存到一個fast map,快速映射表里面,這個緩存是在類里面的,也就是說,其實objc_msgSend是先在fast map中進行匹配的,然后才是方法列表.

  • 如果消息要返回結構體,那么需要使用objc_msgSend_stret
    如果消息返回浮點數(shù),那么需要使用objc_msgSend_fpret
    如果要給父類發(fā)送消息.需要使用objc_msgSendSuper,另外還有objc_msgSendSuper_stret和objc_msgSendSuper_fpret

  • OC的每一個方法都會轉換為C函數(shù),都是<return type>Class_selector(id self,SEL _cmd,...)的格式,每個類中有一張表,其中的指針會指向這些函數(shù),方法名就是key,objc_msgSend根據(jù)key找到對應函數(shù),OC有一種叫做尾調(diào)用優(yōu)化的技術,如果一個函數(shù)的結尾是調(diào)用另一個函數(shù)并且沒有返回值的時候,編譯器會生成跳轉至另一函數(shù)的指令,這個操作省去了將另一個函數(shù)推入新的棧幀,避免過早出現(xiàn)棧溢出.

2.消息轉發(fā)
前面說到消息的傳遞,如果objc_msgSend最終都沒有找到對應的函數(shù),也就是對象收到了無法解讀的消息,這時候就會啟動消息轉發(fā)機制.
分為兩大階段,第一階段,先看類能不能動態(tài)添加方法來處理這條消息,叫做動態(tài)方法解析;第二階段,第一步先找有沒有其他對象可以處理,如果有則轉發(fā)給那個對象,如果沒有則啟動完整消息轉發(fā),消息會被封裝到NSInvocation對象中,再一次詢問接受者能否處理.

  • 動態(tài)方法解析
    對象在接收到無法解讀的消息時,會調(diào)用+ (BOOL)resolveInstanceMethod:(SEL)sel;sel就是那個無法解讀的選擇器,返回bool類型,表示能否新增一個方法來處理這個選擇器,如果這個選擇器是一個類方法,則調(diào)用+ (BOOL)resolveClassMethod:(SEL)sel方法,這兩個方法需要在類中重寫.
    為對象增加方法調(diào)用BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
    Class cls即類對象, SEL name是選擇器,也就是方法名,IMP imp是方法的具體實現(xiàn),也就是C函數(shù),const char *types是函數(shù)類型,例如"v@:"是無參無返回值(v即void),"i@:@",是有參有一個int型返回值
void newMethod(id self, SEL _cmd, id value){
    NSLog(@"this is new method -- %@",value);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"undefind method");
    class_addMethod(self, sel, (IMP)newMethod, "v@:@");
    return YES;
}

Example *exam = [[Example alloc]init];
    NSString *exstr = (NSString *)exam;
    [exstr isEqualToString:@"abc"];

這是一個繼承自NSObject的類,將其對象轉換成NSString,然后調(diào)用isEqualToString方法,顯然這個類是沒有這個方法的,于是就會走resolveInstanceMethod方法,還能看到"abc"打印出來,如果調(diào)用的方法沒有參數(shù),打印出來的就是null


打印出參數(shù)

并且resolveInstanceMethod可以用于實現(xiàn)@dynamic的setter和getter,CALayer可以添加屬性也是這么添加屬性的

  • 備用接收者,快速轉發(fā)路徑
    如果沒有實現(xiàn)resolveInstanceMethod處理,運行時系統(tǒng)會尋找一個備用的接收者來處理這條消息,對應方法是- (id)forwardingTargetForSelector:(SEL)aSelector; 返回一個對象來處理消息.
- (id)forwardingTargetForSelector:(SEL)aSelector{
    return @"abc";
}

Example *exam = [[Example alloc]init];
    NSString *exstr = (NSString *)exam;
    if([exstr isEqualToString:@"abc"]){
        NSLog(@"forward target");
    };
打印結果
  • Invocation,慢速轉發(fā)路徑
    如果上面的流程任然沒有處理未知消息,系統(tǒng)會創(chuàng)建一個NSInvocation對象對應方法是- (void)forwardInvocation:(NSInvocation *)anInvocation;
    運行時調(diào)用對象的方法可以使用performSelector:withObject;但是這個方法不能處理返回值,參數(shù)也最多傳2個(performSelector:<#(SEL)#> withObject:<#(id)#> withObject:<#(id)#>),NSInvocation可以將消息封裝起來,包含選擇器,目標,和參數(shù).
    首先要實現(xiàn)methodSignatureForSelector,返回一個簽名,簽名指定一個選擇器,實現(xiàn)了這個方法,才會調(diào)用forwardInvocation,在forwardInvocation中,還可以修改參數(shù)等,這兩個方法的目的是修改無法響應的消息中的信息,達到可以處理的目的.
//首先要實現(xiàn)這個方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if(![self respondsToSelector:aSelector]){
        //創(chuàng)建一個NSMethodSignature 綁定需要轉換的方法
        return [[self class] instanceMethodSignatureForSelector:@selector(newMethod:)];
    }
   return nil;
}

//如果實現(xiàn)了methodSignatureForSelector 會走這個方法
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //NSMethodSignature的方法要和anInvocation一致,這一步還可以修改參數(shù)
    anInvocation.selector = @selector(newMethod:);
    [anInvocation invoke]; //  執(zhí)行更換后的方法
}

- (void)newMethod:(NSString *)str{
    NSLog(@"newMethod string = %@",str);
}

如果想要通過更換選擇器來處理未知消息,[anInvocation invoke]也可以不執(zhí)行,或者forwardInvocation里什么都不寫,相當于忽略了這條消息.但是無論forwardInvocation里寫不寫代碼,methodSignatureForSelector中一定要換掉消息的選擇器.

NSInvocation的詳細使用方法如下:

//NSInvocation;用來包裝方法和對應的對象,它可以存儲方法的名稱,對應的對象,對應的參數(shù),
    /*
     NSMethodSignature:簽名:再創(chuàng)建NSMethodSignature的時候,必須傳遞一個簽名對象,簽名對象的作用:用于獲取參數(shù)的個數(shù)和方法的返回值
     */
    //創(chuàng)建簽名對象的時候不是使用NSMethodSignature這個類創(chuàng)建,而是方法屬于誰就用誰來創(chuàng)建,是NSObject的方法
    NSMethodSignature*signature = [[self class] instanceMethodSignatureForSelector:@selector(sendMessageWithNumber:WithContent:)];
    //1、創(chuàng)建NSInvocation對象
    NSInvocation*invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    //invocation中的方法必須和簽名中的方法一致。
    invocation.selector = @selector(sendMessageWithNumber:WithContent:);
    /*第一個參數(shù):需要給指定方法傳遞的值
           第一個參數(shù)需要接收一個指針,也就是傳遞值的時候需要傳遞地址*/
    //第二個參數(shù):需要給指定方法的第幾個參數(shù)傳值
    NSString*number = @"1111";
    //注意:設置參數(shù)的索引時不能從0開始,因為0已經(jīng)被self占用,1已經(jīng)被_cmd占用
    [invocation setArgument:&number atIndex:2];
    NSString*number2 = @"啊啊啊";
    [invocation setArgument:&number2 atIndex:3];
    //2、調(diào)用NSInvocation對象的invoke方法
    //只要調(diào)用invocation的invoke方法,就代表需要執(zhí)行NSInvocation對象中指定對象的指定方法,并且傳遞指定的參數(shù)
    [invocation invoke];
消息轉發(fā)流程

在這個流程中,resolveInstanceMethod返回YES也不一定會結束,關鍵看這個方法中,是否添加了函數(shù)去處理這個消息,如果什么都不寫,直接reture YES也一樣會執(zhí)行后續(xù)的流程.

3.交換調(diào)配method swizzling

  • 類的方法列表會把選擇器的名稱,也就是方法名映射到方法實現(xiàn)上,這些方法均以函數(shù)指針的形式來表示,這種指針叫做IMP.因此我們可以在運行時去修改選擇器對應的函數(shù)指針,可以新增選擇器,可以修改方法實現(xiàn),可以互換方法實現(xiàn).
//函數(shù)指針
id (*IMP)(id,SEL,...)

/*互換方法實現(xiàn)*/
//獲取方法實現(xiàn)
    Method method1 = class_getInstanceMethod(NSString.class, @selector(ex_uppercaseString));
    Method method2 = class_getInstanceMethod(NSString.class, @selector(uppercaseString));
    //交換方法實現(xiàn)
    method_exchangeImplementations(method1, method2);

//擴展方法,寫在分類中
- (NSString *)ex_uppercaseString{
    NSString *str = [self ex_uppercaseString];
    /*
     ...
     */
    NSLog(@"擴展方法--%@",str);
    return str;
}

這個方法可以將那些看不到源碼的黑盒方法進行一些擴展,例如增加日志,有助于調(diào)試.

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容