Category is a language feature added after Objective-C 2.0. Its main design idea is a concrete implementation of decorator patterns that dynamically add new behavior to existing classes. This paper will sort out the relevant knowledge points of classification from various aspects

The role of classification

Apple’s official recommendations are as follows:

  • Add methods to existing classes;
  • It is possible to separate the implementation of a class into several different files. This has the following advantages:
    • Can reduce the size of individual files
    • You can group different functions into different categories
    • Multiple developers can work on a class
    • You can load as many categories as you want, and so on.
  • Declare private methods

In addition, developers have developed several other usage scenarios for categories:

  • Simulated multiple inheritance
  • Expose the framework’s private methods: Declare the framework’s private methods in a class, and users can call the corresponding private methods. This scenario is now obsolete, and Apple audit does not allow developers to call their private methods.

See article Category & Extension for details

Realization of classification

The bottom layer of a category is the struct, which can be found in the <objc-runtime-new.h> file in the Runtime source code. It contains:

  • Class name (name)
  • Class (CLS)
  • A list of all instanceMethods added to a class in a category (instanceMethods)
  • A list of all the added classMethods in the category
  • List of all protocols implemented by category
  • All properties added in the category (instanceProperties)
struct category_t { const char *name; // Class name classref_t CLS; Struct method_list_t *instanceMethods; Struct method_list_t *classMethods; Struct protocol_t *protocols; Struct property_list_t *instanceProperties; // Fields below this point are not always present on disk. struct property_list_t *_classProperties; // If it is a metaclass, return the list of class methods; Method_list_t *methodsForMeta(bool isMeta) {if (isMeta) return classMethods; else return instanceMethods; } property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi); };Copy the code

From the definition of category, we can also see that category can be (can add instance methods, class methods, and even implement the protocol, add attributes) and not (can not add instance variables).

Classification loading



The category is attached to the class when map_images is called in _objc_init, and the map_images will eventually call the _read_images method in objC-Runtime-new.mm, At the end of the _read_images method, there is the following code snippet:

// Discover categories. for (EACH_HEADER) { category_t **catlist = _getObjc2CategoryList(hi, &count); for (i = 0; i < count; i++) { category_t *cat = catlist[i]; class_t *cls = remapClass(cat->cls); if (! cls) { // Category's target class is missing (probably weak-linked). // Disavow any knowledge of this category. catlist[i] = NULL; 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 (isRealized(cls)) { remethodizeClass(cls); classExists = YES; } if (PrintConnecting) { _objc_inform("CLASS: found category -%s(%s) %s", getName(cls), cat->name, classExists ? "on existing class" : ""); } } if (cat->classMethods || cat->protocols /* || cat->classProperties */) { addUnattachedCategoryForClass(cat, cls->isa, hi); if (isRealized(cls->isa)) { remethodizeClass(cls->isa); } if (PrintConnecting) { _objc_inform("CLASS: found category +%s(%s)", getName(cls), cat->name); }}}}Copy the code

This code is easy to understand, at this point the compiler does two main things:

  • 1) Add instance methods, protocols, and attributes of the category to the class
  • 2) Add the class methods and protocols of the category to the class metaclass

AddUnattachedCategoryForClass just do class and category in the code above an association mapping, and remethodizeClass is added to deal with real matters, to add instance methods, for example

