[Objective-C] Exploring the Essence of Category's Bottom Layer

Keywords: iOS Attribute

No matter how perfect a class design is, there may be some unpredictable situations in the future demand evolution. So how do you extend existing classes? Generally speaking, inheritance and combination are good choices. However, in Objective-C 2.0, category is also provided, which can dynamically add new behaviors to existing classes. Category is now available in every corner of Objective-C code, from Apple's official framework to open source frameworks, from large and complex APP s to simple applications, catagory is everywhere. In this paper, category has done a more comprehensive collation, hoping to benefit readers.   

The role of category characteristics in Objective-C is as follows:

(1) Class implementations can be decentralized into multiple different files or multiple different frameworks (adding new methods).

(2) Forward references to private methods can be created.

(3) Informal protocols can be added to objects.

The limitations of category characteristics in Objective-C are as follows:

(1) Categories can only add new methods to the original class, and can only add, but not delete or modify the original method, and can not add new attributes to the original class.

(2) The method added to the original class by category is globally effective and has the highest priority. If the method is renamed with the original class, the original method will be overwritten unconditionally.  

1. The underlying implementation of Category

Objective-C implements dynamic language through Runtime runtime. All classes and objects are represented by structure in Runtime. Category is represented by structure category_t in Runtime. The following is the concrete expression of structure category_t:

typedef struct category_t {
    const char *name;//Class Name Main Class Name
    classref_t cls;//class
    struct method_list_t *instanceMethods;//List of Instance Methods
    struct method_list_t *classMethods;//List of class methods
    struct protocol_list_t *protocols;//List of all protocols
    struct property_list_t *instanceProperties;//All attributes added
} category_t;

From the definition of category, we can see that category can be (instance method, class method, even protocol implementation, attribute addition) and not (instance variable cannot be added).

We will explore the implementation principle of Category with the source code of runtime. Open the runtime source project and find the following functions in the file objc-runtime-new.mm:

void _read_images(header_info **hList, uint32_t hCount)
{
    ...
        _free_internal(resolvedFutureClasses);
    }

    // Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist =
            _getObjc2CategoryList(hi, &count);
        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class",
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            BOOL classExists = NO;
            if (cat->instanceMethods ||  cat->protocols
                ||  cat->instanceProperties)
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s",
                                 cls->nameForLogging(), cat->name,
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols
                /* ||  cat->classProperties */)
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)",
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }

    // Category discovery MUST BE LAST to avoid potential races 
    // when other threads call the new category code before 
    // this thread finishes its fixups.

    // +load handled by prepare_load_methods()

    ...
}

We can see that Category is treated in this function as follows:

(1) Register Category and its main class (or metaclass) into the hash table;

(2) If the main class (or metaclass) has been implemented, rebuild its method list;

Principle of Category:

  • During compilation, the method implemented in the classification is generated as a structure method_list_t, the declared attribute is generated as a structure property_list_t, and then a structure category_t is generated through these structures.
  • Then save the structure category_t
  • At runtime, Runtime gets the structure category_t we saved at compile time
  • Then add the list of instance methods, protocols and attributes in the structure category_t to the main class.
  • Add the list of class methods and protocols in the structure category_t to the metaClass of the main class

Second, why is the method priority in Category higher than the method in the original class?

The list of methods in category_t is inserted in front of the list of methods in the main class (similar to inserting with the next pointer in the linked list), so the method implemented in Category does not really override the method in the main class, but just inserts the Category method in front of the list of methods. When the runtime finds a method, it follows the order of the method list. As soon as it finds a method with the corresponding name, it will stop searching. This illusion of overwriting the method will appear here.
// It's probably like inserting in this way.
newproperties->next = cls->data()->properties;
cls->data()->properties = newproperties;,

3. Why can't instance variables be added to Category?

Through the structure category_t, we can know that in Category we can add instance methods, class methods, protocols and attributes. There is no objc_ivar_list structure, which means we can't add instance variables to the classification.
Because at runtime, the memory layout of the object has been determined, adding instance variables will destroy the internal layout of the class, which is the fundamental reason why instance variables cannot be added in Category.
 
Reference: Deep Understanding of Objective-C: Category

Posted by jarcoal on Fri, 09 Aug 2019 04:36:35 -0700