6

我喜欢积木,当我不能使用它们时,我会感到难过。特别是,这主要发生在我每次使用委托时(例如:使用 UIKit 类,主要是预块功能)。

所以我想知道......是否有可能 - 使用 ObjC 的疯狂力量 - 做这样的事情?

   // id _delegate; // Most likely declared as class variable or it will be released
   _delegate = [DelegateFactory delegateOfProtocol:@protocol(SomeProtocol)];
   _delegate performBlock:^{
       // Do something
   } onSelector:@selector(someProtocolMethod)]; // would execute the given block when the given selector is called on the dynamic delegate object.
   theObject.delegate = (id<SomeProtocol>)_delegate;
   // Profit!

performBlock:onSelector:

如果YES,如何?我们是否有理由不尽可能多地这样做?

编辑

看起来这是可能的。当前的答案集中在问题的第一部分,即如何。但最好能就“我们应该这样做”部分进行一些讨论。

4

3 回答 3

10

好的,我终于把WoolDelegate 放到了 GitHub 上。现在我只需要再花一个月的时间来写一个合适的 README(尽管我想这是一个好的开始)。

委托类本身非常简单。它只是维护一个字典映射SELs 到 Block。当一个实例收到一条它没有响应的消息时,它最终会进入forwardInvocation:并在字典中查找选择器:

- (void)forwardInvocation:(NSInvocation *)anInvocation {

    SEL sel = [anInvocation selector];
    GenericBlock handler = [self handlerForSelector:sel];

如果找到,则将 Block 的调用函数指针拉出并传递给 juicy bits:

    IMP handlerIMP = BlockIMP(handler);

    [anInvocation Wool_invokeUsingIMP:handlerIMP];
}

(该BlockIMP()功能以及其他块探测代码感谢Mike Ash。实际上,这个项目的很多内容都是基于我从他的周五问答中学到的东西。如果你还没有读过这些文章,你就错过了.)

我应该注意到,每次发送特定消息时,这都会通过完整的方法解析机制;那里有一个速度。另一种方法是 Erik H. 和EMKPantry各自采用的路径,即为您需要的每个委托对象创建一个新类,并使用class_addMethod(). 由于每个实例WoolDelegate都有自己的处理程序字典,我们不需要这样做,但另一方面,没有办法“缓存”查找或调用。方法只能添加到,不能添加到实例。

我这样做有两个原因:这是一个练习,看看我是否可以解决接下来的部分——从NSInvocation到 Block 调用的切换——以及为每个需要的实例创建一个新似乎简单对我不优雅。它是否不如我的解决方案优雅,我将留给每个读者的判断。

继续前进,这个过程的核心实际上是在项目中找到的NSInvocation类别中。这利用libffi调用一个直到运行时才知道的函数——块的调用——使用在运行时之前也是未知的参数(可以通过 访问NSInvocation)。通常,这是不可能的,原因与 ava_list不能传递的原因相同:编译器必须知道有多少参数以及它们有多大。libffi 包含每个知道/基于这些平台的调用约定的平台的汇编程序。

这里有三个步骤: libffi 需要一个被调用函数的参数类型列表;它需要将参数值本身放入特定的格式;然后需要通过 libffi 调用函数(块的调用指针)并将返回值放回NSInvocation.

第一部分的实际工作主要由 Mike Ash 编写的函数处理,该函数从Wool_buildFFIArgTypeList. libffi 有 internal structs 用于描述函数参数的类型。在准备调用函数时,库需要指向这些结构的指针列表。NSMethodSignature允许访问每个参数的NSInvocation编码字符串;从那里翻译到正确ffi_type的由一组if/else查找处理:

arg_types[i] = libffi_type_for_objc_encoding([sig getArgumentTypeAtIndex:actual_arg_idx]);

...