static void remethodizeClass(class_t *cls) { category_list *cats; BOOL isMeta; rwlock_assert_writing(&runtimeLock); isMeta = isMetaClass(cls); // Re-methodizing: check for more categories if ((cats = unattachedCategoriesForClass(cls))) { chained_property_list *newproperties; const protocol_list_t **newprotos; if (PrintConnecting) { _objc_inform("CLASS: attaching categories to class '%s' %s", getName(cls), isMeta ? "(meta)" : ""); } // Update methods, properties, protocols BOOL vtableAffected = NO; // Add category methods (CLS, cats, &vtableAffected); Newproperties = buildPropertyList(NULL, cats, isMeta); if (newproperties) { newproperties->next = cls->data()->properties; cls->data()->properties = newproperties; Newprotos = buildProtocolList(cats, NULL, CLS ->data()->protocols); if (cls->data()->protocols && cls->data()->protocols ! = newprotos) { _free_internal(cls->data()->protocols); } cls->data()->protocols = newprotos; _free_internal(cats); // Update method caches and vtables flushCaches(cls); if (vtableAffected) flushVtables(cls); }}Copy the code

For instance methods that add classes, the attachCategoryMethods method will be called.

static void attachCategoryMethods(class_t *cls, category_list *cats, BOOL *inoutVtablesAffected) { if (! cats) return; if (PrintReplacedMethods) printReplacements(cls, cats); BOOL isMeta = isMetaClass(cls); method_list_t **mlists = (method_list_t **) _malloc_internal(cats->count * sizeof(*mlists)); // Count backwards through cats to get newest categories first int mcount = 0; int i = cats->count; BOOL fromBundle = NO; while (i--) { method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta); if (mlist) { mlists[mcount++] = mlist; fromBundle |= cats->list[i].fromBundle; } } attachMethodLists(cls, mlists, mcount, NO, fromBundle, inoutVtablesAffected); _free_internal(mlists); }Copy the code

AttachCategoryMethods does a relatively simple job of piecing together the list of instance methods for all categories into one large list of instance methods, which is then passed on to the attachMethodLists method, which we’ll look at just a little bit:

for (uint32_t m = 0;
             (scanForCustomRR || scanForCustomAWZ)  &&  m < mlist->count;
             m++)
        {
            SEL sel = method_list_nth(mlist, m)->name;
            if (scanForCustomRR  &&  isRRSelector(sel)) {
                cls->setHasCustomRR();
                scanForCustomRR = false;
            } else if (scanForCustomAWZ  &&  isAWZSelector(sel)) {
                cls->setHasCustomAWZ();
                scanForCustomAWZ = false;
            }
        }
       
        // Fill method list array
        newLists[newCount++] = mlist;
    .
    .
    .

    // Copy old methods to the method list array
    for (i = 0; i < oldCount; i++) {
        newLists[newCount++] = oldLists[i];
    }
Copy the code

As can be seen from the above implementation principle, the category method does not “completely replace” the existing methods of the original class. Instead, the category method is put in front of the new method list, and the methods of the original class are put behind the new method list. This is what we call a category method that “overrides” the methods of the original class with the same name. That is, if both the category and the original class have methodA, then when the category is attached, there will be two methodA in the class’s method list, just because the runtime looks for methods in the order in which they are listed, and will stop as soon as it finds the corresponding method. When there are multiple categories covering the same method, the order in which it is called is determined by compilation order.

Extend the extension

Role of extension

  • Declare private attributes that are not exposed to subclasses
  • Declare private methods for easy reading
  • Declare private member variables

Extended features

  • Compile-time resolution
  • It exists only as a declaration, with no concrete implementation, and in most cases resides in the.m of the host class, that is, it is not a file that exists independently of the implementation
  • An extension can be thought of as an internal private declaration of a class
  • Extensions cannot be added to system classes

The difference between classification and extension

  • Classification is a run-time resolution and extension is a compile-time resolution
  • Classes can have declarations and implementations, whereas extensions have declarations and implementations written directly into the host class
  • You can add categories to a system class, but you cannot add extensions to a system class

+load vs +initialize

Call time

There are two very special class methods + Load and + Initialize in the NSObject class that are used for class initialization.

  • The +load method is called when a class or class is added to the Objective-C Runtime, and implementing this method allows us to perform some class-related behavior at class load time.
  • The + Initialize method is called before the class or its subclasses receive the first message, which includes instance method and class method calls. That is, the +initialize method is lazy-loaded and will never be called if the program never sends a message to a class or its subclasses.

Call to order

  • When the parent class, subclass, and class have the +load method, the parent class will be called first, then the subclass, and finally the class will be called. If there are multiple classes, the call order will be determined according to the compilation order.
  • If the +initialize method is used by a parent class, a subclass, or a class at the same time, the parent class will be called first and then the subclass will be called. If the parent class has already called the + Initialize method, it will not be called again. Since calling the +initialize method is essentially a message forward, the class overrides the +initialize method.

Whether to use the parent class implementation

  • If the child does not implement the +load method, the child does not use the parent’s implementation, so the parent’s +load method is called only once when the parent is added to the Runtime
  • If a subclass does not implement the +initialize method, the subclass will use the parent class’s implementation, so the parent class’s +initialize method will be called multiple times because the subclass does not implement the +initialize method

The summary is as follows:

+load +initialize
Call time Is added to runtime It may never be called until the first message is received
Call to order Superclass -> subclass -> category Parent class -> child class
Whether an explicit call to a superclass implementation is required no no
Follow the implementation of the parent class no is
Call the number 1 time Many times

Category and associated objects

From the above analysis, we know that you can’t add instance variables to a category within a category. But most of the time we need to add the value Associated with the object in a category. Fortunately, we can compensate for this with Associated Objects. There are three functions Associated with Associated Objects, which are declared in the Runtime.h file:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

id objc_getAssociatedObject(id object, const void *key);

void objc_removeAssociatedObjects(id object);
Copy the code

The names of these three functions are very programmer-friendly and allow us to see what the function does at a glance:

  • Objc_setAssociatedObject is used to add an associated object to an object. Passing nil removes an existing associated object.
  • Objc_getAssociatedObject is used to get the associated object;
  • Objc_removeAssociatedObjects is used to remove all associated objects of an object.

But where do the associated objects exist? How to store it? How do you deal with associated objects when you destroy them? Objc-references. Mm file contains a method called _object_set_associative_reference:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) { // retain the new value (if any) outside the lock. ObjcAssociation old_association(0, nil); id new_value = value ? acquireValue(value, policy) : nil; { AssociationsManager manager; AssociationsHashMap &associations(manager.associations()); disguised_ptr_t disguised_object = DISGUISE(object); if (new_value) { // break any existing association. AssociationsHashMap::iterator i = associations.find(disguised_object); if (i ! = associations.end()) { // secondary table exists ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find(key); if (j ! = refs->end()) { old_association = j->second; j->second = ObjcAssociation(policy, new_value); } else { (*refs)[key] = ObjcAssociation(policy, new_value); } } else { // create the new association (first time). ObjectAssociationMap *refs = new ObjectAssociationMap; associations[disguised_object] = refs; (*refs)[key] = ObjcAssociation(policy, new_value); _class_setInstancesHaveAssociatedObjects(_object_getClass(object)); } } else { // setting the association to nil breaks the association. AssociationsHashMap::iterator i = associations.find(disguised_object); if (i ! = associations.end()) { ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find(key); if (j ! = refs->end()) { old_association = j->second; refs->erase(j); } } } } // release the old value (outside of the lock). if (old_association.hasValue()) ReleaseValue()(old_association);  }Copy the code

We can see that all the associated objects are managed by AssociationsManager. In AssociationsManager, there is a static AssociationsHashMap to store all the associated objects. This is equivalent to storing all associated objects in a global map. The key of the map is the pointer address of this object (the pointer address of any two different objects must be different), and the value of this map is another AssociationsHashMap, which stores the KV pair of the associated object.

Objc-runtimenew.mm: objc-runtimenew.mm:

void *objc_destructInstance(id obj) { if (obj) { Class isa_gen = _object_getClass(obj); class_t *isa = newcls(isa_gen); // Read all of the flags at once for performance. bool cxx = hasCxxStructors(isa); bool assoc = ! UseGC && _class_instancesHaveAssociatedObjects(isa_gen); // This order is important. if (cxx) object_cxxDestruct(obj); if (assoc) _object_remove_assocations(obj); if (! UseGC) objc_clear_deallocating(obj); } return obj; }Copy the code

The Runtime destructinstance function objc_destructInstance checks whether the object has any associated objects and calls _object_remove_assocations to clean up the associated objects.

Learn more about Objective-C: Category classification and Extension Extension