Creando delegates en el lugar con bloques

Amo los bloques y me entristece cuando no puedo usarlos. En particular, esto sucede principalmente cada vez que uso delegates (por ej., Con las clases de UIKit, principalmente con la funcionalidad de prelocking).

Entonces me pregunto … ¿Es posible -utilizando el loco poder de ObjC- hacer algo como esto?

// 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)_delegate; // Profit! 

performBlock:onSelector:

Si es así, ¿cómo? ¿Y hay alguna razón por la cual no deberíamos estar haciendo esto tanto como sea posible?

Editar

Parece que ES posible. Las respuestas actuales se centran en la primera parte de la pregunta, que es cómo. Pero sería bueno tener una discusión sobre la parte ” deberíamos hacerlo “.

De acuerdo, finalmente logré poner a WoolDelegate en GitHub. Ahora solo me llevaría otro mes escribir un LÉAME apropiado (aunque supongo que este es un buen comienzo).

La clase de delegado en sí es bastante simple. Simplemente mantiene un SEL mapeo de diccionario a Bloquear. Cuando una instancia recibe un mensaje al que no responde, termina en forwardInvocation: y busca en el diccionario el selector:

 - (void)forwardInvocation:(NSInvocation *)anInvocation { SEL sel = [anInvocation selector]; GenericBlock handler = [self handlerForSelector:sel]; 

Si se encuentra, el puntero de la función de invocación del Bloque se extrae y se pasa a los bits jugosos:

  IMP handlerIMP = BlockIMP(handler); [anInvocation Wool_invokeUsingIMP:handlerIMP]; } 

(La función BlockIMP() , junto con otro código de BlockIMP() bloque, es gracias a Mike Ash . En realidad, gran parte de este proyecto está basado en cosas que aprendí de las preguntas y respuestas de los viernes. Si no has leído esos ensayos, estás ‘ se está perdiendo.)

Debo señalar que esto pasa por el mecanismo completo de resolución de métodos cada vez que se envía un mensaje en particular; hay un golpe de velocidad allí. La alternativa es la ruta que tomaron Erik H. y EMKPantry , que está creando un nuevo clas para cada objeto de delegado que necesita, y el uso de class_addMethod() . Como cada instancia de WoolDelegate tiene su propio diccionario de controladores, no es necesario que lo hagamos, pero, por otro lado, no hay forma de “almacenar en caché” la búsqueda o la invocación. Un método solo se puede agregar a una clase , no a una instancia.

Lo hice de esta manera por dos razones: este fue un ejercicio para ver si podía resolver la parte que vendría después – la NSInvocation de NSInvocation a la invocación de bloques – y la creación de una nueva clase para cada instancia necesaria simplemente me pareció poco elegante. Si es menos elegante que mi solución, dejaré el juicio de cada lector.

Continuando, la parte NSInvocation de este procedimiento se encuentra en la categoría NSInvocation que se encuentra en el proyecto. Esto utiliza libffi para llamar a una función desconocida hasta el tiempo de ejecución, la invocación del bloque, con argumentos que también son desconocidos hasta el tiempo de ejecución (a los que se puede acceder a través de NSInvocation ). Normalmente, esto no es posible, por la misma razón que una va_list no se puede pasar: el comstackdor tiene que saber cuántos argumentos hay y qué tan grandes son. libffi contiene ensamblador para cada plataforma que sabe / está basado en las convenciones de llamadas de esas plataformas.

Aquí hay tres pasos: libffi necesita una lista de los tipos de argumentos para la función a la que se llama; necesita los valores del argumento en sí mismos puestos en un formato particular; luego, la función (el puntero de invocación del Bloque) debe invocarse a través de libffi y el valor de retorno volver a la NSInvocation .

El trabajo real para la primera parte se maneja en gran medida por una función que es escrita nuevamente por Mike Ash, llamada desde Wool_buildFFIArgTypeList . libffi tiene struct internas que usa para describir los tipos de argumentos de función. Al preparar una llamada a una función, la biblioteca necesita una lista de punteros a estas estructuras. El NSMethodSignature para NSInvocation permite el acceso de la cadena de encoding de cada argumento; la traducción desde allí al ffi_type correcto se maneja mediante un conjunto de búsquedas 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; \ 

A continuación, libffi quiere punteros a los valores del argumento en sí mismos. Esto se hace en Wool_buildArgValList : obtenga el tamaño de cada argumento, nuevamente desde NSMethodSignature , y asigne un trozo de memoria de ese tamaño, luego devuelva la lista:

 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]; 

(Un lado: hay varias notas en el código sobre omitir el SEL , que es el segundo argumento (oculto) pasado a cualquier invocación de método. El puntero de invocación del Bloque no tiene un espacio para contener el SEL ; simplemente se tiene a sí mismo como el primer argumento, y el rest son los argumentos “normales.” Como el Bloque, como está escrito en el código del cliente, nunca podría acceder a ese argumento de todos modos (no existe en ese momento), decidí ignorarlo).

libffi ahora necesita hacer algo de “preparación”; siempre que eso tenga éxito (y se puede asignar espacio para el valor de retorno), el puntero de la función de invocación ahora puede ser “llamado”, y se puede establecer el valor de retorno:

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

Hay algunas demostraciones de la funcionalidad en main.m en el proyecto.

Finalmente, en cuanto a su pregunta de “¿debería hacerse esto?”, Creo que la respuesta es “sí, siempre que lo haga más productivo”. WoolDelegate es completamente genérico, y una instancia puede actuar como cualquier clase completamente escrita. Sin embargo, mi intención era hacer delegates simples y únicos, que solo necesitan uno o dos métodos, y no necesitan vivir más allá de sus delegates, menos trabajo que escribir una clase completamente nueva, y más legible. / mantenible que pegar algunos métodos delegates en un controlador de vista porque es el lugar más fácil para ponerlos. Aprovechando el tiempo de ejecución y el dinamismo del lenguaje como este, con suerte, puede boost la legibilidad de su código, de la misma manera, por ejemplo, los manejadores de NSNotification basados ​​en NSNotification sí lo hacen.

Acabo de armar un pequeño proyecto que te permite hacer solo esto …

 @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 

Y usando un 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]; 

Editar: Esto es lo que surgió después de haber entendido su requerimiento. Esto es solo un truco rápido, una idea para comenzar, no se implementa correctamente ni se prueba. Se supone que funciona para los métodos delegates que toman al remitente como su único argumento. Funciona Se supone que funciona con métodos delegates normales y de devolución de estructuras.

 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 

Uso:

 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"]]];