iOS訊息轉發小記

莫云溪發表於2018-05-31

訊息轉發流程圖

2665383-dddc0877815062f7.jpg
15277558865032.jpg

如果類接收到無法處理的訊息,會觸發訊息轉發機制,一共有三個步驟,接受者在每一步中均有機會處理訊息。步驟越往後,處理訊息的代價就越大,所以最好再第一步就處理完。

第一道防線

在類裡面實現兩個方法來處理未知訊息。執行動態方法解析之前,先會判斷是否曾經有動態解析。

  • resolveInstanceMethod:處理例項方法
  • resolveClassMethod:處理類方法

我們來看個Demo,先看呼叫方程式碼

    TestA *testA = [[TestA alloc] init];
    [testA instanceMethod];
    [TestA classMethod];

再來看看TestA的定義。

// TestA.h
@interface TestA : NSObject

- (void)instanceMethod;
+ (void)classMethod;

@end

// TestA.m
@implementation TestA

- (void)newInstanceMethod {
    NSLog(@"newInstanceMethod");
}

+ (void)newClassMethod {
    NSLog(@"newClassMethod");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(instanceMethod)) {
        // 動態新增方法newInstanceMethod
        Method method = class_getInstanceMethod([self class], @selector(newInstanceMethod));
        IMP imp = method_getImplementation(method);
        class_addMethod([self class], sel, imp, method_getTypeEncoding(method));
        // 成功處理,訊息轉發機制結束,呼叫newInstanceMethod
        return YES;
    }
    // 不能處理,進入第二步
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(classMethod)) {
        // 動態新增方法newClassMethod
        Method method = class_getInstanceMethod(object_getClass(self), @selector(newClassMethod));
        IMP imp = method_getImplementation(method);
        class_addMethod(object_getClass(self), sel, imp, method_getTypeEncoding(method));
        // 成功處理,訊息轉發機制結束,呼叫newClassMethod
        return YES;
    }
    // 不能處理,進入第二步
    return [super resolveClassMethod:sel];
}

@end

TestA中標頭檔案定義了兩個方法,但是沒有實現,如果不用訊息轉發機制處理異常,會導致crash,log想必大家應該很熟悉

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TestA funcA]: unrecognized selector sent to instance 0x6040000125c0'

例項方法儲存在類物件,類方法儲存在元類物件,在呼叫class_addMethod時,第一個引數需要注意。

第二道防線

第二道防線依賴一個函式forwardingTargetForSelector

// 類方法
//+ (id)forwardingTargetForSelector:(SEL)aSelector {
//    
//}
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(instanceMethod)) {
        // 訊息轉發給TestB例項
        return [TestB new];
    }
    // 訊息轉發失敗,進入下一步
    return nil;
}

// TestB.m
- (void)instanceMethod {
    NSLog(@"instanceMethod");
}

第三道防線

第三道防線有兩步

  1. 呼叫methodSignatureForSelector,獲取新的方法簽名(返回值型別,引數型別)
  2. 呼叫forwardInvocation,轉發訊息,
// 方法簽名(返回值型別,引數型別)
// 類方法減號改為加號
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [TestB instanceMethodSignatureForSelector:aSelector];
    return signature;
}

// NSInvocation封裝了方法呼叫,包括:方法呼叫者、方法名、方法引數
// anInvocation.target 訊息接受者
// anInvocation.selector 函式名
// [anInvocation getArgument:NULL atIndex:0]; 獲取引數
// 類方法減號改為加號
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[TestB new]];
}

歡迎關注我的公眾號及部落格

2665383-d14aa5f2692e2711.jpg
公眾號.jpg

相關文章