iOS-訊息轉發和方法調配技術學習

semperhhh發表於2019-03-03

學習的主要摘自 iOS-訊息轉發和方法調配技術學習

在oc中,可以: 1.在執行期向類中新增例項變數 2.在執行期可以繼續向類中新增方法 3.在執行期改變與給定的選擇子名稱相對應的方法(方法調配技術)

理解objc_msgSend的作用

在oc中,如果向某物件傳遞資訊,那就會使用動態繫結機制來決定需要呼叫的方法。在底層,所有方法都是普通的C語言函式.然而物件收到訊息之後,究竟該呼叫哪個方法則完全於執行期決定,甚至可以在程式執行時改變,這些特性使得oc成為一門真正的動態語言.

在oc中,給物件傳送訊息的語法是:

id returnValue = [someObject messageName:parameter];
複製程式碼

這裡,someObject叫做“接收者(receiver)”,messageName:叫做"選擇子(selector)",選擇子和引數合起來稱為“訊息”。編譯器看到此訊息後,將其轉換為一條標準的C語言函式呼叫,所呼叫的函式乃是訊息傳遞機制中的核心函式叫做objc_msgSend,它的原型如下:

void objc_msgSend(id self, SEL cmd, ...)
複製程式碼

第一個引數代表接收者,第二個引數代表選擇子,後續引數就是訊息中的那些引數,數量是可變的,所以這個函式就是引數個數可變的函式。編譯器會把剛才那個例子中的訊息轉換為如下函式

id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
複製程式碼

objc_msgSend函式會依據接受者與選擇子的型別來呼叫適當的方法.為了完成此操作,該方法需要在接收者所屬的類中搜尋其"方法列表",如果能找到與選擇子相符的方法,就跳至其實現程式碼.若是找不到,就沿著繼承體系繼續向上查詢.如果找到了就執行,如果最終還是找不到,就執行訊息轉發操作.

快速執行路徑:按照上面說的想呼叫一個方法似乎需要很多步驟,所幸objc_msgSend會將匹配的結果快取在"快速對映表"裡面,每個類都有這樣一塊快取,若是稍後還向該類傳送與選擇子相同的訊息,那麼執行起來就很快了.

理解這些,就會明白,在傳送訊息時,程式碼究竟是如何執行的,而且也能理解,為何在除錯的時候,棧"回溯"資訊中總是出現objc_msgSend.

理解訊息轉發機制

若想令類能理解某條訊息,我們必須以程式碼實現出對應的方法才行.但是在編譯期向類傳送了其無法解讀的訊息並不會報錯,因為在執行期繼續可以向類中新增方法,所以編譯器在編譯時還不知道類中到底有沒有對應方法的實現.當物件接收到無法解讀的訊息後,就會啟動訊息轉發機制.程式設計師可由此過程告訴物件應該如何處理未知訊息.

開發者在編寫自己的類時,可於轉發過程中設定掛鉤,用以執行預訂的邏輯,而不使應用程式崩潰.

訊息轉發分為兩個階段:

1.徵詢接受者,看它能否動態新增方法,以處理這個未知的選擇子,這個過程叫做動態方法解析(dynamic method resolution). 2.請接受者看看有沒有其他物件能處理這條訊息:   如果有,則執行期系統會把訊息轉給那個物件。   如果沒有,則啟動完整的訊息轉發機制(full forwarding mechanism),執行期系統會把與訊息有關的全部細節都封裝到 NSInvocation物件中,再給接受者最後一次機會,令其設法解決當前還未處理的這條訊息。 類方法+(BOOL)resolveInstanceMethod:(SEL)selector:檢視這個類是否能新增一個例項方法用以處理此選擇子 例項方法- (id)forwardTargetForSelector:(SEL)selector;:詢問是否能找到未知訊息的備援接受者,如果能找到備援物件,就將其返回,如果不能,就返回nil。 例項方法- (void)forwardInvocation:(NSInvocation*)invocation:建立NSInvocation物件,將尚未處理的那條訊息 有關的全部細節都封於其中,在觸發NSInvocation物件時,“訊息派發系統(message-dispatch system)”就會將訊息派給目標物件。

總結: 若物件無法響應某個選擇子,則進入訊息轉發流程. 通過執行期的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中. 物件可以把其無法解讀的某些選擇子轉交給其他物件來處理. 經過上述兩步之後,如果還是沒辦法處理選擇子,那就啟動完整的訊息轉發機制.

應用-向物件傳送沒有實現的訊息


    //呼叫當前類中沒有實現的方法
    [self performSelector:@selector(readBook)];
    [self performSelector:@selector(writeBook)];
    [self performSelector:@selector(findBook)];
}

void readBook(id self,SEL _cmd) {

    NSLog(@"now i can readBook");
}

//1.動態方法解析
+(BOOL)resolveInstanceMethod:(SEL)sel {

    NSLog(@"resolveInstanceMethod: %@",NSStringFromSelector(sel));

    if (sel == @selector(readBook)) {
        class_addMethod([self class], sel, (IMP)readBook, "V@:");

        return YES;
    }

    return [super resolveInstanceMethod:sel];
}

//2.備援接受者
-(id)forwardingTargetForSelector:(SEL)aSelector {
 
    NSLog(@"forwardingTarget: %@",NSStringFromSelector(aSelector));
    
    CLRead *read = [[CLRead alloc]init];
    if ([read respondsToSelector:aSelector]) {
        return read;
    }
    return [super forwardingTargetForSelector:aSelector];
}

