Mobile Development 20 min read

Understanding iOS Message Forwarding and BlocksKit Dynamic Delegation

This article explains the iOS message‑sending and forwarding mechanisms—including dynamic, fast, and normal forwarding—and demonstrates how BlocksKit leverages these mechanisms to implement dynamic delegates using blocks, with detailed code examples and class descriptions.

Qunar Tech Salon
Qunar Tech Salon
Qunar Tech Salon
Understanding iOS Message Forwarding and BlocksKit Dynamic Delegation

Blocks are widely used in iOS programming as encapsulated units of logic that can be executed concurrently or as callbacks, offering two main advantages: they allow writing execution logic in the caller’s context without separating functions, and they can capture local variables.

iOS Message Sending

In Objective‑C, calling a method is equivalent to sending a message to an object, e.g.:

[obj msg];

If the method is not implemented, the runtime looks up the selector in the method cache and method table, eventually calling objc_msgSend . An uncaught exception such as:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Obj msg]: unrecognized selector sent to instance'

occurs because the instance does not implement the selector, and the message‑forwarding mechanism fails to find an implementation.

Message Forwarding Mechanisms

The Objective‑C runtime provides three forwarding strategies, executed in order: dynamic resolution, fast forwarding, and normal (slow) forwarding.

1. Dynamic Method Resolution

To resolve a missing method dynamically, a class can implement:

+(BOOL)resolveInstanceMethod:(SEL)sel;
+(BOOL)resolveClassMethod:(SEL)sel;

When [obj msg] is called, the runtime invokes resolveInstanceMethod: to search the inheritance chain for an implementation. If none is found, it returns NO , allowing the runtime to proceed to the next step. The @dynamic property declaration is a common use case.

Selectors can also be checked with:

- (BOOL)respondsToSelector:(SEL)selector;
- (void)instancesRespondToSelector:(SEL)selector;

2. Fast Forwarding

If dynamic resolution fails, the runtime gives the object a chance to redirect the message to another object by implementing:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return redirectObj;
}

The returned object becomes the new receiver for the original selector.

3. Normal (Slow) Forwarding

When fast forwarding also fails, the runtime creates an NSInvocation object. The class must provide:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (void)invokeWithTarget:(id)anObject;

methodSignatureForSelector: supplies a method signature for the NSInvocation . forwardInvocation: can either invoke the block stored in a custom object or forward the invocation to a real delegate. If no handling is provided, the runtime finally calls:

- (void)doesNotRecognizeSelector:(SEL)aSelector;

which raises NSInvalidArgumentException .

BlocksKit and Dynamic Delegation

BlocksKit uses the above forwarding mechanisms to replace traditional delegate methods with blocks. The core classes are:

A2BlockInvocation – similar to NSInvocation , stores a block and its method signature.

A2DynamicDelegate – a subclass of NSProxy that maps selectors to A2BlockInvocation objects and forwards messages.

NSObject+A2DynamicDelegate – a category that creates and caches dynamic delegate objects.

Key code from NSObject+A2DynamicDelegate :

// Find the dynamic delegate class along the inheritance chain
static Class a2_dynamicDelegateClass(Class cls, NSString *suffix) {
    while (cls) {
        NSString *className = [NSString stringWithFormat:@"A2Dynamic%@%@", NSStringFromClass(cls), suffix];
        Class ddClass = NSClassFromString(className);
        if (ddClass) return ddClass;
        cls = class_getSuperclass(cls);
    }
    return [A2DynamicDelegate class];
}

