Objective C

函数调用 / 消息传递机制

在 OC 里调用类或对象的方法 Func 有以下几个步骤

  1. 查找类或对象函数列表,当存在 Func 则调用,不存在就继续向父类查找。
  2. 直达继承尽头都无法处理,则执行 +(BOOL) resolveInstanceMethod:(SEL) sel。如果内部补充上了 Func,则跳转并执行。
  3. resolveInstanceMethod没有补充,则执行-(id) forwardingTargetForSelector:(SEL) sel
  4. forwardInvocation
  5. resolveInstanceMethod都没法处理 Func 则报错。

所以当实现了 resolveInstanceMethod在要执行不能存在的函数时,就可以进行处理。

对象实现了resolveInstanceMethod在要执行不能存在的函数时,就可以转派进行处理。

////////////////// Just for back up
@interface TempClass {
}
@end
    
@implementation TempClass {
}

-(void) Temp {
     NSLog(@"TempClass: Temp");
}

@end

////////////////// MyClass for test
@interface MyClass {
}
@end
    
@implementation MyClass {
}

+(BOOL) resolveInstanceMethod:(SEL) sel {
    NSLog(@"resolveInstanceMethod");
    NSLog(@"trying use selector: %s", sel);
    if (sel == @selector(Temp)) {
        NSLog(@"not exist. adding selector: %s", sel);
        // choose one way
        // one way: add objective-c func
        IMP imp = class_getMethodImplementation([self class], @selector(Temp_OC_Func));
        class_addMethod([self class], self, imp, "v@:");
        // another way: add c func
        class_addMethod([self class], self, (IMP) , "v@:");
        
        return YES;
    }
    
    // fallback
    return [super resolveInstanceMethod:sel];
}

-(void) Temp_OC_Func {
    NSLog(@"MyClass: Temp_OC_Func");
}

-(id) forwardingTargetForSelector:(SEL) sel {
    TempClass* tempObj = [[TempClass* alloc] init];
	return tempObj
}
@end
    
void Temp_C_Func(id self, SEL sel) {
    NSLog(@"Other: Temp_C_Func");
}

int main(int argc, const char* argv[]) {
    MyClass* myClass = [[MyClass alloc] init];

    // Error: can't invoke Temp method by this way, there on Temp in myClass
    // [myClass Temp]
    
    // right way
    [myClass performSelector:@selector(Temp)];
    
    return 0;
}

在上面的例子中展示了resolveInstanceMethodforwardingTargetForSelector的使用方法

在 main 中调用 [myClass performSelector:@selector(Temp)],因为 myClass 中不存在 Temp(直接[myClass Temp]则会无法通过编译),所以resolveInstanceMethod被调用,并用两种方式将方法加到 myClass 中。其中,对于 objective-c 的方法,需要搭配class_getMethodImplementation获取对象中的函数指针,而对于 c 的方法,转为 (IMP) 即可。需要注意的是,其实任何 objective-c 函数的原形都是 return_type c_funcName(id self, SEL sel, ...)只不过被隐藏了。所以在指定 c 的方法是,需要加上 (id self, SEL sel)

如果把resolveInstanceMethod注释掉,则对象不存在可执行的函数,转到forwardingTargetForSelector中,在那里返回了一个 TempClass 对象,因为TempClass 实现了 Temp(必须同名),所以转而执行 TempClass 的 Temp 方法。

如果再把 resolveInstanceMethod注释掉,则 myClass 任何途径都无法执行 Temp,出现报错。

有几点需要注意的

Runtime 传参问题

Runtime 调用函数,使用performSelector, 跳转到performSelector的定义发现有三种方式:

-(id) performSelector:(SEL) selector;
-(id) performSelector:(SEL) selector withObject:(id) object;
-(id) performSelector:(SEL) selector withObject:(id) object withObject:(id) object;

可以看出默认提供最多允许传入两个参数,并且只允许对象类型。

但由于可以传入对象,通过传入数组,字典,hash 表就足以处理传入多参的情况了。

objective-c 函数名称实际上是要用过联级拼接得到的,例如

-(void) Func_One:(NSString*) n1 {
    NSLog(@"Func_One param = %@,", n1);
}

-(void) Func_Two:(NSString*) n1 Name:(NSString*) n2 {
    NSLog(@"Func_Two params1 = %@, params2 = %@", n1, n2);
}

void main() {
    // [myClass performSelector:@selector(Func_One), withObject:@"str1"]; // wrong
    [myClass performSelector:@selector(Func_One:), withObject:@"str1"]; // right
    [myClass performSelector:@selector(Func_Two:Name:), withObject:@"str2-1", withObject:@"str2-2"]; // right
}

当配合 resolveInstanceMethod使用时,还可以做些非常规的操作

//...
+(BOOL) resolveInstanceMethod:(SEL) sel {
    if (sel == @selector(Temp)) {
        IMP imp = class_getMethodImplementation([self class], @selector(Temp_OC_Func:Name:));
        class_addMethod([self class], self, imp, "v@:");

        return YES;
    }
    
    // fallback
    return [super resolveInstanceMethod:sel];
}

-(void) Temp_OC_Func:(NSString*) n1 Name:(NSString*) n2 {
    NSLog(@"Func_Two params1 = %@, params2 = %@", n1, n2);
}
//...

void main() {
    [myClass performSelector:@selector(Temp) withObject:@"str2-1", withObject:@"str2-2"];
}

上面的代码,用过resolveInstanceMethod修改了Temp的实际相应函数为Temp_OC_Func:Name:,所以 performSelector传入的参数必须为两个。

也就是说performSelector传入的参数要与实际处理的函数对应,而不是匹配performSelector调用时使用的函数名,否则会报错,或者产生非预期的结果,传入参数可以多,但是不能少。

Runtime 调用的返回值

可以发现performSelector的返回值是个 id 类型,可知返回值无法通过performSelector接收。如果需要调用有返回值的函数,请使用 NSInvocation