preface

Category is frequently used in iOS development, especially when extending system classes. We can directly add methods to system classes without inheriting system classes, which maximized the dynamic language features of Objective-C.

This article is long, but the content is complete, it is suggested to follow the content of the article to explore again, after all, practice is the real knowledge.

The role of Catergory

  1. Spread the implementation of a class across multiple different files or multiple different frameworks. As follows: Different functional modules are processed by different categories

  2. You can add extension methods to a class without modifying the original class. If we need to add methods to the system’s own classes.

  3. It overrides methods that have the same name on the parent class, methods that have the same name on multiple categories, and it executes the methods in the last compiled Category in the order they were compiled.

  4. Can I have an Extension?

    • A Category is a run-time decision, while Extension is a compile-time decision that is part of the class.
    • Category cannot add instance variables (only via Runtime), Extension can add instance variables. (Because the memory structure of an object is already defined at runtime, adding instance variables would break the internal structure of the class.)
    • You can use both Category and Extension@propertyAttributes are added, but Category attributes do not generate member variables andgetter.setterMethod implementation, that is, cannot pass_varCall, but it can be passed manuallyobjc_get/setAssociatedObjectManual implementation.

Category compile time exploration

So that’s a lot of stuff, and now I’m going to start exploring the validity of that explanation, and let’s see what happens at compile time.

  • Create a new Test project, create an entity class, and a Test class, declare a Test method, and CMD +B compile

  • Open the command-line tool, CD to the current directory, run the clang-rewrite-objc Person+ test. m command, then go to the current project folder, locate the compiled Person+ test. CPP file and open it

  • The file is very large, and since we know that the category structure is _categoy_t, the global search finds its structure

struct _category_t {
	const char *name;
	struct _class_t *cls; / / class
	const struct _method_list_t *instance_methods;// List of instance methods
	const struct _method_list_t *class_methods;// List of class methods
	const struct _protocol_list_t *protocols;// Protocol list
	const struct _prop_list_t *properties;// Attribute list
};
Copy the code
  • Continue to search_category_tAnd found the categories of our tests
static struct _category_t _OBJC_The $_CATEGORY_Person_The $_Test __attribute__ ((used.section(" __DATA, __objc_const"))) = 
{
	"Person".0.// &OBJC_CLASS_$_Person,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
	0.0.0};Copy the code
  • Here,section ("__DATA,__objc_const")Is a segment identifier that will be stored in ourdyldloadingMachoThe one in the executablesectionThe dyld and Macho files will be stored at compile timeMachoIn the file)
  • Compare that to the top_category_tThe class name here isPersonOne is stored in the instance method listTestmethods
  • Search for the instance method and see it in the above lines of code
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"Test"."v16@0:8", (void *)_I_Person_Test_Test}}
};
Copy the code
  • This holds the size of the method, and the second value is the method type, found below_objc_methodAnd you can see the structure side by side
  • The third parameterimpContains theSelMethod number, method signature, and real function address
struct _objc_method {
	struct objc_selector * _cmd;
	const char *method_type;
	void  *_imp;
};
Copy the code
  • We learned that all of the above is that the compiler is mainly putcategoryCompile to structure, and then fill the corresponding value into the structure, save inMachoExecutable file
  • Continue to search_category_tAnd found the following code
static struct _category_t *L_OBJC_LABEL_CATEGORY_$[1] __attribute__((used.section(" __DATA, __objc_catlist.regular.no_dead_strip"))) = {
	&_OBJC_$_CATEGORY_Person_$_Test,
};
Copy the code
  • So this is actually taking all of our APPcategoryMethods are saved to__objc_catlistIn, that is, loading toMachoThe corresponding one in the filesectionparagraph

Compile time summary

At compile time, the category is compiled into the corresponding structure and stored in the Macho file section, and all classification methods are stored in the Macho file __objc_catlist section

Category runtime exploration

Knowing that the compiler mostly does save operations, the runtime is no doubt a load operation that loads everything that was just saved at compile time.

Dyld load

So let’s see how categories are loaded, right

  1. Dyld is Apple’s dynamic loader for loading images (image refers to a Mach-O binary file, not an image)

  2. When the program starts, the system kernel will load dyLD first, and dyLD will load various libraries that our APP depends on into memory, including libobJC library (OC and Runtime), which is done before the APP’s main function executes

  3. _objc_init is an entry function for object-C runtime. It reads the Segment section corresponding to object-C runtime and initializes runtime data structures.

To verify that _objc_init is an entry function, open the test project, add a symbolic breakpoint _objc_init, and run the project

_objc_init
dyld_start

Explore _objc_init

Source code project OBJC4 network disk link password: TUW8

  • Here you need to download the source code, openobjc4, direct search_objc_initFind its function implementation
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
Copy the code
  • There are all of theminitDelta function, the point here is_dyld_objc_notify_register, registered three callback functions
  • &map_imagesLoad the image into memory
  • load_imagesDyld initializes the loading image method
  • unmap_imagesRemove the memory

We’re going to explore how categories get loaded into memory, so what exactly does &map_images do, click on this method, okay

void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}
Copy the code
  • Let’s go ahead and find the key function_read_images
