美文网首页
Category原理

Category原理

作者: Rathen | 来源:发表于2018-06-27 15:05 被阅读86次

    在项目开发中,都会遇到扩展已有类的情况,在iOS中,普遍使用继承,但是在Objective-C 2.0中,提供了category这个语言特性,可以动态的为已有的类添加新功能。下面我面就探究一下它的原理

    Category简介

    Category是Objective-C 2.0之后添加的语言特性,category的主要作用是为已经存在的类添加方法。除此之外,apple还推荐了category的另外两个使用场景1

    • 可以把类的实现分开在几个不同的文件里面。这样做有几个显而易见的好处,a)可以减少单个文件的体积 b)可以把不同的功能组织到不同的category里 c)可以由多个开发者共同完成一个类 d)可以按需加载想要的category 等等。
    • 声明私有方法

    不过除了apple推荐的使用场景,广大开发者脑洞大开,还衍生出了category的其他几个使用场景:

    • 模拟多继承
    • 把framework的私有方法公开

    Category和Extension比较

    Extension看起来很像一个匿名的Category,但是Extension和有名字的Category几乎完全是两个东西,Extension编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及和实现文件里的@implement一起形成一个完整的类,它随着类的产生而产生,亦随之一起消亡,Extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加Extension,所以你无法为系统的类添加Extension(详见2
    但是Category实在运行期决议的。
    就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

    Category实现

    struct _category_t {
        const char *name;
        struct _class_t *cls;
        const struct _method_list_t *instance_methods;
        const struct _method_list_t *class_methods;
        const struct _protocol_list_t *protocols;
        const struct _prop_list_t *properties;
    };
    

    创建一个类的分类,然后用clang编译器编译源文件,得到一个.cpp文件,就可以在里面看到。


    image.png

    在iOS中,所有的OC类和对象,在runtime层都是用struct表示的,Category也是一样,它使用_category_t。

    • const char *name:类的名字,可以找到是哪个类的分类
    • struct _class_t *cls:类
    • const struct _method_list_t *instance_methods:实例方法列表
    • const struct _method_list_t *class_methods:类方法列表
    • const struct _protocol_list_t *protocols:协议方法列表
    • const struct _prop_list_t *properties:属性方法列表
      从_category_t结构体中可以给分类添加实例方法、类方法、协议和属性,但是不能添加实例变量。


      image.png
      image.png

    首先先定义一个Person类和一个Person+Run的分类,使用clang -rewrite-objc Person+Run.m之后,我们得到一个大约3.5M的.cpp文件,在文件的最后

    struct _category_t {
        const char *name;
        struct _class_t *cls;
        const struct _method_list_t *instance_methods;
        const struct _method_list_t *class_methods;
        const struct _protocol_list_t *protocols;
        const struct _prop_list_t *properties;
    };
    extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache;
    #pragma warning(disable:4273)
    
    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_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Person_Run_run}}
    };
    
    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_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"run", "v16@0:8", (void *)_C_Person_Run_run}}
    };
    
    static const char *_OBJC_PROTOCOL_METHOD_TYPES_NSCopying [] __attribute__ ((used, section ("__DATA,__objc_const"))) = 
    {
        "@24@0:8^{_NSZone=}16"
    };
    
    static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[1];
    } _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}}
    };
    
    struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
        0,
        "NSCopying",
        0,
        (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
        0,
        0,
        0,
        0,
        sizeof(_protocol_t),
        0,
        (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
    };
    struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCopying = &_OBJC_PROTOCOL_NSCopying;
    
    static struct /*_protocol_list_t*/ {
        long protocol_count;  // Note, this is 32/64 bit
        struct _protocol_t *super_protocols[1];
    } _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        1,
        &_OBJC_PROTOCOL_NSCopying
    };
    
    static struct /*_prop_list_t*/ {
        unsigned int entsize;  // sizeof(struct _prop_t)
        unsigned int count_of_properties;
        struct _prop_t prop_list[2];
    } _OBJC_$_PROP_LIST_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_prop_t),
        2,
        {{"name","T@\"NSString\",C,N"},
        {"height","Td,N"}}
    };
    
    extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_Person;
    
    static struct _category_t _OBJC_$_CATEGORY_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = 
    {
        "Person",
        0, // &OBJC_CLASS_$_Person,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Run,
        (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Run,
        (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Run,
    };
    static void OBJC_CATEGORY_SETUP_$_Person_$_Run(void ) {
        _OBJC_$_CATEGORY_Person_$_Run.cls = &OBJC_CLASS_$_Person;
    }
    #pragma section(".objc_inithooks$B", long, read, write)
    __declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
        (void *)&OBJC_CATEGORY_SETUP_$_Person_$_Run,
    };
    static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
        &_OBJC_$_CATEGORY_Person_$_Run,
    };
    static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
    
    
    • 首先编译器生成了实例方法列表OBJC$CATEGORY_INSTANCE_METHODS_Person$_Run 和类方法列表OBJC$CATEGORY_CLASS_METHODS_Person$_Run,而且这两个方法列表中都有自己实现的方法“run”
      _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying中添加了遵循的协议
      OBJC$PROP_LIST_Person$_Run 中添加了属性
    • 其次,编译器商城了category本身OBJC$CATEGORY_Person$_Run,并且初始化category本身
    • 最后,编译器在DATA段下的__objc_catlist,regular,no_dead_strip中保存了一个大小为1的category_t数组L_OBJC_LABEL_CATEGORY$,如果是多个就会增加数组的长度,用于运行期category的加载,到此为止,编译器的工作就已经完成。

    Category的加载

    iOS的运行时是依赖OC的runtime来实现的,OS X和iOS通过dyld动态加载。
    在runtime源码中的objc-os.mm文件中,找到OC的入口方法

    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);
    }
    

    category呗附加到类上面是在map_iamges的时候发生的,在new-ABI标准下,_objc_init里面调用的map_images最终会调用objc-runtime-new.mm里面的_read_images方法,而在_read_images方法的结尾,有以下的代码片段:

    // Discover categories. 
        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);
                    }
                }
            }
        }
    
    

    首先,我们拿到的catlist就是上节中讲到的编译器为我们准备的category_t数组,把category的实例方法、协议以及属性添加到类上,把category的类方法和协议添加到类的metaclass上。
    值得注意的是,在代码中有一小段注释 / || cat->classProperties /,看来苹果有过给类添加属性的计划啊。
    ok,我们接着往里看,category的各种列表是怎么最终添加到类上的,就拿实例方法列表来说吧:
    在上述的代码片段里,addUnattachedCategoryForClass只是把类和category做一个关联映射,而remethodizeClass才是真正去处理添加事宜的功臣。

    static void remethodizeClass(Class cls)
    {
        category_list *cats;
        bool isMeta;
    
        runtimeLock.assertWriting();
    
        isMeta = cls->isMetaClass();
    
        // 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)" : "");
            }
            
            attachCategories(cls, cats, true /*flush caches*/);        
            free(cats);
        }
    }
    

    而对于添加类的实例方法而言,又会去调用attachCategoryMethods这个方法,我们去看下attachCategories

    // oldest categories first.
    static void 
    attachCategories(Class cls, category_list *cats, bool flush_caches)
    {
        if (!cats) return;
        if (PrintReplacedMethods) printReplacements(cls, cats);
    
        bool isMeta = cls->isMetaClass();
    
        // fixme rearrange to remove these intermediate allocations
        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));
    
        // 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--) {
            auto& entry = cats->list[i];
    
            method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
            if (mlist) {
                mlists[mcount++] = mlist;
                fromBundle |= entry.hi->isBundle();
            }
    
            property_list_t *proplist = 
                entry.cat->propertiesForMeta(isMeta, entry.hi);
            if (proplist) {
                proplists[propcount++] = proplist;
            }
    
            protocol_list_t *protolist = entry.cat->protocols;
            if (protolist) {
                protolists[protocount++] = protolist;
            }
        }
    
        auto rw = cls->data();
    
        prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
        rw->methods.attachLists(mlists, mcount);
        free(mlists);
        if (flush_caches  &&  mcount > 0) flushCaches(cls);
    
        rw->properties.attachLists(proplists, propcount);
        free(proplists);
    
        rw->protocols.attachLists(protolists, protocount);
        free(protolists);
    }
    

    attachCategories做的工作相对比较简单,它只是把所有category的实例方法列表拼成了一个大的实例方法列表,然后转交给了prepareMethodLists方法

    static void 
    prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount, 
                       bool baseMethods, bool methodsFromBundle)
    {
        runtimeLock.assertWriting();
    
        if (addedCount == 0) return;
    
        // Don't scan redundantly
        bool scanForCustomRR = !cls->hasCustomRR();
        bool scanForCustomAWZ = !cls->hasCustomAWZ();
    
        // There exist RR/AWZ special cases for some class's base methods. 
        // But this code should never need to scan base methods for RR/AWZ: 
        // default RR/AWZ cannot be set before setInitialized().
        // Therefore we need not handle any special cases here.
        if (baseMethods) {
            assert(!scanForCustomRR  &&  !scanForCustomAWZ);
        }
    
        // Add method lists to array.
        // Reallocate un-fixed method lists.
        // The new methods are PREPENDED to the method list array.
    
        for (int i = 0; i < addedCount; i++) {
            method_list_t *mlist = addedLists[i];
            assert(mlist);
    
            // Fixup selectors if necessary
            if (!mlist->isFixedUp()) {
                fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
            }
    
            // Scan for method implementations tracked by the class's flags
            if (scanForCustomRR  &&  methodListImplementsRR(mlist)) {
                cls->setHasCustomRR();
                scanForCustomRR = false;
            }
            if (scanForCustomAWZ  &&  methodListImplementsAWZ(mlist)) {
                cls->setHasCustomAWZ();
                scanForCustomAWZ = false;
            }
        }
    }
    

    需要注意的有两点:
    1)、category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
    2)、category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休_,殊不知后面可能还有一样名字的方法。

    Category+load方法

    我们知道,在类和category中都可以有+load方法,那么有两个问题:
    1)、在类的+load方法调用的时候,我们可以调用category中声明的方法么?
    2)、这么些个+load方法,调用顺序是咋样的呢?
    首先先做一个实验,我们定义两个category并且在里面都定义并且实现了一个相同的方法run


    image.png
    image.png
    image.png

    我们在main函数中引用文件,并且创建对象执行方法


    image.png
    运行结果后
    image.png

    当我们注释掉Person+Test文件后


    image.png

    奇迹出现了,运行结果仍然是Person+Test文件中的run方法


    image.png

    我们在两个category中都实现+(void)load方法,并且在Xcode中点击Edit Scheme,添加如下两个环境变量OBJC_PRINT_LOAD_METHODS,OBJC_PRINT_REPLACE_METHODS(可以在执行load方法以及加载category的时候打印log信息,更多的环境变量选项可参见objc-private.h):


    image.png

    运行后得到的信息

    objc[10123]: LOAD: class 'NSObject' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_queue' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_source' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_mach' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_queue_runloop' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_semaphore' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_group' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_queue_serial' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_queue_concurrent' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_queue_main' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_queue_root' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_queue_network_event' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_queue_mgr' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_queue_specific_queue' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_queue_attr' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_mach_msg' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_io' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_operation' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_disk' scheduled for +load
    objc[10123]: LOAD: class 'OS_voucher' scheduled for +load
    objc[10123]: LOAD: class 'OS_dispatch_data_empty' scheduled for +load
    objc[10123]: LOAD: +[NSObject load]
    
    objc[10123]: LOAD: +[OS_dispatch_queue load]
    
    objc[10123]: LOAD: +[OS_dispatch_source load]
    
    objc[10123]: LOAD: +[OS_dispatch_mach load]
    
    objc[10123]: LOAD: +[OS_dispatch_queue_runloop load]
    
    objc[10123]: LOAD: +[OS_dispatch_semaphore load]
    
    objc[10123]: LOAD: +[OS_dispatch_group load]
    
    objc[10123]: LOAD: +[OS_dispatch_queue_serial load]
    
    objc[10123]: LOAD: +[OS_dispatch_queue_concurrent load]
    
    objc[10123]: LOAD: +[OS_dispatch_queue_main load]
    
    objc[10123]: LOAD: +[OS_dispatch_queue_root load]
    
    objc[10123]: LOAD: +[OS_dispatch_queue_network_event load]
    
    objc[10123]: LOAD: +[OS_dispatch_queue_mgr load]
    
    objc[10123]: LOAD: +[OS_dispatch_queue_specific_queue load]
    
    objc[10123]: LOAD: +[OS_dispatch_queue_attr load]
    
    objc[10123]: LOAD: +[OS_dispatch_mach_msg load]
    
    objc[10123]: LOAD: +[OS_dispatch_io load]
    
    objc[10123]: LOAD: +[OS_dispatch_operation load]
    
    objc[10123]: LOAD: +[OS_dispatch_disk load]
    
    objc[10123]: LOAD: +[OS_voucher load]
    
    objc[10123]: LOAD: +[OS_dispatch_data_empty load]
    
    objc[10123]: LOAD: class 'OS_os_log' scheduled for +load
    objc[10123]: LOAD: class 'OS_os_activity' scheduled for +load
    objc[10123]: LOAD: +[OS_os_log load]
    
    objc[10123]: LOAD: +[OS_os_activity load]
    
    objc[10123]: LOAD: class 'OS_xpc_connection' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_service' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_null' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_bool' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_int64' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_uint64' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_double' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_pointer' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_date' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_data' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_string' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_uuid' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_fd' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_shmem' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_mach_send' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_array' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_dictionary' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_error' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_endpoint' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_serializer' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_pipe' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_mach_recv' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_bundle' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_service_instance' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_activity' scheduled for +load
    objc[10123]: LOAD: class 'OS_xpc_file_transfer' scheduled for +load
    objc[10123]: LOAD: +[OS_xpc_connection load]
    
    objc[10123]: LOAD: +[OS_xpc_service load]
    
    objc[10123]: LOAD: +[OS_xpc_null load]
    
    objc[10123]: LOAD: +[OS_xpc_bool load]
    
    objc[10123]: LOAD: +[OS_xpc_int64 load]
    
    objc[10123]: LOAD: +[OS_xpc_uint64 load]
    
    objc[10123]: LOAD: +[OS_xpc_double load]
    
    objc[10123]: LOAD: +[OS_xpc_pointer load]
    
    objc[10123]: LOAD: +[OS_xpc_date load]
    
    objc[10123]: LOAD: +[OS_xpc_data load]
    
    objc[10123]: LOAD: +[OS_xpc_string load]
    
    objc[10123]: LOAD: +[OS_xpc_uuid load]
    
    objc[10123]: LOAD: +[OS_xpc_fd load]
    
    objc[10123]: LOAD: +[OS_xpc_shmem load]
    
    objc[10123]: LOAD: +[OS_xpc_mach_send load]
    
    objc[10123]: LOAD: +[OS_xpc_array load]
    
    objc[10123]: LOAD: +[OS_xpc_dictionary load]
    
    objc[10123]: LOAD: +[OS_xpc_error load]
    
    objc[10123]: LOAD: +[OS_xpc_endpoint load]
    
    objc[10123]: LOAD: +[OS_xpc_serializer load]
    
    objc[10123]: LOAD: +[OS_xpc_pipe load]
    
    objc[10123]: LOAD: +[OS_xpc_mach_recv load]
    
    objc[10123]: LOAD: +[OS_xpc_bundle load]
    
    objc[10123]: LOAD: +[OS_xpc_service_instance load]
    
    objc[10123]: LOAD: +[OS_xpc_activity load]
    
    objc[10123]: LOAD: +[OS_xpc_file_transfer load]
    
    objc[10123]: LOAD: class '__IncompleteProtocol' scheduled for +load
    objc[10123]: LOAD: class 'Protocol' scheduled for +load
    objc[10123]: LOAD: class '__NSUnrecognizedTaggedPointer' scheduled for +load
    objc[10123]: LOAD: +[__IncompleteProtocol load]
    
    objc[10123]: LOAD: +[Protocol load]
    
    objc[10123]: LOAD: +[__NSUnrecognizedTaggedPointer load]
    
    objc[10123]: LOAD: category 'NSObject(NSObject)' scheduled for +load
    objc[10123]: LOAD: +[NSObject(NSObject) load]
    
    objc[10123]: LOAD: class 'NSMergePolicy' scheduled for +load
    objc[10123]: LOAD: +[NSMergePolicy load]
    
    objc[10123]: LOAD: category 'NSObject(NSObject)' scheduled for +load
    objc[10123]: LOAD: +[NSObject(NSObject) load]
    
    objc[10123]: LOAD: class 'NSApplication' scheduled for +load
    objc[10123]: LOAD: class 'NSBinder' scheduled for +load
    objc[10123]: LOAD: class 'NSColorSpaceColor' scheduled for +load
    objc[10123]: LOAD: class 'NSNextStepFrame' scheduled for +load
    objc[10123]: LOAD: +[NSApplication load]
    
    objc[10123]: LOAD: +[NSBinder load]
    
    objc[10123]: LOAD: +[NSColorSpaceColor load]
    
    objc[10123]: LOAD: +[NSNextStepFrame load]
    
    objc[10123]: LOAD: class '_MediaServicesOSLog' scheduled for +load
    objc[10123]: LOAD: +[_MediaServicesOSLog load]
    
    objc[10123]: LOAD: class 'Person' scheduled for +load
    objc[10123]: LOAD: category 'Person(Run)' scheduled for +load
    objc[10123]: LOAD: category 'Person(Test)' scheduled for +load
    objc[10123]: LOAD: +[Person load]
    
    objc[10123]: LOAD: +[Person(Run) load]
    
    objc[10123]: LOAD: +[Person(Test) load]
    
    

    所以,对于上面两个问题,答案是很明显的:
    1)、可以调用,因为附加category到类的工作会先于+load方法的执行
    2)、+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。
    目前的编译顺序是这样的:


    image.png

    调整两个category的编译顺序之后


    image.png
    控制台的输出为
    image.png
    虽然对于+load的执行顺序是这样,但是对于“覆盖”掉的方法,则会先找到最后一个编译的category里的对应方法。
    怎么调用到原来类中被category覆盖掉的方法?
    对于这个问题,我们已经知道category其实并不是完全替换掉原来类的同名方法,只是category在方法列表的前面而已,所以我们只要顺着方法列表找到最后一个对应名字的方法,就可以调用原来类的方法。

    Category和关联对象

    我们知道在category里面是无法为category添加实例变量的。但是我们很多时候需要在category中添加和对象关联的值,这个时候可以求助关联对象来实现


    image.png
    image.png

    但是关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?
    我们去翻一下runtime的源码,在objc-references.mm文件中有个方法_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);
                    object->setHasAssociatedObjects();
                }
            } 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);
    }
    

    我们可以看到所有的关联对象都由AssociationsManager管理,而AssociationsManager定义如下:

    class AssociationsManager {
        // associative references: object pointer -> PtrPtrHashMap.
        static AssociationsHashMap *_map;
    public:
        AssociationsManager()   { AssociationsManagerLock.lock(); }
        ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
        
        AssociationsHashMap &associations() {
            if (_map == NULL)
                _map = new AssociationsHashMap();
            return *_map;
        }
    };
    

    AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。
    而在对象的销毁逻辑里面,见objc-runtime-new.mm:

    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;
    }
    

    嗯,runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。

    ps:由于本有能力有限,有关runtime源码解析的部分是查找资料得来的。

    相关文章

      网友评论

          本文标题:Category原理

          本文链接:https://www.haomeiwen.com/subject/ypkbyftx.html