if(str[0] == @encode(type)[0]) \
{ \
    if(sizeof(type) == 1) \
        return &ffi_type_sint8; \
    else if(sizeof(type) == 2) \
        return &ffi_type_sint16; \

接下来,libffi 想要指向参数值本身的指针。这是在 中完成的Wool_buildArgValList:再次从 中获取每个参数的大小NSMethodSignature,并分配一块该大小的内存,然后返回列表:

NSUInteger arg_size;
NSGetSizeAndAlignment([sig getArgumentTypeAtIndex:actual_arg_idx], 
                      &arg_size, 
                      NULL);
/* Get a piece of memory that size and put its address in the list. */
arg_list[i] = [self Wool_allocate:arg_size];
/* Put the value into the allocated spot. */
[self getArgument:arg_list[i] atIndex:actual_arg_idx];

(顺便说一句:代码中有几个关于跳过 的注释SEL,这是任何方法调用的(隐藏的)第二个传递参数。块的调用指针没有一个插槽来保存SEL;它只是将自己作为第一个参数,其余的是“正常”参数。由于用客户端代码编写的块,无论如何都无法访问该参数(它当时不存在),我决定忽略它。)

libffi 现在需要做一些“准备”;只要成功(并且可以为返回值分配空间),现在可以“调用”调用函数指针,并且可以设置返回值:

ffi_call(&inv_cif, (genericfunc)theIMP, ret_val, arg_vals);
if( ret_val ){
    [self setReturnValue:ret_val];
    free(ret_val);
}

在项目的 main.m 中有一些功能演示。

最后,关于你“应该这样做吗?”的问题,我认为答案是“是的,只要它能让你更有效率”。WoolDelegate是完全通用的,实例可以像任何完全写出的类一样工作。不过,我的意图是制作简单的一次性委托——只需要一两个方法,并且不需要经过委托——比编写一个全新的类更少工作,而且更易读/maintainable 比将一些委托方法粘贴到视图控制器中,因为这是最容易放置它们的地方。像这样利用运行时和语言的活力有望提​​高代码的可读性,就像基于块的NSNotification处理程序一样。

于 2013-04-19T20:32:46.313 回答
5

我只是整理了一个小项目,让你做到这一点......

@interface EJHDelegateObject : NSObject

+ (id)delegateObjectForProtocol:(Protocol*) protocol;

@property (nonatomic, strong) Protocol *protocol;
- (void)addImplementation:(id)blockImplementation forSelector:(SEL)selector;

@end


@implementation EJHDelegateObject
static NSInteger counter;

+ (id)delegateObjectForProtocol:(Protocol *)protocol 
{
    NSString *className = [NSString stringWithFormat:@"%s%@%i",protocol_getName(protocol),@"_EJH_implementation_", counter++];
    Class protocolClass = objc_allocateClassPair([EJHDelegateObject class], [className cStringUsingEncoding:NSUTF8StringEncoding], 0);
    class_addProtocol(protocolClass, protocol);
    objc_registerClassPair(protocolClass);
    EJHDelegateObject *object = [[protocolClass alloc] init];
    object.protocol = protocol;
    return object;
}


- (void)addImplementation:(id)blockImplementation forSelector:(SEL)selector
{
    unsigned int outCount;
    struct objc_method_description *methodDescriptions = protocol_copyMethodDescriptionList(self.protocol, NO, YES, &outCount);
    struct objc_method_description description;
    BOOL descriptionFound = NO;
    for (int i = 0; i < outCount; i++){
        description = methodDescriptions[i];
        if (description.name == selector){
            descriptionFound = YES;
            break;
        }
    }
    if (descriptionFound){
        class_addMethod([self class], selector, imp_implementationWithBlock(blockImplementation), description.types);
    }
}

@end

并使用 EJHDelegateObject:

self.alertViewDelegate = [EJHDelegateObject delegateObjectForProtocol:@protocol(UIAlertViewDelegate)];
[self.alertViewDelegate addImplementation:^(id _self, UIAlertView* alertView, NSInteger buttonIndex){
    NSLog(@"%@ dismissed with index %i", alertView, buttonIndex);
} forSelector:@selector(alertView:didDismissWithButtonIndex:)];

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Example" message:@"My delegate is an EJHDelegateObject" delegate:self.alertViewDelegate cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
[alertView show];
于 2013-03-15T19:33:47.797 回答
2

编辑:这是我在了解您的要求后提出的。这只是一个快速破解,一个让你开始的想法,它没有正确实施,也没有经过测试。它应该适用于将发送者作为唯一参数的委托方法。它工作它应该与正常和返回结构的委托方法一起工作。

typedef void *(^UBDCallback)(id);
typedef void(^UBDCallbackStret)(void *, id);

void *UBDDelegateMethod(UniversalBlockDelegate *self, SEL _cmd, id sender)
{   
    UBDCallback cb = [self blockForSelector:_cmd];
    return cb(sender);
}

void UBDelegateMethodStret(void *retadrr, UniversalBlockDelegate *self, SEL _cmd, id sender)
{
    UBDCallbackStret cb = [self blockForSelector:_cmd];
    cb(retaddr, sender);
}

@interface UniversalBlockDelegate: NSObject

- (BOOL)addDelegateSelector:(SEL)sel isStret:(BOOL)stret methodSignature:(const char *)mSig block:(id)block;

@end

@implementation UniversalBlockDelegate {
    SEL selectors[128];
    id blocks[128];
    int count;
}

- (id)blockForSelector:(SEL)sel
{
    int idx = -1;
    for (int i = 0; i < count; i++) {
        if (selectors[i] == sel) {
            return blocks[i];
        }
    }

    return nil; 
}

- (void)dealloc
{
    for (int i = 0; i < count; i++) {
        [blocks[i] release];
    }
    [super dealloc];
}

- (BOOL)addDelegateSelector:(SEL)sel isStret:(BOOL)stret methodSignature:(const char *)mSig block:(id)block
{
    if (count >= 128) return NO;

    selectors[count] = sel;
    blocks[count++] = [block copy];

    class_addMethod(self.class, sel, (IMP)(stret ? UBDDelegateMethodStret : UBDDelegateMethod), mSig);

    return YES;
}

@end

用法:

UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
UniversalBlockDelegate *d = [[UniversalBlockDelegate alloc] init];
webView.delegate = d;
[d addDelegateSelector:@selector(webViewDidFinishLoading:) isStret:NO methodSignature:"v@:@" block:^(id webView) {
    NSLog(@"Web View '%@' finished loading!", webView);
}];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://google.com"]]];
于 2013-03-15T17:22:38.407 回答