Classification of the Category

Function of classification

In OC, we can use classes to add methods, attributes to classes. You can also override the existing methods of a class and add new implementations yourself. The reason will be explained later at classification loading time.)

Structure of classification

const char *name; classref_t cls; WrappedPtr<method_list_t, PtrauthStrip> instanceMethods; WrappedPtr<method_list_t, PtrauthStrip> 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); protocol_list_t *protocolsForMeta(bool isMeta) { if (isMeta) return nullptr; else return protocols; }};Copy the code

As you can see from the source, the classification is structure-type data named category_T.

A member of the category_t

  • InstanceMethods: instance method
  • ClassMethods: class method
  • Separate protocols: agreement
  • InstanceProperties: instanceProperties
  • _classProperties: class attribute

The loading order of the classification

Analyzing a series of methods of iOS App startup, we can see the loading rules of the classification. Here’s a list of ways to do it

_objc_init ==> map_images ==> map_images_nolock ==> _read_images ==> remethodizeClass ==> attachCategories

_read_images

// Discover classes. Fix up unresolved future classes. Mark bundle classes. for (EACH_HEADER) { if (! mustReadClasses(hi)) { // Image is sufficiently optimized that we need not call readClass() continue; } bool headerIsBundle = hi->isBundle(); bool headerIsPreoptimized = hi->isPreoptimized(); classref_t *classlist = _getObjc2ClassList(hi, &count); for (i = 0; i < count; i++) { Class cls = (Class)classlist[i]; Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized); if (newCls ! = cls && newCls) { // Class was moved but not deleted. Currently this occurs // only when the new class resolved a future class. // Non-lazily realize the class below. resolvedFutureClasses = (Class *) realloc(resolvedFutureClasses, (resolvedFutureClassCount+1) * sizeof(Class)); resolvedFutureClasses[resolvedFutureClassCount++] = newCls; } } } // Discover protocols. Fix up protocol refs. for (EACH_HEADER) { extern objc_class OBJC_CLASS_$_Protocol; Class cls = (Class)&OBJC_CLASS_$_Protocol; assert(cls); NXMapTable *protocol_map = protocols(); bool isPreoptimized = hi->isPreoptimized(); bool isBundle = hi->isBundle(); protocol_t **protolist = _getObjc2ProtocolList(hi, &count); for (i = 0; i < count; i++) { readProtocol(protolist[i], cls, protocol_map, isPreoptimized, isBundle); } } // Discover categories. Only do this after the initial category // attachment has been done. For categories present at startup, // discovery is deferred until the first load_images call after // the call to _dyld_objc_notify_register completes. rdar://problem/53119145 if (didInitialAttachCategories) { for (EACH_HEADER) { load_categories_nolock(hi); } } // Realize non-lazy classes (for +load methods and static instances) for (EACH_HEADER) { classref_t const *classlist  = hi->nlclslist(&count); for (i = 0; i < count; i++) { Class cls = remapClass(classlist[i]); if (! cls) continue; addClassTableEntry(cls); if (cls->isSwiftStable()) { if (cls->swiftMetadataInitializer()) { _objc_fatal("Swift class %s with a metadata initializer " "is not allowed to be non-lazy", cls->nameForLogging()); } // fixme also disallow relocatable classes // We can't disallow all Swift classes because of // classes like Swift.__EmptyArrayStorage } realizeClassWithoutSwift(cls, nil); }}Copy the code

From the _read_images method, I’ve extracted some of the code. From this section of code, we can see the loading order of the classes.

  • Discover classes. Fix up unresolved future classes. Mark bundle classes.

  • Discover protocols. Fix up protocol refs.

  • Discover categories.===> load_categories_nolock but it’s obvious. There is a didInitialAttachCategories judgment value. As you can see from the source code, this value starts as false, so the classification is not loaded at this time.

  • As you go down, there’s a situation. Is non – lazy classes will call realizeClassWithoutSwift method, method of internal calls methodizeClass used internally objc: : unattachedCategories. AttachToClass Void load_images(const char *path __unused, const struct mach_header *mh) {if (! didInitialAttachCategories && didCallDyldNotifyRegister) { didInitialAttachCategories = true; loadAllCategories(); }

    // 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 { mutex_locker_t lock2(runtimeLock); prepare_load_methods((const headerType *)mh); } // Call +load methods (without runtimeLock - re-entrant) call_load_methods();Copy the code

    }

    static void loadAllCategories() { mutex_locker_t lock(runtimeLock);

    for (auto *hi = FirstHeader; hi ! = NULL; hi = hi->getNext()) { load_categories_nolock(hi); }Copy the code

    }

load_categories_nolock

