Swift & runtimeHistorical technical note

Reading the Objective-C runtime source to understand messaging

Uses an unrecognized selector error to introduce Objective-C messaging, examining method lookup, caching and forwarding in the source. Runtime examples then explain how a method call is resolved during execution.

Original byline:Sindri

Originally credited to Sindri; edited and archived by Bison.

Source listed in the original

English translation of the Chinese original, translated on . The article body uses automated translation; app descriptions, historical reading notes and video subtitles receive separate editorial checks. Original publication dates, attribution, code and version notes are retained.

iOS has a general object type, id, that can represent any object. Calling any existing method on an object typed as id therefore does not produce a compiler error. For example:

Editor: Bison
Contributor: Sindri’s Notes

id wrongArr = @"This is a NSString instance.";
[wrongArr addObject: @"The operate will crash your application"];


As expected, the compiler displays this message and the app crashes. Almost every developer has encountered it:

-[__NSCFConstantString addObject:]: unrecognized selector sent to instance 0x10675c060

The reason is simple: we sent a method not belonging to the instance at 0x10675c060 . This is not saying instance 0x10675c060 called unrecognized selector, but reporting a message-send error. Every method call on an OC对象 is actually a message send.

1

Static and dynamic languages

First, a programming-language distinction: dynamic versus static languages. Objective-C is dynamic. The differences are:

  • Static languages check types before execution. The memory addresses of class members and methods are determined at compile time, and members can access only their own methods and variables. A call like the one above would fail compilation and produce an error. This also gives static languages a structured form, easier debugging, and various opportunities for performance optimization. Common examples include java, C++, and C.

  • Dynamic languages defer most checks to runtime, resolving member variables and method addresses then. Members and methods can be added dynamically at runtime. They are flexible, customisable and readable, but methods generally cannot benefit from inlining and similar optimisations.

I will not judge which approach is better here, but smalltalk is a pioneer of dynamic languages and a major influence on Objective-C. In smalltalk, everything is an object, or should be treated as one. For example, 2 + 3 is understood as sending the object 2 the message + with the argument 3.

Message sending

In the previous article,Runtime: properties and variables we imported a runtime header for one-step archiving. Today we import a different file:<objc/message.h>

In Objective-C, a method call looks like this:

[davin playWith: friend];

When a method is called, the runtime converts it into a C function call, sending davin the playWith: message with the argument friend:

objc_msgSend(davin, @selector(playWith:), friend);


What happens inside this C function, and how is the class method found? Apple has open-sourced runtime implementation. For performance, Apple wrote this function in assembly. The source is under Source/objc-msg-arm.s file):

/*****************************************************************
 *
 * id objc_msgSend(id self, SEL	_cmd,...);
 *
 *****************************************************************/
    ENTRY objc_msgSend
    MESSENGER_START

    cbz	r0, LNilReceiver_f    // 判断消息接收者是否为nil

    ldr	r9, [r0]		      // r9 = self->isa
    CacheLookup NORMAL           // 到缓存中查找方法

