Search code examples
objective-cobjective-c-runtimemethod-swizzling

Using dispatch_once in method swizzling


In NSHipter's article on method swizzling, it says "Swizzling should always be done in a dispatch_once." Why is this necessary since +load happens only once per class?


Solution

  • It's not required. +load is guaranteed to be thread-safe and reentrant. See load_images in objc-runtime-new.mm:

    /***********************************************************************
    * load_images
    * Process +load in the given images which are being mapped in by dyld.
    * Calls ABI-agnostic code after taking ABI-specific locks.
    *
    * Locking: write-locks runtimeLock and loadMethodLock
    **********************************************************************/
    const char *
    load_images(enum dyld_image_states state, uint32_t infoCount,
                const struct dyld_image_info infoList[])
    {
        BOOL found;
    
        recursive_mutex_lock(&loadMethodLock);
    
        // Discover load methods
        rwlock_write(&runtimeLock);
        found = load_images_nolock(state, infoCount, infoList);
        rwlock_unlock_write(&runtimeLock);
    
        // Call +load methods (without runtimeLock - re-entrant)
        if (found) {
            call_load_methods();
        }
    
        recursive_mutex_unlock(&loadMethodLock);
    
        return nil;
    }
    

    Notice the recursive mutex, that guarantees that all loads are done while blocking, and call_load_methods() will ensure that +load only gets invoked once per implementation of +load.

    Note that +load is special in that there can be multiple implementations of it on a per-class basis, which is one of the reasons why it's preferred for swizzling - your +load, as well as the original +load is guaranteed to be called.

    Bonus: Here's the relevant documentation on call_load_methods() that directly addresses why this is thread safe in the way that it is:

    /***********************************************************************
    * call_load_methods
    * Call all pending class and category +load methods.
    * Class +load methods are called superclass-first. 
    * Category +load methods are not called until after the parent class's +load.
    * 
    * This method must be RE-ENTRANT, because a +load could trigger 
    * more image mapping. In addition, the superclass-first ordering 
    * must be preserved in the face of re-entrant calls. Therefore, 
    * only the OUTERMOST call of this function will do anything, and 
    * that call will handle all loadable classes, even those generated 
    * while it was running.
    *
    * The sequence below preserves +load ordering in the face of 
    * image loading during a +load, and make sure that no 
    * +load method is forgotten because it was added during 
    * a +load call.
    * Sequence:
    * 1. Repeatedly call class +loads until there aren't any more
    * 2. Call category +loads ONCE.
    * 3. Run more +loads if:
    *    (a) there are more classes to load, OR
    *    (b) there are some potential category +loads that have 
    *        still never been attempted.
    * Category +loads are only run once to ensure "parent class first" 
    * ordering, even if a category +load triggers a new loadable class 
    * and a new loadable category attached to that class. 
    *
    * Locking: loadMethodLock must be held by the caller 
    *   All other locks must not be held.
    **********************************************************************/
    void call_load_methods(void)