iOS進階之Runtime運行時(方法執行過程、攔截調用)

目錄

前言

OC中發送消息:使用中括號把接收者和消息括起來。直到運行時才會把消息與方法實現綁定。
[person run]; 會被編譯器轉為 objc_msgSend(person,@selector(run));

objc_msgSend

/*
objc_msgSend(person, @selector(run))       
消息發送步驟:
  1. 檢測 SEL 是否應該被忽略
  2. 檢測 target 是否是nil ,為nil則忽略該消息。
      向nil發消息(不會崩)
        返回值是對象,返 nil
        返回值為指針類型,返回 0。
        返回值為結構體,返回 0(結構體中各個字段的值將都是0)。
        返回值以上都不是,返回 值未定義
  3. 查找IMP
    具體參見下面第1小節
*/

id objc_msgSend(id self,SEL op, ...);     
/*
根據實例對象和SEL 尋找 IMP
    // 第一個參數: id
    typedef struct objc_object *id;
    // 第二個參數: SEL
    typedef struct objc_selector *SEL;   
    // 第三個參數: 方法參數列表
*/

SEL

方法名的唯一標識
    一個方法名對應一個SEL(不同類相同方法的SEL相同)。
    OC中不支持函數重載就是因為一個類的方法列表中不能存在兩個相同的 SEL 。
    SEL x1=@selector(method:);       
    SEL x2=NSSelectorFromString(@"method:");
    SEL x3=sel_registerName("method:");

typedef struct objc_selector *SEL;
struct objc_selector {
    char *name;                       OBJC2_UNAVAILABLE;
    char *types;                      OBJC2_UNAVAILABLE;
};

IMP

方法具體實現的地址  

    typedef id (*IMP)(id, SEL, ...); (類實例或元類,SEL,參數)  函數地址
/*
instance -> class -> method -> SEL -> IMP -> 實現函數
實例對象中存放 isa 指針以及實例變量,有 isa 指針可以找到實例對象所屬的類對象 (類也是對象,面向對象中一切都是對象),類中存放著實例方法列表,在這個方法列表中 SEL 作為 key,IMP 作為 value。 在編譯時期,根據方法名字會生成一個唯一的 Int 標識,這個標識就是 SEL。IMP 其實就是函數指針 指向了最終的函數實現。整個 Runtime 的核心就是 objc_msgSend 函數,通過給類發送 SEL 以傳遞消息,找到匹配的 IMP 再獲取最終的實現。

類中的 super_class 指針可以追溯整個繼承鏈。向一個對象發送消息時,Runtime 會根據實例對象的 isa 指針找到其所屬的類,并自底向上直至根類(NSObject)中 去尋找 SEL 所對應的方法實現,找到后就運行。

metaClass是元類,也有 isa 指針、super_class 指針。其中保存了類方法列表。
*/
對象的內存布局

1. 方法執行過程

1: 首先,在對象的緩存方法列表中尋找被調用的方法,如果找到則轉向相應方法實現并執行。
    objc_cache
2: 如果沒找到,在對象中的方法列表中尋找被調用的方法,如果找到則存添加到緩存列表并轉向相應方法實現并執行。 
    objc_method_list
3: 如果沒找到,去父類中繼續執行1,2。
4: 以此類推,如果一直到根類還沒找到則轉向【攔截調用】。
5: 如果沒有重寫攔截調用的方法,程序報錯。

2. 攔截調用(unrecognized selector崩潰前的一道關卡)

三次補救

當找不到相應方法,在未識別方法崩潰之前會轉向攔截調用(有三次補救機會)。

注意:
  1. 有些個第三方內部也會實現下面幾個方法,如果不小心被自己寫的方法覆蓋會出錯

》》》》實例方法

當調用不存在的實例方法時首先會調用方法1

方法1 (默認返回false,然后去調用方法2(方法2未實現則崩潰))
    // 可添加方法后返回true
    + (BOOL)resolveInstanceMethod:(SEL)aSEL{
        if (aSEL == @selector(goToSchool:)) {
            class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:aSEL];
    }
方法2 
    // 轉發給擁有該方法的實例
    // 返回nil 則調用3(3未實現則崩)
    - (id)forwardingTargetForSelector:(SEL)aSelector{
        if(aSelector == @selector(mysteriousMethod:)){
            return [Person new];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
方法3 
    // 作相應處理后,調用invokeWithTarget:將invocation傳給擁有該方法的實例
    - (void)forwardInvocation:(NSInvocation *)anInvocation{
        anInvocation.target=nil;
        [anInvocation invocation];
        /*
        // invocation封裝了原始的消息和消息的參數
        Person *person=[Person new];
        if ([person respondsToSelector:[anInvocation selector]]){
            [anInvocation invokeWithTarget: person];
        }else{
            [super forwardInvocation:anInvocation];
        }
        */
    }
    /* 
    調用forwardInvocation:之前,會先調用methodSignatureForSelector:,并取到返回的方法簽名用于生成NSInvocation對象。
    在重寫forwardInvocation:的同時必須重寫methodSignatureForSelector:獲取函數的參數和返回值,系統會自動創建NSInvocation并調用forwardInvocation:,否則會拋異常。
    返回nil會拋異常。
    */
    - (NSMethodSignature*)methodSignatureForSelector:(SEL)selector{
        NSMethodSignature* signature = [super methodSignatureForSelector:selector];
        if (!signature) {
            signature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
        }
        return signature;
    }

》》》》類方法

1 
    // 當調用不存在的類方法時調用(默認:false,調用2(2未實現則崩))
    // 可作相應處理后返回true
    + (BOOL)resolveClassMethod:(SEL)sel {
        if (sel == @selector(learnClass:)) {
            class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
            return YES;
        }
        return [class_getSuperclass(self) resolveClassMethod:sel];
    }
2

例1

請求網絡獲取數據的時候會經常需要:responseObsect[@"data"][@"persons"]
萬一后端數據返回的data數據類型不是字典還是數組,會導致未識別方法崩潰。

解決:在NSArray分類中添加
+(BOOL)resolveInstanceMethod:(SEL)aSEL{
    if (aSEL==@selector(objectForKeyedSubscript:)) {
        class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
-(void)myInstanceMethod:(id)param{
    NSLog(@"objectForKeyedSubscript未識別方法崩潰捕捉");
}

例2

萬一后端數據返回的data數據類型是null,會導致未識別方法崩潰。

解決:在NSNull分類中添加
-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector{
    //
    NSMethodSignature *signature=[super methodSignatureForSelector:selector];
    if(!signature){
        signature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation{
    invocation.target = nil;
    [invocation invoke];
    NSLog(@"NSNull未識別方法崩潰捕捉");
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容