LCacheMiss:                      // 方法未缓存
    MESSENGER_END_SLOW
    ldr	r9, [r0, #ISA]		
    b	__objc_msgSend_uncached

LNilReceiver:                    // 消息接收者为nil处理
    mov	r1, #0
    mov	r2, #0
    mov	r3, #0
    FP_RETURN_ZERO
    MESSENGER_END_NIL
    bx	lr	

LMsgSendExit:
    END_ENTRY objc_msgSend


Even without assembly knowledge, the comments reveal what is happening. The code shows the following stages of a method call:

  • Check whether the receiver is nil. If so, clear registers and return nil from message sending.

  • Search the class cache; if the method exists, return it directly.

  • If uncached, search the class’s method list in order.

The method implementation is looked up through the unusually named function _class_lookupMethodAndLoadCache3:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                          YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    Class curClass;
    IMP methodPC = nil;
    Method meth;
    bool triedResolver = NO;
    
    methodListLock.assertUnlocked();

    // 如果传入的cache为YES,到类缓存中查找方法缓存
    if (cache) {
        methodPC = _cache_getImp(cls, sel);
        if (methodPC) return methodPC;    
    }

    // 判断类是否已经被释放
    if (cls == _class_getFreedObjectClass())
        return (IMP) _freedHandler;

    // 如果类未初始化,对其进行初始化。如果这个消息是initialize,那么直接进行类的初始化
    if (initialize  &&  !cls->isInitialized()) {
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }

 retry:
    methodListLock.lock();

    // 忽略在GC环境下的部分消息,比如retain、release等
    if (ignoreSelector(sel)) {
        methodPC = _cache_addIgnoredEntry(cls, sel);
        goto done;
    }

    // 遍历缓存方法,如果找到,直接返回
    methodPC = _cache_getImp(cls, sel);
    if (methodPC) goto done;

    // 遍历类自身的方法列表查找方法实现
    meth = _class_getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, cls, meth, sel);
        methodPC = method_getImplementation(meth);
        goto done;
    }

    // 尝试向上遍历父类的方法列表查找实现
    curClass = cls;
    while ((curClass = curClass->superclass)) {
        // Superclass cache.
        meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
        if (meth) {
            if (meth != (Method)1) { 
                log_and_fill_cache(cls, curClass, meth, sel);
                methodPC = method_getImplementation(meth);
                goto done;
            }
            else {
                // Found a forward:: entry in a superclass.
                // Stop searching, but don't cache yet; call method 
                // resolver for this class first.
                break;
            }
        }

        // 查找父类的方法列表
        meth = _class_getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, curClass, meth, sel);
            methodPC = method_getImplementation(meth);
            goto done;
        }
    }

    // 没有找到任何的方法实现,进入消息转发第一阶段“动态方法解析”
    // 调用+ (BOOL)resolveInstanceMethod: (SEL)selector
    // 征询接收者所属的类是否能够动态的添加这个未实现的方法来解决问题
    if (resolver  &&  !triedResolver) {
        methodListLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        triedResolver = YES;
        goto retry;
    }

    // 仍然没有找到方法实现进入消息转发第二阶段“备援接收者”
    // 先后会调用 -(id)forwardingTargetForSelector: (SEL)selector 
    // 以及 - (void)forwardInvocation: (NSInvocation*)invocation 进行最后的补救
    // 如果补救未成功抛出消息发送错误异常
    _cache_addForwardEntry(cls, sel);
    methodPC = _objc_msgForward_impcache;

 done:
    methodListLock.unlock();

    assert(!(ignoreSelector(sel)  &&  methodPC != (IMP)&_objc_ignored_method));
    return methodPC;
}


That is the complete method-call process, divided into three main parts:

  • Look for a corresponding cached method; if found, call it directly.
    For performance, the method cache uses a hash table, discussed in more detail next.

  • If the cache misses, search the class itself and then up its hierarchy for the implementation. The returned method_t * type is also named Method.

    //非加锁状态下查找方法实现
    static method_t * getMethodNoSuper_nolock(Class cls, SEL sel)
    {
        runtimeLock.assertLocked();
    
        assert(cls->isRealized());
        // fixme nil cls? 
        // fixme nil sel?
        for (auto mlists = cls->data()->methods.beginLists(), 
                end = cls->data()->methods.endLists(); 
                 mlists != end;
                   ++mlists)
        {
            method_t *m = search_method_list(*mlists, sel);
            if (m) return m;
        }
    
        return nil;
    }
    
    // 搜索方法列表
    static method_t * search_method_list(const method_list_t *mlist, SEL sel)
    {
        int methodListIsFixedUp = mlist->isFixedUp();
        int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
        if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
              // 对有序数组进行线性探测
              return findMethodInSortedMethodList(sel, mlist);
        } else {
            // Linear search of unsorted method list
            for (auto& meth : *mlist) {
                if (meth.name == sel) return &meth;
            }
        }
    
    #if DEBUG
        // sanity-check negative results
        if (mlist->isFixedUp()) {
            for (auto& meth : *mlist) {
                if (meth.name == sel) {
                    _objc_fatal("linear search worked when binary search did not");
                }
            }
        }
    #endif
    
        return nil;
    }
    


