SoFunction
Updated on 2025-04-12

Detailed explanation of Category implementation classification example of Objective-C

introduction

WritingObjective-CWhen code, if you want to add some methods to classes that cannot obtain the source code,CategoryThat is, classification is a good method. This article will take you to understand how classification implements adding methods to classes.

Let's talk about the conclusion first, the methods in the classification will becomecategory_tThe variables of the structure are merged into the main class at runtime. The methods in the classification will be placed before the methods in the main class, and the original methods in the main class will not be overwritten. At the same time, the classification method with the same name and the later compiled classification method will "overwrite" the first compiled classification method.

Compile time

At compile time, all the categories we write will be converted intocategory_tvariables of structures,category_tThe source code is as follows:

struct category_t {
    const char *name; // Category name    classref_t cls; // Main class    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods; // Instance method    WrappedPtr<method_list_t, PtrauthStrip> classMethods; // Class method    struct protocol_list_t *protocols; // protocol    struct property_list_t *instanceProperties; // Attributes    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties; // Class attributes    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

This structure is mainly used to store information that can be expressed in the classification, and it also indirectly shows that classification cannot create instance variables.

Runtime

map_images_nolockIt is the beginning of the runtime, and it also determines the influence of the compilation order on the priority between classification methods. The later compilation classification method will be placed before the first compilation:

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    ...
    {
        uint32_t i = mhCount;
        while (i--) { // The order of reading header_info determines that the later compiled classification method will be placed before the first compile            const headerType *mhdr = (const headerType *)mhdrs[i];
            auto hi = addHeader(mhdr, mhPaths[i], totalClasses, unoptimizedTotalClasses);
    ...

At runtime, the starting method for loading the classification isloadAllCategories, you can see that the method fromFirstHeaderStart, traverse allheader_infoand call it in turnload_categories_nolockThe method is implemented as follows:

static void loadAllCategories() {
    mutex_locker_t lock(runtimeLock);
    for (auto *hi = FirstHeader; hi != NULL; hi = hi->getNext()) {
        load_categories_nolock(hi);
    }
}

existload_categories_nolockIn the method, we will determine whether the class isstubClassWhether the initialization is completed is determined where the classification is attached. The implementation is as follows:

static void load_categories_nolock(header_info *hi) {
    // Whether it has class attributes    bool hasClassProperties = hi->info()->hasCategoryClassProperties();
    size_t count;
    auto processCatlist = [&](category_t * const *catlist) { // Get the list of categories that need to be processed        for (unsigned i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls); // Get the main class corresponding to the classification            locstamped_category_t lc{cat, hi};
            if (!cls) { // The main class cannot be obtained (maybe because of weak links), skip this loop                // Category's target class is missing (probably weak-linked).
                // Ignore the category.
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class",
                                 cat->name, cat);
                }
                continue;
            }
            // Process this category.
            if (cls->isStubClass()) { // If stubClass is, it is impossible to determine which metaclass object is, so it is attached to stubClass itself first                // Stub classes are never realized. Stub classes
                // don't know their metaclass until they're
                // initialized, so we have to add categories with
                // class methods or properties to the stub itself.
                // methodizeClass() will find them and add them to
                // the metaclass as appropriate.
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    objc::(lc, cls);
                }
            } else {
                // First, register the category with its target class.
                // Then, rebuild the class's method lists (etc) if
                // the class is realized.
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    if (cls->isRealized()) { // Indicates that the class object has been initialized and the merge method will be entered.                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    } else {
                        objc::(lc, cls);
                    }
                }
                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                    if (cls->ISA()->isRealized()) { // Indicates that the metaclass object has been initialized and will enter the merge method.                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else {
                        objc::(lc, cls->ISA());
                    }
                }
            }
        }
    };
    processCatlist(hi->catlist(&count));
    processCatlist(hi->catlist2(&count));
}

The method of merging classification is throughattachCategoriesWhen the method is performed, the method, attribute and protocol are attached separately. It should be noted that in the new version of the runtime method, the method is not placed in therwIn the middle, a new one was created calledrweThe purpose of the attribute is to save memory. The implementation of the method is as follows:

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }
    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];
    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS); // Is it a metaclass object    auto rwe = cls->data()->extAllocIfNeeded(); // Allocate storage space for rwe generation    for (uint32_t i = 0; i < cats_count; i++) { // traverse the classification list        auto& entry = cats_list[i];
        method_list_t *mlist = ->methodsForMeta(isMeta); // Get a list of instance methods or class methods        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) { // When the capacity limit of the container is reached                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__); // Prepare method list                rwe->(mlists, mcount); // Attach method to main class                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist; // Put the classification method list into the prepared container            fromBundle |= ->isBundle();
        }
        property_list_t *proplist =
            ->propertiesForMeta(isMeta, ); // Get the object attribute or class attribute list        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) { // Attach when the capacity limit of the container is reached                rwe->(proplists, propcount); // Attach attributes to class or metaclass                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }
        protocol_list_t *protolist = ->protocolsForMeta(isMeta); // Get the protocol list        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) { // Attach when the capacity limit of the container is reached                rwe->(protolists, protocount); // Attach the compliant protocol to the class or metaclass                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }
    // Attach the remaining methods, properties and protocols    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        rwe->(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->();
            });
        }
    }
    rwe->(proplists + ATTACH_BUFSIZ - propcount, propcount);
    rwe->(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

The real method is attachedattachListsThe function of a method is to place the classification method in a class object or a metaclass object, and put it in front of the original methods of the class and metaclass object. This is why if a method with the same name appears in a class and a class, the classification will be called first. It also explains from the side that the methods in the original class are not actually overwritten:

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return; // The number is 0 and returns directly        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count; // Number of original method list            uint32_t newCount = oldCount + addedCount; // Number of merged method lists            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount)); // Create a new array            newArray->count = newCount;
            array()->count = newCount;
            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i]; // Put the original method at the end of the newly created array            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i]; // Put the method in the classification in front of the array            free(array()); // Free up the memory space of the original array            setArray(newArray); // Use the merged array as a new method array            validate();
        }
        else if (!list  &&  addedCount == 1) { // If the method list does not exist, replace it directly            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        } 
        else { // If there is only one list, it becomes multiple, follow this logic            // 1 list -> many lists
            Ptr<List> oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount; // Calculate the number of all methods lists            setArray((array_t *)malloc(array_t::byteSize(newCount))); // Allocate new memory space and assign value            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList; // Put the original method at the end of the newly created array            for (unsigned i = 0; i < addedCount; i++) // Put the method in the classification in front of the array                array()->lists[i] = addedLists[i]; 
            validate();
        }
    }

The above is the detailed explanation of the Objective-C implementation classification example. For more information about Objective-C classification, please pay attention to my other related articles!