Category

Introduction of the Category

Category is a language feature added after Objective-C 2.0. The main purpose of category is to add methods to existing classes without changing the content of the original class. In addition, Apple recommends two other scenarios for using categories:

  • You can separate the implementation of a class into several different files. There are several obvious benefits to doing this,
    • 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
  • Declare private methods

However, in addition to the usage scenarios recommended by Apple, the majority of developers have rich imagination and have derived several other usage scenarios of category:

  • Simulated multiple inheritance
  • Expose the framework’s private methods

Category simple implementation

@interface MJPerson : NSObject
- (void)run;
@end

@implementation MJPerson
- (void)run
{
    NSLog(@"MJPerson - run");
}
@end

@interface MJPerson (Test)
- (void)test;
@end

@implementation MJPerson (Test)
- (void)test
{
    NSLog(@"test");
}
+ (void)test2
{
}
@end
Copy the code

Category underlying structure

We convert.m files to.cpp files by using the following command:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MJPerson+Test.m -o MJPerson+Test.cpp
Copy the code

In the MJPerson+ test.cpp file, the underlying structure of a Category actually looks like this:

struct _category_t {
    const char *name;  // Class name
    struct _class_t *cls;  / / class
    const struct _method_list_t *instanceMethods;  // A list of all instance methods added to the class in the category
    const struct _method_list_t *classMethods;  // A list of all the added class methods in the category
    const struct _protocol_list_t *protocols;  // A list of all the protocols implemented by category
    const struct _prop_list_t *instanceProperties;  // All attributes added to the category
};
Copy the code

Here is the implementation of Category MJPerson(test) :

// List of object methods
static struct/ * _method_list_t* / {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1].
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test"."v16@0:8", (void *)_I_MJPerson_Test_test}}
};
// List of class methods
static struct/ * _method_list_t* / {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1].
} _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test2"."v16@0:8", (void *)_C_MJPerson_Test_test2}}
};
/ / Category
static struct _category_t _OBJC_The $_CATEGORY_MJPerson_The $_Test __attribute__ ((used.section(" __DATA, __objc_const"))) =
{
	"MJPerson".0.// &OBJC_CLASS_$_MJPerson,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Test,
	0.0};Copy the code

As you can see, object methods added to the Category are not integrated into the class object after compilation, but are stored in the _category_t structure. When are they integrated into the class object? Next we explore objc’s source code to see how the Runtime loads categories and integrates them into class objects and metaclass objects.