If an implementation is found here, add it to the method cache so the next call can find it quickly:

  // 记录并且缓存方法
  static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
  {
  #if SUPPORT_MESSAGE_LOGGING
      if (objcMsgLogEnabled) {
          bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                  cls->nameForLogging(),
                                  implementer->nameForLogging(), 
                                  sel);
          if (!cacheIt) return;
      }
  #endif
      cache_fill (cls, sel, imp, receiver);
  }

  //在无加锁状态下缓存方法
  static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
  {
      cacheUpdateLock.assertLocked();

      if (!cls->isInitialized()) return;
      if (cache_getImp(cls, sel)) return;

      cache_t *cache = getCache(cls);
      cache_key_t key = getKey(sel);

      // 如果缓存占用不到3/4,进行缓存。
      mask_t newOccupied = cache->occupied() + 1;
      mask_t capacity = cache->capacity();
      if (cache->isConstantEmptyCache()) {
          cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
      }
      else if (newOccupied <= capacity / 4 * 3) {
      }
      else {
          // 扩充缓存。为了性能,扩充后原有缓存方法全部移除
          cache->expand();
      }
      bucket_t *bucket = cache->find(key, receiver);
      if (bucket->key() == 0) cache->incrementOccupied();
      bucket->set(key, imp);
  }


If no implementation exists in the class, repeatedly obtain the superclass and search again. Once found, cache it in the original class rather than the superclass.

  • If no implementation is found, message forwarding provides a final recovery mechanism.

    Message forwarding has two stages here. In the first, we can add a method dynamically and let the lookup run again. The second is called 备援的接收者: finding another object to handle the event.

    void _class_resolveMethod(Class cls, SEL sel, id inst)
    {
        // 非beta类的情况下直接调用 resolveInstanceMethod 方法
        if (! cls->isMetaClass()) {
            _class_resolveInstanceMethod(cls, sel, inst);
        } 
        else {
            // 先调用 resolveClassMethod 请求动态添加方法
            // 然后进行一次查找判断是否处理完成
            // 如果没有添加,再调用 resolveInstanceMethod 方法
            _class_resolveClassMethod(cls, sel, inst);
            if (!lookUpImpOrNil(cls, sel, inst, 
                          NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
            {
                _class_resolveInstanceMethod(cls, sel, inst);
            }
        }
    }
    


Method caching

As explained in the previous runtime article, every Objective-C object is essentially wrapped in an objc_class structure. In objc-runtime-new.h in the latest runtime source, objc_class has the following structure, with most functions omitted here:

struct objc_class : objc_object {
    Class superclass;          // Class ISA;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
    bits.setData(newData);
    }
    // .........
}

The structure is clear: cache stores the method cache used during lookup. The cache uses a hash table to greatly accelerate retrieval:

#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    // functions
}

// cache method
buckets = (cache_entry **)cache->buckets;
for (index = CACHE_HASH(sel, cache->mask); 
     buckets[index] != NULL; 
     index = (index+1) & cache->mask)
{ }
buckets[index] = entry;

After each call to a method not already cached, the cache-insertion code below runs. Apple performs a simple bitwise operation on the sel pointer and mask, then finds an empty slot to store it. From this, we can infer how sel implementations are found in the cache through CacheLookup . For maximum performance, Apple implements the lookup in assembly. The official comments are sufficient to follow its broad outline:

