在运行时动态实现委托

时间:2014-05-23 00:00:49

标签: ios objective-c

在我的课程中,我有一个关于UIViewController的参考,并希望在运行时在这个ViewController上实现一个委托。委托只有一个方法(有两个参数),当调用ViewController上的delegate-method时,我的类应该处理这个调用。

我很确定这种方法可以通过某种方式调整等等,但我不知道如何实现这一目标。

2 个答案:

答案 0 :(得分:3)

你想要什么是可能的,但这不是方法调整,因为你不想切换到方法,而是添加一个新方法。由于Objective-C的动态特性,它可以完成,但它仍然是一个肮脏的黑客,所以也向库供应商提交功能请求。

你想要的是class_addMethod()和一个C函数以及它的实际实现。还有一件事,Objective-C方法 C方法,但有两个隐含参数,self_cmd,必须记住(在创建C方法时)当告诉class_addMethod你的方法签名时。这里有一个如何拉出这样的东西的SSCE:

#import <Foundation/Foundation.h>
#import <objc/runtime.h> // Required for class_addMethod()

@interface MyClass : NSObject
@end

@implementation MyClass
@end


@protocol MyProtocol <NSObject>
- (void)printString:(NSString *)string;
@end




// Note the method signature containing the
// two implicit parameters self and _cmd!
void MyClassPrinStringIMP(id self, SEL _cmd, NSString *string)
{
    NSLog(@"Hi I'm %@:%s and this is the string: %@", self, sel_getName(_cmd), string);
}


void PimpMyClass()
{
    // The last argument is the signature. First character is the return type, in our case void
    // Then comes self and _cmd, followed by the NSString. You can use @encode() to find out how your
    // type is encoded. Best is to build this string at runtime, since the encoding can change with architectures
    class_addMethod([MyClass class], @selector(printString:), (IMP)MyClassPrinStringIMP, "v@:@");
}



int main(int argc, const char * argv[])
{
    @autoreleasepool
    {
        PimpMyClass();

        id foo = [[MyClass alloc] init]; // id, to silence the compiler!
        [foo printString:@"Hello World"];
    }

    return 0;
}

示例输出:

Hi I'm <MyClass: 0x100101810>:printString: and this is the string: Hello World

编辑:您可能会发现,在运行时检查传递的对象是否符合协议,或者使用conformsToProtocol:。由于此代码只是添加了方法实现,它仍然会失败,但您可以告诉运行时您完全使用这一个函数调用实现该协议:

class_addProtocol([MyClass class], @protocol(MyProtocol));

替代方案:代理

Objective-Cs的动态性和消息转发已经被@JasperBlues称赞,但是,Objective-C中有一个特定的类可以实现这一目的:NSProxy。它旨在拦截已发送的消息并将其动态分派给相关目标,并使用高级NSInvocation方法。如果您可以以某种方式传递代理对象作为委托(取决于您的代码允许和不允许的内容),创建NSProxy子类可能是最干净的方法。

但是,请注意,然后你最终得到了一个包裹你的另一个对象的填充对象,它有自己的痛苦,当你尝试通过->语法直接访问变量时会破坏。它不是一个完全不可见的代理,但对大多数情况来说都足够好了。

答案 1 :(得分:2)

首先,一些评论表明你所要求的是“做坏事”或“肮脏的黑客”。我在这里不同意。大多数现代面向对象语言都支持这些功能,并且它们被许多系统级框架用于良好的效果。当然,或许使用这些动态特性而不是真正需要(为了娱乐或实践),即使更简单的方法可以正常工作也是人性的。要小心这一点。

Objective-C令人钦佩,因为它有点遗留的语言并且接近“裸机”,但却具有令人惊讶的动态水平,使得在没有任何外部库或框架的情况下支持这些要求相对容易。

除了使用另一个答案正确指出的 class_addMethod 指南外,还有其他一些方法:


消息转发:(推荐)

所有NSObject子类都能够将他们无法响应的方法转发给另一个目标对象。这类似于蹦床的低级概念。 Apple publishes a guide on using this approach

使用正向调用的优点是它使用 NSInvocation 抽象级别,而不是直接调用C ObjC运行时API。这抽象出以下细节:

  • 结构和基元将自动装箱/取消装箱
  • 使用动态/未知数量的参数调度方法变得很容易。直到arm64,这可以使用va_args完成,但是在arm64上,va_args可以直接复制到寄存器,而不是从堆栈中弹出。


解析实例方法:

通过将C函数注册为响应给定消息的实现来创建实例方法。使用IMP_ImplementationWithBlock:

可以使用块整齐地完成此操作
+ (BOOL)resolveInstanceMethod:(SEL)sel
{

        IMP imp = imp_implementationWithBlock((__bridge id) objc_unretainedPointer(
            ^(id me, BOOL firstParam, NSString* secondParam)
        {
            //Implementation goes in here
            return something; //something of type 'id'

        }));
        class_addMethod(self, sel, imp, "@@:");
        return YES;
    }
    return NO;
}


使用libffi:

Libffi也可以做这种事情,但如果你使用的是纯Objective-C则没有必要,因为运行时已经有了这些功能。