Objective-C runtime 機制

Runtime使用C語言結構體表示對象,用C語言函數表示方法,這些C語言函數和結構體被Runtime封裝後,咱們就能夠在程序中執行建立,檢查,修改類和對象和他們的方法面試

runtime
一、是由C、C++、彙編寫成的api
二、OC運行時,裝載到內存api

相對應的編譯時,源代碼翻譯緩存

OC SWIFT JAVA 高級語言,不被機器所識別,須要編譯成響應的機器語言,二進制併發

Objective-c程序有三種途徑和運行時系統交互
一、經過Objective-c源代碼,如@selector()
二、經過Foundation框架中NSObject的方法,如 iskindof
三、經過調用運行時系統給咱們提供的api接口,如objc_msgSend,objc_getClass框架

OC對象本質是結構體
調用方法就是發送消息 objc_msgSend
消息的組成:((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
第一個參數p消息的接收者,第二個參數sel_registerName("run")方法編號
imp 函數實現的指針,sel找到imp函數

查看關係圖atom

OC的Class實際上是一個objc_class結構體的指針,下面是Class類的定義spa

typedef struct objc_class *Class;

查看objc/runtime.h中objc_class結構體的定義以下翻譯

struct objc_class {
 Class isa OBJC_ISA_AVAILABILITY; //isa指針   
#if !__OBJC2__ Class
super_class OBJC2_UNAVAILABLE; // 父類
const char *name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本信息,默認爲0
long info OBJC2_UNAVAILABLE; // 類信息
long instance_size OBJC2_UNAVAILABLE; // 類佔據的內存大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 成員變量鏈表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法鏈表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法緩存列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協議鏈表
 #endif
 } OBJC2_UNAVAILABLE;

這個isa指針的指向就是該類對象的元類,每個類都是它的元類的對象,元類是對類對象的描述,就像類是普通實例對象的描述同樣。設計

每個類裏面聲明的類方法,其本質就是把該類方法放到元類的方法列表上面,因此類在調用類方法時,能夠想象成是元類的對象在調用一個實例方法。

A的父類是B,A的元類的父類是B的元類,B的父類是NSObject,NSObject的父類是nil,B元類的父類是NSObject的元類;特別注意的一點,NSObject的元類的父類是NSObject,NSObject的isa指針又指向NSObject的元類,因此在NSObject裏面的全部方法,NSObject的元類也都擁有,一、因此用NSObject 調用任意NSObject裏面的實例方法都是能夠成功的,

類和元類是一個閉環,實例指向類,類指向元類,元類指向跟元類,跟元類指向自身,根元類的父類是NSObject

元類是 Class 對象的類。每一個類(Class)都有本身獨一無二的元類(每一個類都有本身第一無二的方法列表)。這意味着全部的類對象都不一樣。

NSObject裏面的全部實力方法,任意類均可以經過類方法調用。

全部的meta-class使用基類的meta-class做爲本身的基類,對於頂層基類的meta-class也是同樣,只是它指向本身而已

 

[obj foo] 等同於 obj_msgSend(obj,@selector(foo))

objc 在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類,而後在該類的方法列表以及其父類方法列表中尋找方法運行。若是在層層的尋找中均位找到方法的實現,
 就會拋出unrecognized selector sent to XXX的異常,致使程序奔潰.
 在這奔潰前,oc運行時提供了三次拯救程序的機會
 
 一、Method resolution ,動態方法解析階段
 對應的具體方法是+(BOOL)resolveInstanceMethod:(SEL)sel 和+(BOOL)resolveClassMethod:(SEL)sel,
 當方法是實例方法時調用前者,當方法爲類方法時,調用後者。這個方法設計的目的是爲了給類利用 class_addMethod 添加方法的機會。

// void(*)()
// 默認方法都有兩個隱式參數,
void eat(id self,SEL sel)
{
    NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
pragma mark 消息轉發第一步(實例) 如此便達到了,當此類調用未定義的實例方法時,自動調用eat函數,而避免了崩潰的狀況。
// 當一個對象調用未實現的方法,會調用這個方法處理,而且會把對應的方法列表傳過來.
// 恰好能夠用來判斷,未實現的方法是否是咱們想要動態添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{

    if (sel == @selector(eat)) {
        // 動態添加eat方法

        // 第一個參數:給哪一個類添加方法
        // 第二個參數:添加方法的方法編號
        // 第三個參數:添加方法的函數實現(函數地址)
        // 第四個參數:函數的類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd
        class_addMethod(self, @selector(eat), (IMP)eat, "v@:");

    }

    return [super resolveInstanceMethod:sel];
}


 二、fowarding 方法轉發,備援接收者階段
 對象的具體方法是-(id)forwardingTargetForSelector:(SEL)aSelector ,
 此時,運行時詢問可否把消息轉給其餘接收者處理,也就是此時系統給了個將這個 SEL 轉給其餘對象的機會。

  #pragma mark 消息轉發第二步, 第一步失敗後執行                     
 #pragma mark 其實只要返回對象不爲self 和 nil 就會把消息轉發給返回的對象 
 - (id)forwardingTargetForSelector:(SEL)aSelector { 
 NSString * str = NSStringFromSelector(aSelector); 
 NSString * obj = [NSString stringWithFormat:@"testClass"]; 
 NSLog(@"方法 %@ 即將轉發給 Class %@",str,[obj class]); 
 return obj; 
 }


 三、 fowarding 方法轉發,完整消息轉發階段
 首先會調用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法,假若返回值爲nil,則runtime會發出doesNotRecognizeSelector:消息,引起異常,程序崩潰。若是返回了一個合理的函數簽名,Runtime就會建立一個NSInvocation對象併發送-forwardInvocation:消息給目標對象。參數 anInvocation 中包含未處理消息的各類信息(selector\target\參數...)。
 在這個方法中,能夠把 anInvocation 轉發給多個對象,與第二步不一樣,第二步只能轉給一個對象
 
 若是上述3個方法都沒有來處理這個消息,就會進入 NSObject 的-(void)doesNotRecognizeSelector:(SEL)aSelector方法中,拋出異常

總結一下整個消息轉發的流程:

 

代碼:

#import "ViewController.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UILabel *displayLabel;
- (IBAction)buttonTest:(UIButton *)sender;
@end

@implementation ViewController
- (IBAction)buttonTest:(UIButton *)sender {
    NSLog(@"--1--");
    [self performSelector:@selector(setText:) withObject:@"hello"];
}
+(BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"--2--");
    return NO;
}
//+(BOOL)resolveClassMethod:(SEL)sel
//{
//    NSLog(@"--2--");
//    return NO;
//}

-(id)forwardingTargetForSelector:(SEL)aSelector
{
    NSLog(@"--3--");
    return nil;
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"--4--");
   NSMethodSignature *signature= [super methodSignatureForSelector:aSelector];
    if (!signature) {
        signature=[self.displayLabel methodSignatureForSelector:aSelector];
    }
    
    return signature;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"--5--");
    SEL seletor=[anInvocation selector];
    if([self.displayLabel respondsToSelector:seletor]){
        [anInvocation invokeWithTarget:self.displayLabel];
    }
}
@end

 問題:那咱們只用最後一個接盤俠方法多好啊,爲何還須要前2個呢?
其實還與這3個方法的用途不一樣有關:
運行期添加方法,用1;
轉發給另1個對象、改變方法時,用2;
須要轉發給多個對象時,用3;

 

參考:

連接:iOS消息轉發機制詳解

連接:OC最實用的runtime總結,面試、工做你看我就足夠了!