.macro CacheLookup

    ldrh	r12, [r9, #CACHE_MASK]	// r12 = mask
    ldr	r9, [r9, #CACHE]	// r9 = buckets
.if $0 == STRET  ||  $0 == SUPER_STRET
    and	r12, r12, r2		// r12 = index = SEL & mask
.else
    and	r12, r12, r1		// r12 = index = SEL & mask
.endif
    add	r9, r9, r12, LSL #3	// r9 = bucket = buckets+index*8
    ldr	r12, [r9]		// r12 = bucket->sel
2:
.if $0 == STRET  ||  $0 == SUPER_STRET
    teq	r12, r2
.else
    teq	r12, r1
.endif
    bne	1f
    CacheHit $0
1:	
    cmp	r12, #1
    blo	LCacheMiss_f		// if (bucket->sel == 0) cache miss
    it	eq			// if (bucket->sel == 1) cache wrap
    ldreq	r9, [r9, #4]		// bucket->imp is before first bucket
    ldr	r12, [r9, #8]!		// r12 = (++bucket)->sel
    b	2b

.endmacro


The source can be downloaded from Apple open source here. Apple’s comments explain this method clearly.

Everything above concerns method caching and lookup, but what is a method itself? Objective-C abstracts a method as the Method data type. Readers who have used the runtime may recognize it. Its structure is:

struct old_method {
    SEL method_name;
    char *method_types;
    IMP method_imp;
};
typedef struct method_t *Method;


  • method_imp is the method's implementation code, which you can think of as a block. In fact, the latter can be converted to an IMP to perform certain runtime tricks.

  • method_types method-argument encoding. What does that mean? In Properties and variables I explained that each data type has its own character encoding. Here these encode method return values and parameters, for example:-(void)playWith:(id) has the character encoding v@:@

  • method_name, as its name suggests, is the method name. We normally use @selector() to obtain a method's sel地址, which participates in hashing to store its imp implementation. Since SEL uses hashing, identically named methods in the same class prevent the imp address from being unique. This is also why Apple does not allow same-named methods distinguished only by parameter type.

Message forwarding

Calling a method that does not belong to an object usually crashes the app. I have encountered multiple reports of crashes caused by NSNull values returned by a backend. The method-call source above shows that a missing implementation does not trigger an immediate crash. Before crashing, message forwarding gives us three opportunities to avoid it and find an object that can respond.

1

Consider the first stage. User experience matters in iOS development, and crashing merely because of an unexpected data type is disruptive. We can use class_addMethod to add handling dynamically. A class can add methods after registration with objc_registerClassPair, but cannot dynamically add properties; see the category mechanism.

id wrongTypeGetter(id object, SEL sel) {
    return nil;
}

void wrongTypeSetter(id object, SEL sel, id value) {
    // do nothing
}

+ (BOOL)resolveInstanceMethod: (SEL)selector
{
    NSString * selName = NSStringFromSelector(selector);
    if ([sel hasPrefix: @"set"]) {
        class_addMethod(self, selector, (IMP)wrongTypeSetter, "v@:@");
    } else {
        class_addMethod(self, selector, (IMP)wrongTypeGetter, "@@:")
    }
}

At the beginning of the second stage, it is assumed that the original receiver will not handle this method, so we need a 消息接盘侠. This can be useful. iOS does not support multiple inheritance, although protocols and composition can simulate it. The difference is that 多继承是将多个类的功能组合到一个对象当中, whereas 伪多继承多个类的功能依旧分布在不同对象当中,但是对象彼此对消息发送者透明. Forwarding a message to another object can implement this form of simulated multiple inheritance.

@interface Person: NSObject

@property (nonatomic, strong) NSNumber * age;

@end

@implementation Person

- (id)forwardingTargetForSelector: (SEL)aSelector
{
    // 甚至可以通过runtime遍历自己属性找到可以响应方法的接盘侠
    NSString * selName = NSStringFromSelector(aSelector);
    if ([selName hasSuffix: @"Value"]) {
        return self.age;
    }
    return nil;
}

@end

// View controller
id p = [[Person alloc] init];
[p setAge: @(18)];
NSLog(@"%lu, %.2f", [p integerValue], [p doubleValue]);    //18, 18.00

If you still have not found another object to handle the method, this is the last chance to prevent a crash. The runtime needs to build a methodSignature, obtained by calling the receiver's -(NSMethodSignature *)methodSignatureForSelector:. It contains parameter types, parameter count, receiver information, and so on. This is assembled into an NSInvocation for the final forwarding attempt, calling the receiver's -forwardInvocation:. We can modify the invocation freely, including its argument values and receiver. I most often use this to reduce array iteration:

@implementation NSArray(LXDRuntime)

- (void)forwardInvocation: (NSInvocation *)anInvocation
{
    for (id item in self) {
        if ([item respondsToSelector: anInvocation.selector]) {
            [anInvocation invokeWithTarget: item];
        }
    }
}

@end


The message-sending process is summarised in this diagram:

1

Although message forwarding can significantly reduce crashes, do not add these protections during development. Add them only around store submission, so unexpected message-sending problems are not overlooked during development.

Message-mechanism techniques

The sequence behind a method call is clearly complex, but these features are worth learning and using. The runtime provides a series of Method operations for aspect-oriented programming, including replacing and exchanging method implementations.

Suppose I need a round button that responds only to taps within its circle. Use a UIButton+LXDRuntime extension to replace the existing -pointInside:withEvent: method.

@interface UIButton(LXDRuntime)

@property (nonatomic, assign) BOOL roundTouchEnable;

@end


const void * LXDRoundTouchEnableKey = & LXDRoundTouchEnableKey;
@implementation UIButton(LXDRuntime)

- (BOOL)roundTouchEnable
{
    return [objc_getAssociatedObject(self, LXDRoundTouchEnableKey) boolValue];
}

- (void)setRoundTouchEnable: (BOOL)roundTouchEnable
{
    objc_setAssociatedObject(self, LXDRoundTouchEnableKey, @(roundTouchEnable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)replacePointInside: (CGPoint)point withEvent: (UIEvent *)event
{
    if (CGRectGetWidth(self.frame) != CGRectGetHeight(self.frame) 
        || !self. roundTouchEnable)
    {
        return [super pointInside: point withEvent: event];
    }
    CGFloat radius = CGRectGetWidth(self.frame) / 2;
    CGPoint offset = CGPointMake(point.x - radius, point.y - radius);
    return sqrt(offset.x * offset.x + offset.y * offset.y) <= radius;
}

// 替换方法实现
+ (void)initialize
{
    [super initialize];
    Method replaceMethod = class_getInstanceMethod([self class], @selector(replacePointInside:withEvent:));
    Method originMethod = class_getInstanceMethod([self class], @selector(pointInside:withEvent:));
    method_setImplementation(originMethod, method_getImplementation(replaceMethod));
}

@end

When I need a button to respond only within a circular region, setting button.roundTouchEnable = YES enables that hit test automatically. Besides method replacement, another common runtime trick exchanges two implementations. This is possible because the Method structure separates the method name sel from the implementation imp. Think of an imp as a block code block: exchanging implementations is like swapping these two block s.

@interface Person : NSObject

@property (nonatomic, strong) NSString * name;
@property (nonatomic, strong) NSNumber * age;

@end

@implementation Person

+ (void)initialize
{
    [super initialize];
    Method ageGetter = class_getInstanceMethod([self class], @selector(age));
    Method nameGetter = class_getInstanceMethod([self class], @selector(name));
    method_exchangeImplementations(ageGetter, nameGetter);
}

// View controller
Person * p = [[Person alloc] init];
p.age = @(56);
p.name = @"Job Steve";

NSLog(@"%@ is %@ year old", p.name, p.age); 
// LXDRuntimeDemo[7316:244912] 56 is Job Steve year old

@end

The code above exchanges the implementations of name and age, illustrated as follows:

1

It is easy to see why method_exchangeImplementations is favored: this exchange does not alter the original implementation code. Throughout the process, age and its IMP, and name and its IMP, are not modified. Its disadvantages are also clear:

  • If multiple developers swizzle the same method, business logic becomes complex and debugging difficult.

  • Swapping affects every instance and subclass of that class, making it unsuitable for changing a single object.

The tricks offered by the runtime are double-edged tools; sensible use makes them powerful. Beyond Method, there is an IMP pitfall worth mentioning. As noted above, IMP and block are similar. The former can be cast to a function pointer and viewed as a special block. The system provides conversions through imp_implementationWithBlock and imp_getBlock. When a method call becomes a message send, objc_msgSend already has two parameters, id object and SEL aSelector. Following that reasoning, conversion between IMP and block might appear to work like this:

+ (void)initialize
    void (^requestBlock)(id object, SEL aSelector, id URL, id parameters) = 
        ^(id object, SEL aSelector, id URL, id parameters) {
        // do some networking request
    };
    IMP requestIMP = imp_implementationWithBlock(requestBlock);
    class_addMethod([self class], @selector(networkReuqest:parameters:), requestIMP, "v@:@@");
}

// View controller
[self performSelector: @selector(networkReuqest:parameters:) withObject: URL withObject: parameters];


The code above crashes in a puzzling way with an EXC_BAD_ACCESS error. This point bears repeating three times:

The block argument has no SEL!
The block argument has no SEL!
The block argument has no SEL!

Remove the SEL aSelector parameter and the code executes normally.

Closing remarks

runtime Every iOS developer should understand this. Through runtime source reveals Apple’s extensive performance optimisations and the hidden implementation beneath the code we write. As runtime The second article in the series took a long time to begin; reading the source was overwhelming, but it deepened my understanding of development and code structure. This article has no companion code, but provides Apple’s Runtime source URL


Recommended reading: Rich text with hyperlinks on iOS