introduction
WritingObjective-C
When code, if you want to add some methods to classes that cannot obtain the source code,Category
That 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_t
The 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_t
variables of structures,category_t
The 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_nolock
It 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 fromFirstHeader
Start, traverse allheader_info
and call it in turnload_categories_nolock
The 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_nolock
In the method, we will determine whether the class isstubClass
Whether 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 throughattachCategories
When 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 therw
In the middle, a new one was created calledrwe
The 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 attachedattachLists
The 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!