static void load_categories_nolock(header_info *hi) { bool hasClassProperties = hi->info()->hasCategoryClassProperties(); size_t count; auto processCatlist = [&](category_t * const *catlist) { for (unsigned i = 0; i < count; i++) { category_t *cat = catlist[i]; Class cls = remapClass(cat->cls); locstamped_category_t lc{cat, hi}; // 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()) { attachCategories(cls, &lc, 1, ATTACH_EXISTING); } else { objc::unattachedCategories.addForClass(lc, cls); } } if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) { if (cls->ISA()->isRealized()) { attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS); } else { objc::unattachedCategories.addForClass(lc, cls->ISA()); }}}}; processCatlist(_getObjc2CategoryList(hi, &count)); processCatlist(_getObjc2CategoryList2(hi, &count)); }Copy the code

Part of the code was extracted from the load_categories_NOLock method and attachCategories was called internally.

attachCategories

// 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); auto rwe = cls->data()->extAllocIfNeeded(); for (uint32_t i = 0; i < cats_count; i++) { auto& entry = cats_list[i]; method_list_t *mlist = entry.cat->methodsForMeta(isMeta); if (mlist) { if (mcount == ATTACH_BUFSIZ) { prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__); rwe->methods.attachLists(mlists, mcount); mcount = 0; } mlists[ATTACH_BUFSIZ - ++mcount] = mlist; fromBundle |= entry.hi->isBundle(); } property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi); if (proplist) { if (propcount == ATTACH_BUFSIZ) { rwe->properties.attachLists(proplists, propcount); propcount = 0; } proplists[ATTACH_BUFSIZ - ++propcount] = proplist; } protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta); if (protolist) { if (protocount == ATTACH_BUFSIZ) { rwe->protocols.attachLists(protolists, protocount); protocount = 0; } protolists[ATTACH_BUFSIZ - ++protocount] = protolist; } } if (mcount > 0) { prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle, __func__); rwe->methods.attachLists(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->cache.isConstantOptimizedCache(); }); } } rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount); rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount); }Copy the code
  • Method_list_t *mlists(array of methods)
  • Property_list_t *proplists(Array of properties)
  • Protocol_list_t * Protolists (Array of protocols)

You can see that in the attachCategories method. It loads methods, properties, protocols. You can apply to the load_categories_NOLock method. Instance methods are loaded first, then class methods. In other words, the class object is processed first, and then the metaclass object is processed.

The list of loaded categories is iterated through in attachCategories. Attach each classification method, attribute, and protocol through the attachLists method. To assemble three numbers into a list of methods, attributes, and protocols corresponding to a class object or metaclass object.

See the classification loading time from attachToClass

From the source can be seen. When you load the categories. Will be called objc: : unattachedCategories attachToClass. I added my own Test class for comparison. Add your own code to attachToClass and add a breakpoint. View the call stack

  • 1. The +load method is implemented for both the Test class and the classification

  • 2. The Test class implements the +load method. The Test class does not implement the +load method

  • The Test class does not implement the +load method, and the Test class implements the +load method

  • The Test class does not implement the +load method, and the Test class does not implement the +load method

Case 1,2 results:

Case 3 Result:

Case 4 Result:

As you can see, whenever a class implements a +load method. The class forces categorical loading at read_images.

The class does not implement +load, and if it does, it will load the classes in the load_images method

And neither class nor classification is implemented. MsgSend ===> lookUpImpOrForward is used to load the class when the method is called

attachLists

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; array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount)); newArray->count = newCount; array()->count = newCount; for (int i = oldCount - 1; i >= 0; i--) newArray->lists[i + addedCount] = array()->lists[i]; for (unsigned i = 0; i < addedCount; i++) newArray->lists[i] = addedLists[i]; free(array()); setArray(newArray); validate(); } else if (! list && addedCount == 1) { // 0 lists -> 1 list list = addedLists[0]; validate(); } else { // 1 list -> many lists Ptr<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; for (unsigned i = 0; i < addedCount; i++) array()->lists[i] = addedLists[i]; validate(); }}Copy the code

As you can see from attachLists. When you expand the method array, you move the class’s old methods back, and then use the for loop to fill the classified methods into the empty space. So, if the class has a method with the same name as the class, it does not override the original method. Only the classification method is at the top of the method list, so msgSend procedure. Will be hit first.

What is the loading order when multiple classes have methods of the same name?

@interface Test : NSObject
@end

@implementation Test
@end

@interface Test (Test1)
+ (void)test;
@end

@implementation Test (Test1)
+ (void)test {
    NSLog(@"%s",__func__);
}
@end

@interface Test (Test2)
+ (void)test;
@end

@implementation Test (Test2)
+ (void)test {
    NSLog(@"%s",__func__);
}
@end
Copy the code

Build Phases after switching Test1 and Test2. The printout is painless.

TestExtension[60935:1095749] +[Test(Test1) Test] // TestExtension[60935:1095749] +[Test(Test1) Test]Copy the code

Category Add attribute

As you can see above, although category_T has a property list, there is no member list. So when you add attributes to a class, there is no member list. So it’s not like when a class declares a property, there’s this at sign synthesize _xxxx step by default. Generate getters,setter methods, and _ member names member scalars.

So when we use categorically declared properties directly, either as values or assignments. Both report the corresponding unrecognized selector sent to instance error.

So, if we want to add attributes to a class by classification, how do we do that?

We can add attributes to classes by classifying them using OC’s associated objects.

associations

In Runtime, associated objects allow us to dynamically associate an object with a block of addresses. The space is too long to do the analysis of principle here. I’m just going to write briefly how do I do associative objects

//Test+Test1.h #import "Test.h" NS_ASSUME_NONNULL_BEGIN @interface Test (Test1) @property(nonatomic, copy)NSString *testName; @end NS_ASSUME_NONNULL_END //Test+Test1.m #import "Test+Test1.h" #import <objc/runtime.h> @implementation Test (Test1) -  (void)setTestName:(NSString *)testName { objc_setAssociatedObject(self, "testName", testName, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (NSString *)testName { return objc_getAssociatedObject(self, "testName"); } @endCopy the code

This is an example of adding attributes to a class using associated objects to complete the classification.