Open the objC source code, search for category_t {, and find that the definition of category_t in the source code is basically the same as the compiled definition.

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t* _classProperties;

    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

The loading process of a Category

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unopt imizedTotalClasses)
{
		... / / to omit
    
		// Discover and process all categories
		for (EACH_HEADER) {
         // The outer loop iterates through the current class to find the Category array corresponding to the class
         category_t **catlist = _getObjc2CategoryList(hi, &count);
         bool hasClassProperties = hi->info() - >hasCategoryClassProperties(a);// The inner loop iterates through all categories of the current class
         for (i = 0; i < count; i++) {
             category_t *cat = catlist[i];
             Class cls = remapClass(cat->cls);
             // First, register the Category by its owning class. If the class is already implemented, the list of methods for the class is reconstructed
             bool classExists = NO;
             if (cat->instanceMethods || cat->protocols || cat->instanceProperties) {
                 // Add the Category to the corresponding value, which is the array of all categories corresponding to the Class
                 addUnattachedCategoryForClass(cat, cls, hi);
                 // Add the method, protocol, and property of the Category to the Class
                 if (cls->isRealized()) {
                   	 // Reorganize Class
                     remethodizeClass(cls); classExists = YES; }}// Same logic as above, but in Meta Class
             if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) {
                 addUnattachedCategoryForClass(cat, cls->ISA(), hi); 
                 if (cls->ISA() - >isRealized()) {
                     remethodizeClass(cls->ISA()); }}}}.../ / to omit
}

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting(a); isMeta = cls->isMetaClass(a);// Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        // Append the Category to the Class
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats); }}static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if(! cats)return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass(a);// fixme rearrange to remove these intermediate allocations
  	// Array of methods
  	/* [ [method_t, method_t], [method_t, method_t] ] */
    method_list_t **mlists = (method_list_t* *)malloc(cats->count * sizeof(*mlists));
  	// Attribute array
  	/* [ [property_t, property_t], [property_t, property_t] ] */
    property_list_t **proplists = (property_list_t* *)malloc(cats->count * sizeof(*proplists));
  	// Protocol array
  	/* [ [protocol_t, protocol_t], [protocol_t, protocol_t] ] */
    protocol_list_t **protolists = (protocol_list_t* *)malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
      	// Retrieve a category
        auto& entry = cats->list[i];
				[method_t, method_t]
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle(a); }// From this we can see that the last Category to be compiled will be placed at the top of the mlists
      
				// Retrieve the list of attributes in the classification
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
				// Retrieve the protocol list in the classification
        protocol_list_t *protolist = entry.cat->protocols;
        if(protolist) { protolists[protocount++] = protolist; }}Struct class_rw_t {}
    auto rw = cls->data(a);prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
  	// Append all classified methods to the class object's list of methods
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
		// Append the attributes of all classes to the attribute list of the class object
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
		// Append all classified protocols to the protocol list of the class object
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;

    if (hasArray()) {
        // many lists -> many lists
        uint32_t oldCount = array()->count;
        uint32_t newCount = oldCount + addedCount;
      	/ / capacity
        setArray((array_t *)realloc(array(), array_t: :byteSize(newCount)));
        array()->count = newCount;
      	// Move the list of methods of the original class to the end, leaving the addedCount space
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
      	// Memory replication, which copies the list of methods added to the Category into the free addedCount memory space
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
      	// When a class and a Category have methods of the same name, why are the methods in the Category executed
    }
    else if(! list && addedCount ==1) {
        // 0 lists -> 1 list
        list = addedLists[0];
    } 
    else {
        // 1 list -> many lists
        List* oldList = list;
        uint32_t oldCount = oldList ? 1 : 0;
        uint32_t newCount = oldCount + addedCount;
        setArray((array_t *)malloc(array_t: :byteSize(newCount)));
        array()->count = newCount;
        if (oldList) array()->lists[addedCount] = oldList;
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0])); }}Copy the code
  1. Categories are loaded at Runtime, not at compile time.
  2. Classification reimplements the methods of the original class, but overwrites the original methods, making them unusable (there is no actual replacement, but the Category methods are 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 normally call a Category method that “overwrites” the methods of the original class with the same name. This is because the runtime looks for methods in the order of the list of methods, and stops when it finds a method with the same name, not knowing that there may be other methods with the same name behind them.
  3. When the classification, the original class, the original class with the same method in the parent class, the priority of the method call: classification (when there are multiple classification, finally to participate in the classification of the compilation preferred) – > the original class – > the parent class, namely to call classification methods in the first, not the method of classification to the original class, find the original class not to go looking for in the parent class.

The load method

If a class has multiple classes and the +load() method is overridden in all of them, which +load() method is executed?

If there are multiple categories, the methods in the last Category to be compiled will be called in the order in which they were compiled. In other words, the +load() method should only be called once.

But when we actually try it, we find that the +load() method of the class and each class is called. Why?

The load_images method is found in the _objc_init method:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods(a); }void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked(a);// Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush(a);do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads(a); }// 2. Call category +loads ONCE
        more_categories = call_category_loads(a);// 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if(! cls)continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
      	// The +load method is called directly from the class, instead of the objc_msgSend function
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

struct loadable_class {
    Class cls;  // may be nil
    IMP method; // Method implementation
};
Copy the code

So what is the order in which the +load() methods are called if there are superclasses, subclasses, and subclasses?

We can see from the above source code, first call the class +load() method, then call the class +load() method.

The +load() method of the class is called directly from loadable_classes. Moving forward a bit, there is a prepare_load_methods method in the load_images method.

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertWriting(a);classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if(! cls)continue;  // category for ignored weak-linked class
        realizeClass(cls);
        assert(cls->ISA() - >isRealized());
        add_category_to_loadable_list(cat); }}static void schedule_class_load(Class cls)
{
    if(! cls)return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked(a); method = cls->getLoadMethod(a);if(! method)return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked(a); method = _category_getLoadMethod(cat);// Don't bother if cat has no +load method
    if(! method)return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}
