美文网首页
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