void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    // Other irrelevant code has been omitted
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

    firstTime = NO;
}
Copy the code
  • _read_imagesThe content of the function is more, find andcategoryRelated code, this code is longer
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
// The rest of the code is omitted
       for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        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  
                ||  (hasClassProperties && 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); }}}}Copy the code
  • First of all,_getObjc2CategoryListThe function reads all of themcategoryMethod, click on this method and you can see that it’s actually reading from our compile time_objc_catlistthesectionparagraph
GETSECT(_getObjc2CategoryList,        category_t *,    "__objc_catlist");
Copy the code
  • And then go through all the classification methods and process,if (cat->instanceMethods)You can see that the current is determined herecategoryIs a class method or an instance method, and does different processing
  • addUnattachedCategoryForClass(cat, cls, hi);Here is thecategoryWith theclassThe original class association mapping, you can click into the method to see the content
  • See there’s another one hereremethodizeClass(cls);The name is like resetting the function in the class. Click on it to see what the function is
static void remethodizeClass(Class cls)
{
    // Redundant code is omitted
    category_list *cats;
        // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats); }}Copy the code
  • Keep clicking on itattachCategoriesThe concrete realization of associative classification function
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    // omit extraneous code
    method_list_t **mlists = (method_list_t* *)malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t* *)malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t* *)malloc(cats->count * sizeof(*protolists));
        
    // omit extraneous code
           prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
}
Copy the code
  • This initializes the method list, protocol list, and collection listmlists
  • You can see that there are two important approaches hereprepareMethodListsandattachLists
  • Click inside firstprepareMethodLists
// omit other extraneous code
for (int i = 0; i < addedCount; i++) {
        // Retrieve the list of methods to add to the original class
        method_list_t *mlist = addedLists[i];
        assert(mlist);

        // Fixup selectors if necessary
        if(! mlist->isFixedUp()) { fixupMethodList(mlist, methodsFromBundle,true/*sort*/); }}Copy the code
  • Once you get the list of methods out, you call fixupMethodList, so click on it

  • So what we’re doing here is we’re going to register all the methods in the method list with the original class

  • In short, prepareMethodLists do the preparatory work before adding a method list

  • Go back outside, click on attachLists and see how the original class method is associated, okay

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;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            Void *memmove(void *__dst, const void *__src, size_t __len); * /
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
Copy the code
  • You can see that this is done by getting the classification method list to add and its size, and then copying it into the original method list
  • So this is finally going throughmemmoveandmemcpyThe function does the copy, so what exactly do these two functions do?

Memmove moves the list of methods in the original class back by the size of the list of methods to be added

Memcopy copies the list of methods to be added to the empty space in the list of methods of the original class

  • At this point, it is clear why the classified method overrides the method of the original class. It is just called before the method of the original class. It does not actually override the method. It is also understood why multiple methods with the same name are executed in the last compiled method of the class, and also in the order in which they are called.
  • This is also related to the method call flow, binary lookup, so the previous method is called first

Explore the Category association object objc_get/setAssociatedObject

Objc_get /setAssociatedObject is known to simulate adding attributes to a category, but how does that work?

  • Continue to open the source code, searchobjc_setAssociatedObjectFind its method content
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
Copy the code
  • Continue to find _object_set_associative_reference, where there is more code, line by line analysis

  • AcquireValue is used for memory management

  • So here’s an AssociationsManager, and you see that it has an AssociationsHashMap inside it, and access is locked, and it’s thread-safe

  • disguised_ptr_t disguised_object = DISGUISE(object); Here we use the inverted value of the object as the key, and the value in the iterator is the ObjectAssociationMap

  • If you look at ObjectAssociationMap, its key is the custom key that the user passed in, and its value is object Society

  • The last important method, setHasAssociatedObjects, associates the property with the class and sets the isa pointer to isa.has_assoc for release

  • Likewise, objc_getAssociatedObject gets its value from here

When was the association property removed?

We know above that the attribute is associated with the class’s ISA, so it should be removed when the object is destroyed. Similarly, a search for dealloc in the current objc source code found its implementation

- (void)dealloc {
    _objc_rootDealloc(self);
}
Copy the code
  • Continue to follow
void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}
Copy the code
  • We got a hitrootDealloc
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if(fastpath(isa.nonpointer && ! isa.weakly_referenced && ! isa.has_assoc && ! isa.has_cxx_dtor && ! isa.has_sidetable_rc)) { assert(! sidetable_present());free(this);
    } 
    else {
        object_dispose((id)this); }}Copy the code
  • Here we did some isa marking judgment first, and did not find what we want
  • Continue to followobject_dispose
id 
object_dispose(id obj)
{
    if(! obj)return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
Copy the code
  • I kept following it, and finallyobjc_destructInstanceAttribute destruction was found in
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}
Copy the code
  • The isa.has_assoc identifier is used to determine whether the current object has an associated attribute. If so, _object_remove_assocations is called to remove the associated attribute

  • Moving on to _object_remove_assocations, this is similar to the code we used to set the association properties, except that it is removed and then deleted by delete refs

conclusion

The above is the whole process of exploring the underlying principle of Category, which also makes the function of Category at the beginning of the article verified. The whole process is boring and tedious, but there is still a great harvest after the exploration. This article is a long one, and I hope you’ll try it out for yourself, not just to know what it is, but why it is.