Copy the code
  • Prepare_load_methods, extract the classes from the compiled class list, and execute the schedule_class_load method
    • schedule_class_load
      • Schedule_class_load (CLS ->superclass), calls itself recursively to add the +load method of its parent class
      • Add_class_to_loadable_list (CLS) finds the loadable_classes we are looking for
    • add_category_to_loadable_list(cat)

From this, we can conclude that the load method is called in the following order:

  • Call the class’s +load method first
    • Call in compile order (compile first call first)
    • The child’s +load method is called before the parent’s +load method is called
  • Then call the +load method of the classification
    • Call in compile order (compile first call first)

The initialize method

associations

We can add attributes to a class by Category, but this is different from the attributes in the class:

  • For attributes added to a class, OC automatically adds member variables and generates declarations and implementations of getter and setter methods
  • Attributes added to a Category automatically generate getters, setters and declarations, but do not implement them or add member variables

By default, member variables cannot be added to a category because of the underlying structure of the category, but they can be done indirectly by associating objects.

Associated objects provide the following apis:

// Add the associated object
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)

// Get the associated object
id objc_getAssociatedObject(id object, const void * key)

// Remove all associated objects
void objc_removeAssociatedObjects(id object)
Copy the code

Common uses of Key:

objc_AssociationPolicy

objc_AssociationPolicy Corresponding modifier
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong, atomic
OBJC_ASSOCIATION_COPY copy, atomic

Principles of associated objects

The core objects that implement associative object technology are

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjcAssociation

The runtime file objc-references. Mm 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 associated objects are managed by AssociationsManager, which is defined as follows:

class AssociationsManager {
    static OSSpinLock _lock;
    static AssociationsHashMap *_map;               // associative references: object pointer -> PtrPtrHashMap.
public:
    AssociationsManager()   { OSSpinLockLock(&_lock); }
    ~AssociationsManager()  { OSSpinLockUnlock(&_lock); }
    
    AssociationsHashMap &associations(a) {
        if (_map == NULL)
            _map = new AssociationsHashMap(a);return*_map; }};Copy the code

AssociationsManager is a static AssociationsHashMap that stores all 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);
        boolassoc = ! 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

Well, the Runtime’s objc_destructInstance function determines whether the object has any associated objects, and if so, calls _object_remove_assocations to clean up associated objects.

The interview questions

1. Realization principle of Category

  • After Category compilation, the underlying structure is struct category_t, which stores classified object methods, class methods, attributes, and protocol information
  • At runtime, the Runtime merges the Category data into the class information (class object, metaclass object).

2. What is the difference between Category and Extension?

  • Class Extension is defined at compile time as part of a Class, formed at compile time along with @Interface in the header file and @Implement in the implementation file as a complete Class, which is born and dies with the Class. Extensions are generally used to hide private information about classes. You must have the source code of a class to add an extension to a class, so you cannot add an extension to a system class such as NSString.
  • Categories are determined at run time, and data is merged into class information at run time.

From the difference between category and Extension, we can infer the obvious fact that extension can add instance variables, while category cannot. (Because at runtime, the memory layout of the object is already determined, adding instance variables would break the internal layout of the class. This is disastrous for compiled languages).

3. Is there a load method in Category? When is the load method called? Can the load method inherit?

  • With the load method
  • The load method is called when the Runtime loads a class or class
  • The load method can be inherited, but usually the system does not automatically call the load method

4. What is the difference between load and initialize methods? The order in which they are called in a Category, and how they call each other when inheritance occurs?

The difference between load and Initialize methods

  • Call timing: The load method is called when Runtim loads a class or class. The load method for each class or class is called only once during application execution. The + Initialize method is called the first time the class receives a message
  • Call method: Load method is called by directly finding the address of the load function through the Runtime. +initialize is called via objc_msgSend
  • After the class is implemented: if the load method is implemented in the class, then both the class and the class load method will be called; If the class implements +initialize, override the +initialize call of the class itself

What is the call order of load and initialize?

  • load

    • Call the load class first
      • Classes compiled first call load first
      • The load of the subclass is called before the load of the subclass is called
    • Call the load of the class
      • Load is called first for classes compiled first
  • initialize

    • Initialize the parent class first
    • Initialize the subclass (it may end up calling the initialize method of the parent class)
    • If the class implements +initialize, override the +initialize call of the class itself

5. Can I add member variables to a Category? If so, how do YOU add member variables to a Category?

You cannot add member variables to a Category directly, but you can use associated objects to make it look like a Category has member variables indirectly