- (id)bk_dynamicDelegateWithClass:(Class)cls forProtocol:(Protocol *)protocol {
    __block A2DynamicDelegate *dynamicDelegate;
    dispatch_sync(a2_backgroundQueue(), ^{
        dynamicDelegate = objc_getAssociatedObject(self, (__bridge const void *)protocol);
        if (!dynamicDelegate) {
            dynamicDelegate = [[cls alloc] initWithProtocol:protocol];
            objc_setAssociatedObject(self, (__bridge const void *)protocol, dynamicDelegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
    });
    return dynamicDelegate;
}

Mapping a selector to a block is done in A2DynamicDelegate :

- (void)implementMethod:(SEL)selector withBlock:(id)block {
    NSCAssert(selector, @"Attempt to implement or remove NULL selector");
    BOOL isClassMethod = self.isClassProxy;
    if (!block) {
        [self.invocationsBySelectors bk_removeObjectForSelector:selector];
        return;
    }
    struct objc_method_description methodDescription = protocol_getMethodDescription(self.protocol, selector, YES, !isClassMethod);
    if (!methodDescription.name) methodDescription = protocol_getMethodDescription(self.protocol, selector, NO, !isClassMethod);
    A2BlockInvocation *inv = nil;
    if (methodDescription.name) {
        NSMethodSignature *protoSig = [NSMethodSignature signatureWithObjCTypes:methodDescription.types];
        inv = [[A2BlockInvocation alloc] initWithBlock:block methodSignature:protoSig];
    } else {
        inv = [[A2BlockInvocation alloc] initWithBlock:block];
    }
    [self.invocationsBySelectors bk_setObject:inv forSelector:selector];
}

The delegate’s methodSignatureForSelector: implementation searches several places before falling back to NSObject :

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    A2BlockInvocation *invocation = nil;
    if ((invocation = [self.invocationsBySelectors bk_objectForSelector:aSelector]))
        return invocation.methodSignature;
    else if ([self.realDelegate methodSignatureForSelector:aSelector])
        return [self.realDelegate methodSignatureForSelector:aSelector];
    else if (class_respondsToSelector(object_getClass(self), aSelector))
        return [object_getClass(self) methodSignatureForSelector:aSelector];
    return [[NSObject class] methodSignatureForSelector:aSelector];
}

And forwardInvocation: forwards to the stored block or the real delegate:

- (void)forwardInvocation:(NSInvocation *)outerInv {
    SEL selector = outerInv.selector;
    A2BlockInvocation *innerInv = nil;
    if ((innerInv = [self.invocationsBySelectors bk_objectForSelector:selector])) {
        [innerInv invokeWithInvocation:outerInv];
    } else if ([self.realDelegate respondsToSelector:selector]) {
        [outerInv invokeWithTarget:self.realDelegate];
    }
}

A2BlockInvocation stores the block’s signature and provides the method to actually invoke the block with an NSInvocation :

@property (nonatomic, strong, readonly) NSMethodSignature *methodSignature;
@property (nonatomic, copy, readonly) id block;
@property (nonatomic, readonly) NSMethodSignature *blockSignature;

The internal block structure used by BlocksKit is:

// _BKBlock structure
typedef struct _BKBlock {
    __unused Class isa;
    BKBlockFlags flags;
    __unused int reserved;
    void (__unused *invoke)(struct _BKBlock *block, ...);
    struct {
        unsigned long int reserved;
        unsigned long int size;
        void (*copy)(void *dst, const void *src);
        void (*dispose)(const void *);
        const char *signature;
        const char *layout;
    } *descriptor;
    // imported variables
} *BKBlockRef;

Compatibility between a method signature and a block signature is checked with:

+ (BOOL)isSignature:(NSMethodSignature *)signatureA compatibleWithSignature:(NSMethodSignature *)signatureB {
    if (!signatureA || !signatureB) return NO;
    if ([signatureA isEqual:signatureB]) return YES;
    if (!typesCompatible(signatureA.methodReturnType, signatureB.methodReturnType)) return NO;
    NSMethodSignature *methodSignature = nil, *blockSignature = nil;
    if (signatureA.numberOfArguments > signatureB.numberOfArguments) {
        methodSignature = signatureA;
        blockSignature = signatureB;
    } else if (signatureB.numberOfArguments > signatureA.numberOfArguments) {
        methodSignature = signatureB;
        blockSignature = signatureA;
    } else {
        return NO;
    }
    for (NSUInteger i = 2; i < methodSignature.numberOfArguments; i++) {
        if (!typesCompatible([methodSignature getArgumentTypeAtIndex:i], [blockSignature getArgumentTypeAtIndex:i - 1])) {
            return NO;
        }
    }
    return YES;
}

When a block lacks a selector argument, a method signature compatible with the block is created:

+ (NSMethodSignature *)methodSignatureForBlockSignature:(NSMethodSignature *)original {
    if (!original) return nil;
    if (original.numberOfArguments < 1) return nil;
    if (original.numberOfArguments >= 2 && strcmp(@encode(SEL), [original getArgumentTypeAtIndex:1]) == 0) {
        return original;
    }
    NSMutableString *signature = [[NSMutableString alloc] initWithCapacity:original.numberOfArguments + 1];
    const char *retTypeStr = original.methodReturnType;
    [signature appendFormat:"%s%s%s", retTypeStr, @encode(id), @encode(SEL)];
    for (NSUInteger i = 1; i < original.numberOfArguments; i++) {
        const char *typeStr = [original getArgumentTypeAtIndex:i];
        NSString *type = [[NSString alloc] initWithBytesNoCopy:(void *)typeStr length:strlen(typeStr) encoding:NSUTF8StringEncoding freeWhenDone:NO];
        [signature appendString:type];
    }
    return [NSMethodSignature signatureWithObjCTypes:signature.UTF8String];
}

Finally, the block is invoked with the outer NSInvocation :

- (BOOL)invokeWithInvocation:(NSInvocation *)outerInv returnValue:(out NSValue **)outReturnValue setOnInvocation:(BOOL)setOnInvocation {
    NSParameterAssert(outerInv);
    NSMethodSignature *sig = self.methodSignature;
    if (![outerInv.methodSignature isEqual:sig]) {
        NSAssert(0, @"Attempted to invoke block invocation with incompatible frame");
        return NO;
    }
    NSInvocation *innerInv = [NSInvocation invocationWithMethodSignature:self.blockSignature];
    void *argBuf = NULL;
    for (NSUInteger i = 2; i < sig.numberOfArguments; i++) {
        const char *type = [sig getArgumentTypeAtIndex:i];
        NSUInteger argSize;
        NSGetSizeAndAlignment(type, &argSize, NULL);
        if (!(argBuf = reallocf(argBuf, argSize))) return NO;
        [outerInv getArgument:argBuf atIndex:i];
        [innerInv setArgument:argBuf atIndex:i - 1];
    }
    [innerInv invokeWithTarget:self.block];
    NSUInteger retSize = sig.methodReturnLength;
    if (retSize) {
        if (outReturnValue || setOnInvocation) {
            if (!(argBuf = reallocf(argBuf, retSize))) return NO;
            [innerInv getReturnValue:argBuf];
            if (setOnInvocation) [outerInv setReturnValue:argBuf];
            if (outReturnValue) *outReturnValue = [NSValue valueWithBytes:argBuf objCType:sig.methodReturnType];
        }
    } else {
        if (outReturnValue) *outReturnValue = nil;
    }
    free(argBuf);
    return YES;
}

In summary, the runtime’s three‑step forwarding process (dynamic resolution → fast forwarding → normal forwarding) enables BlocksKit to replace traditional delegate methods with blocks, offering a concise and flexible way to handle callbacks in iOS applications.

iOSRuntimeObjective‑CMessage ForwardingBlocksKitDynamic Delegation
Qunar Tech Salon
Written by

Qunar Tech Salon

Qunar Tech Salon is a learning and exchange platform for Qunar engineers and industry peers. We share cutting-edge technology trends and topics, providing a free platform for mid-to-senior technical professionals to exchange and learn.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.