//3.完整的訊息轉發
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
    NSLog(@"method signature for selector: %@",NSStringFromSelector(aSelector));
    
    if (aSelector == @selector(code)) {
        return [NSMethodSignature signatureWithObjCTypes:"V@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation {
    
    NSLog(@"forwardInvocation: %@",NSStringFromSelector([anInvocation selector]));
    
    if ([anInvocation selector] == @selector(code)) {
        CLRead *read = [[CLRead alloc]init];
        [anInvocation invokeWithTarget:read];
    }
}
複製程式碼

方法調配技術

在oc中,與給定的選擇子名稱相對應的方法是不是也可以在執行期改變呢?沒錯,若能善用此特性,則可發揮出巨大優勢,因為我們既不需要原始碼,也不需要通過繼承子類來覆寫方法就能改變這個類本身的功能.這樣一來,新功能將在本類的所有例項中生效,而不是僅限於覆寫了相關方法的那些子類例項.此方案經常稱為"方法調配"

類的方法列表會把選擇子的名稱對映到相關的方法實現之上,使得"動態訊息派發系統"能夠據此找到應該呼叫的方法.這些方法均以函式指標的形式來表示,這種指標叫做IMP,其原型如下:

id (*IMP)(id,SEL,...)
複製程式碼

交換方法實現

交換方法實現,可用下列函式:

void method_exchangeImplementations(Method m1,Method m2)
複製程式碼

此函式的兩個參數列示待交換的兩個方法實現,而方法實現則可通過下列函式獲得:

Method class_getInstanceMethod(Class aClass, SEL aSelector)
複製程式碼

此函式根據給定的選擇從類中取出與之相關的方法.

很少有人在除錯程式之外的場合用上述"方法調配技術"來永久改動某個類的功能.不能僅僅因為Objective-c語言裡有這個特性就一定要用它.若是濫用,反而會令程式碼變得不易讀懂且難於維護.

應用-防止陣列越界的crach


//在NSArray中新增分類
+(void)load {
    
    //替換不可變陣列中的方法 []呼叫的方法
    Method oldObjectAtIndex = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
    Method newObjectAtIndex = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedNewSubscript:));
    
    method_exchangeImplementations(oldObjectAtIndex, newObjectAtIndex);
    
    //替換不可變陣列中的objectAtIndex
    Method oldObjectAtIndex1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method newObjectAtIndex1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(newObjectAtIndex:));
    
    method_exchangeImplementations(oldObjectAtIndex1, newObjectAtIndex1);
    
    //替換可變陣列中的方法 []呼叫的方法
    Method oldMutableObjectAtIndex = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndexedSubscript:));
    Method newMutableObjectAtIndex = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(mutableObjectAtIndexedNewSubscript:));

    method_exchangeImplementations(oldMutableObjectAtIndex, newMutableObjectAtIndex);
    
    //替換可變陣列中的方法 objectatindex
    Method oldMutableObjectAtIndex1 = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
    Method newMutableObjectAtIndex1 = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(newMutableObjectAtIndex:));
    
    method_exchangeImplementations(oldMutableObjectAtIndex1, newMutableObjectAtIndex1);
}

-(id)newMutableObjectAtIndex:(NSUInteger)index {
 
    if (index > self.count - 1 || !self.count) {
        
        @try {
            
            //因為前面方法交換了,這裡其實呼叫的原生的方法
            return [self newMutableObjectAtIndex:index];
        } @catch (NSException *exception) {
            NSLog(@"可變陣列越界了");
            return nil;
        } @finally {
            
        }
    }else {
        return [self newMutableObjectAtIndex:index];
    }
}

-(id)mutableObjectAtIndexedNewSubscript:(NSUInteger)index {
    
    if (index > self.count - 1 || !self.count) {
        
        @try {
            
            //因為前面方法交換了,這裡其實呼叫的原生的方法
            return [self mutableObjectAtIndexedNewSubscript:index];
        } @catch (NSException *exception) {
            NSLog(@"可變陣列越界了");
            return nil;
        } @finally {
            
        }
    }else {
        return [self mutableObjectAtIndexedNewSubscript:index];
    }
}

-(id)newObjectAtIndex:(NSUInteger)index {
    
    if (index > self.count - 1 || !self.count) {
        
        @try {
            
            //因為前面方法交換了,這裡其實呼叫的原生的方法
            return [self newObjectAtIndex:index];
        } @catch (NSException *exception) {
            NSLog(@"不可變陣列越界了");
            return nil;
        } @finally {
            
        }
    }else {
        return [self newObjectAtIndex:index];
    }
}

-(id)objectAtIndexedNewSubscript:(NSUInteger)index {
    
    if (index > self.count - 1 || !self.count) {
        
        @try {
            
            //因為前面方法交換了,這裡其實呼叫的原生的方法
            return [self objectAtIndexedNewSubscript:index];
        } @catch (NSException *exception) {
            NSLog(@"不可變陣列越界了");
            return nil;
        } @finally {
            
        }
    }else {
        return [self objectAtIndexedNewSubscript:index];
    }
}

複製程式碼

編譯期

編譯時就是獲取objc檔案.

相關文章