Runtime(3)--分类Category

分类Category

在平日编程中或阅读第三方代码时,category可以说是无处不在。category也可以说是OC作为一门动态语言的一大特色。category为我们动态扩展类的功能提供了可能,或者我们也可以把一个庞大的类进行功能分解,按照category进行组织。

一般分类Category都会和扩展Extension用来比较(oc语法中的扩展)
1.一般用扩展做什么:
1.1 声明私有属性。
1.2 声明私有方法
1.3 声明私有成员变量。
2.拓展的特点(与分类区别):
2.1 扩展是有编译时决定的,分类是由运行时决定的
2.2 扩展只以声明的形式存在,多数情况下寄生于宿主类的.m实现文件中
2.3 不能为系统类添加拓展

category的数据结构

category对应到runtime中的结构体是struct category_t

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *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);
};

category_t的定义很简单。从定义中看出,category 可以:添加实例方法(instanceMethods),类方法(classMethods),协议(protocols)和实例属性(instanceProperties)。不可以:不能够添加实例变量

注意:虽然category可以添加实例属性@property,但是它既不会生成大括号里下划线( _* )的成员变量,也不会生成相应的setter、getter方法。也就是添加了其实也没啥作用。在编译的时候调用是没有问题的,编译器不会报错;但是在运行时的时候就会崩溃。因为找不到getter/setter方法。想使用就需要自己去关联对象实现,关联对象也是不会自动生成下划线( _* )的成员变量的,这是因为在clas里的Ivarlist、IvarLayout大小在编译器都确定了,不能修改。

category的加载

知道了category的数据结构,我们来深入探究一下category是如何在runtime中实现的。

原理很简单:runtime会分别将category 结构体中的instanceMethods, protocolsinstanceProperties添加到target class的实例方法列表,协议列表,属性列表中,会将category结构体中的classMethods添加到target class所对应的元类的实例方法列表中。其本质就相当于runtime在运行时期,修改了target class的结构。

经过这一番修改,category中的方法,就变成了target class方法列表中的一部分,其调用方式也就一模一样啦~

现在,就来看一下具体是怎么实现的。

首先,在Mach-O文件中,category数据会被存放在__DATA段下的__objc_catlist section中。

当OC被dyld加载起来时,OC进入其入口点函数_objc_init

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

我们忽略一堆init方法,重点来看_dyld_objc_notify_register方法。该方法会向dyld注册监听Mach-O中OC相关section被加载入\载出内存的事件。

具体有三个事件:
_dyld_objc_notify_mapped(对应&map_images回调):当dyld已将images加载入内存时。
_dyld_objc_notify_init(对应load_images回调):当dyld初始化image后。OC调用类的+load方法,就是在这时进行的。
_dyld_objc_notify_unmapped(对应unmap_image回调):当dyld将images移除内存时。

category写入target class的方法列表,则是在_dyld_objc_notify_mapped,即将Mach-O相关sections都加载到内存之后所发生的。

我们可以看到其对应回调为map_images方法。

map_images 最终会调用_read_images 方法来读取OC相关sections,并以此来初始化OC内存环境。_read_images 的极简实现版如下,可以看到,rumtime是如何根据Mach-O各个section的信息来初始化其自身的:

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{

    static bool doneOnce;
    TimeLogger ts(PrintImageTimes);
    
    runtimeLock.assertWriting();
    
    if (!doneOnce) {
        doneOnce = YES;
        
        ts.log("IMAGE TIMES: first time tasks");
    }
    
    
    // Discover classes. Fix up unresolved future classes. Mark bundle classes.
    
    for (EACH_HEADER) {
        
        classref_t *classlist = _getObjc2ClassList(hi, &count);
        for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[i];
            Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
        }
    }
    
    ts.log("IMAGE TIMES: discover classes");
    
    // Fix up remapped classes
    // Class list and nonlazy class list remain unremapped.
    // Class refs and super refs are remapped for message dispatching.

    for (EACH_HEADER) {
        Class *classrefs = _getObjc2ClassRefs(hi, &count);
        for (i = 0; i < count; i++) {
            remapClassRef(&classrefs[i]);
        }
        // fixme why doesn't test future1 catch the absence of this?
        classrefs = _getObjc2SuperRefs(hi, &count);
        for (i = 0; i < count; i++) {
            remapClassRef(&classrefs[i]);
        }
    }
   
    ts.log("IMAGE TIMES: remap classes");
    
    
    for (EACH_HEADER) {
        if (hi->isPreoptimized()) continue;
        
        bool isBundle = hi->isBundle();
        SEL *sels = _getObjc2SelectorRefs(hi, &count);
        UnfixedSelectors += count;
        for (i = 0; i < count; i++) {
            const char *name = sel_cname(sels[i]);
            sels[i] = sel_registerNameNoLock(name, isBundle);
        }
    }
    
    ts.log("IMAGE TIMES: fix up selector references");
    
    
    // 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);
        }
    }
    
    ts.log("IMAGE TIMES: discover protocols");
    
    // Fix up @protocol references
    // Preoptimized images may have the right
    // answer already but we don't know for sure.
    for (EACH_HEADER) {
        protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count);
        for (i = 0; i < count; i++) {
            remapProtocolRef(&protolist[i]);
        }
    }
    
    ts.log("IMAGE TIMES: fix up @protocol references");
    
    // Realize non-lazy classes (for +load methods and static instances)
    for (EACH_HEADER) {
        classref_t *classlist =
        _getObjc2NonlazyClassList(hi, &count);
        for (i = 0; i < count; i++) {
            Class cls = remapClass(classlist[i]);
            if (!cls) continue;
            realizeClass(cls);
        }
    }
    
    ts.log("IMAGE TIMES: realize non-lazy classes");
    
    // Realize newly-resolved future classes, in case CF manipulates them
    if (resolvedFutureClasses) {
        for (i = 0; i < resolvedFutureClassCount; i++) {
            realizeClass(resolvedFutureClasses[i]);
            resolvedFutureClasses[i]->setInstancesRequireRawIsa(false/*inherited*/);
        }
        free(resolvedFutureClasses);
    }
    ts.log("IMAGE TIMES: realize future classes");
    
    // 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);
            
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols
                ||  cat->instanceProperties)
            {
                addUnattachedCategoryForClass(cat, cls, hi);
            }
            
            if (cat->classMethods  ||  cat->protocols
                ||  (hasClassProperties && cat->_classProperties))
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
            }
        }
    }
    
    ts.log("IMAGE TIMES: discover categories");
}

大致的逻辑是,runtime调用_getObjc2XXX格式的方法,依次来读取对应的section内容,并根据其结果初始化其自身结构。

_getObjc2XXX 方法有如下几种,可以看到他们都一一对应了Mach-O中相关的OC seciton

//      function name                 content type     section name
GETSECT(_getObjc2SelectorRefs,        SEL,             "__objc_selrefs"); 
GETSECT(_getObjc2MessageRefs,         message_ref_t,   "__objc_msgrefs"); 
GETSECT(_getObjc2ClassRefs,           Class,           "__objc_classrefs");
GETSECT(_getObjc2SuperRefs,           Class,           "__objc_superrefs");
GETSECT(_getObjc2ClassList,           classref_t,      "__objc_classlist");
GETSECT(_getObjc2NonlazyClassList,    classref_t,      "__objc_nlclslist");
GETSECT(_getObjc2CategoryList,        category_t *,    "__objc_catlist");
GETSECT(_getObjc2NonlazyCategoryList, category_t *,    "__objc_nlcatlist");
GETSECT(_getObjc2ProtocolList,        protocol_t *,    "__objc_protolist");
GETSECT(_getObjc2ProtocolRefs,        protocol_t *,    "__objc_protorefs");
GETSECT(getLibobjcInitializers,       Initializer,     "__objc_init_func");

可以看到,我们使用的类,协议和category,都是在_read_images 方法中读取出来的。
在读取cateogry的方法 _getObjc2CategoryList(hi, &count)中,读取的是Mach-O文件的 __objc_catlist 段。

我们重点关注和category相关的代码:

    // 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);
            
            bool classExists = NO;
            // 如果Category中有实例方法,协议,实例属性,会改写target class的结构
            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" : "");
                }
            }
            // 如果category中有类方法,协议,或类属性(目前OC版本不支持类属性), 会改写target 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);
                }
            }
        }
    }
    
    ts.log("IMAGE TIMES: discover categories");

discover categories的逻辑如下:

  1. 先调用_getObjc2CategoryList读取__objc_catlist seciton下所记录的所有category。并存放到category_t *数组中。
  2. 依次读取数组中的category_t * cat
  3. 对每一个cat,先调用remapClass(cat->cls),并返回一个objc_class *对象cls。这一步的目的在于找到到category对应的类对象cls
  4. 找到category对应的类对象cls后,就开始进行对cls的修改操作了。首先,如果category中有实例方法,协议,和实例属性之一的话,则直接对cls进行操作。如果category中包含了类方法,协议,类属性(不支持)之一的话,还要对cls所对应的元类(cls->ISA())进行操作。
  5. 不管是对cls还是cls的元类进行操作,都是调用的方法addUnattachedCategoryForClass。但这个方法并不是category实现的关键,其内部逻辑只是将class和其对应的category做了一个映射。这样,以classkey,就可以取到所其对应的所有的category
  6. 做好classcategory的映射后,会调用remethodizeClass方法来修改classmethod list结构,这才是runtime实现category的关键所在。

remethodizeClass

既然remethodizeClasscategory的实现核心,那么我们就单独一节,细看一下该方法的实现:


/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
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);
    }
}

该段代码首先通过unattachedCategoriesForClass 取出还未被附加到class上的category list,然后调用attachCategories将这些category附加到class上。

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, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // 首先分配method_list_t *, property_list_t *, protocol_list_t *的数组空间,数组大小等于category的个数
    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--) {  // 依次读取每一个category,将其methods,property,protocol添加到mlists,proplist,protolist中存储
        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;
        }
    }

    // 取出class的data()数据,其实是class_rw_t * 指针,其对应结构体实例存储了class的基本信息
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);  // 将category中的method 添加到class中
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls); // 如果需要,同时刷新class的method list cache


    rw->properties.attachLists(proplists, propcount); // 将category的property添加到class中
    free(proplists);

    rw->protocols.attachLists(protolists, protocount); // 将category的protocol添加到class中
    free(protolists);
}

到此为止,我们就完成了category的加载工作。可以看到,最终,cateogry被加入到了对应class的方法,协议以及属性列表中。

最后我们再看一下attachLists方法是如何将两个list合二为一的:

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;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            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;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

仔细看会发现,attachLists方法其实是使用的头插的方式将新的list插入原有list中的。即,新的list会插入到原始list的头部。

这也就说明了,为什么category中的方法,会‘覆盖class的原始方法。其实并没有真正的‘覆盖’,而是由于cateogry中的方法被排到了原始方法的前面,那么在消息查找流程中,会返回首先被查找到的cateogry方法的实现。

category和+load方法

  • 在类的+load方法中,可以调用分类方法吗?

要回答这个问题,其实要搞清load方法的调用时机和category附加到class上的先后顺序。

如果在load方法被调用前,category已经完成了附加到class上的流程,则对于上面的问题,答案是肯定的。

我们回到runtime的入口函数来看一下,

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

runtime在入口点分别向dyld注册了三个事件监听:mapped oc sectionsinit oc section 以及 unmapped oc sections

而这三个事件的顺序是: mapped oc sections -> init oc section -> unmapped oc sections

mapped oc sections 事件中,我们已经看过其源码,runtime会依次读取Mach-O文件中的oc sections,并根据这些信息来初始化runtime环境。这其中就包括cateogry的加载。

之后,当runtime环境都初始化完毕,在dyld的init oc section 事件中,runtime会调用每一个加载到内存中的类的+load方法。

这里我们注意到,+load方法的调用是在cateogry加载之后的。因此,在+load方法中,是可以调用category方法的。

调用已被category‘覆盖’的方法

前面我们已经知道,类中的方法并不是真正的被category‘覆盖’,而是被放到了类方法列表的后面,消息查找时找不到而已。我们当然也可以手动来找到并调用它,代码如下:

@interface Son : NSObject
- (void)sayHi;
@end

@implementation Son
- (void)sayHi {
    NSLog(@"Son say hi!");
}
@end

// son 的分类,覆写了sayHi方法
@interface Son (Good)
- (void)sayHi;
- (void)saySonHi;
@end

- (void)sayHi {
    NSLog(@"Son's category good say hi");
}

- (void)saySonHi {
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList([self class], &methodCount);
    
    SEL sel = @selector(sayHi);
    NSString *originalSelName = NSStringFromSelector(sel);
    IMP lastIMP = nil;
    for (NSInteger i = 0; i < methodCount; ++i) {
        Method method = methodList[i];
        NSString *selName = NSStringFromSelector(method_getName(method));
        if ([originalSelName isEqualToString:selName]) {
            lastIMP = method_getImplementation(method);
        }
    }
    
    if (lastIMP != nil) {
        typedef void(*fn)(id, SEL);
        fn f = (fn)lastIMP;
        f(self, sel);
    }
    free(methodList);
    
}

// 分别调用sayHi 和 saySonHi
Son *mySon1 = [Son new];
[mySon1 sayHi];
[mySon1 saySonHi];
category和关联对象

众所周知,category是不支持向类添加实例变量的。这在源码中也可以看出,cateogry仅支持实例方法、类方法、协议、和实例属性(注意,实例属性并不等于实例变量)。

但是,runtime也给我提供了一个折中的方式,虽然不能够向类添加实例变量,但是runtime为我们提供了方法,可以向类的实例对象添加关联对象。

所谓关联对象,就是为目标对象添加一个关联的对象,并能够通过key来查找到这个关联对象。说的形象一点,就像我们去跳舞,runtime可以给我们分配一个舞伴一样。

这种关联是对象和对象级别的,而不是类层次上的。当你为一个类实例添加一个关联对象后,如果你再创建另一个类实例,这个新建的实例是没有关联对象的。

我们可以通过重写set/get方法的形式,来自动为我们的实例添加关联对象。

ViewController+Cat.h:

#import "ViewController.h"

NS_ASSUME_NONNULL_BEGIN

@interface ViewController (Cat)

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END

ViewController+Cat.m:

#import "ViewController+Cat.h"
#import <objc/runtime.h>

@implementation ViewController (Cat)

- (NSString *)name {
    NSString *nameobject = objc_getAssociatedObject(self, "name");
    return nameobject;;
}

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY);
}

@end

代码很简单,我们重点关注一下其背后的实现。

objc_setAssociatedObject

我们要设置关联对象,需要调用objc_setAssociatedObject 方法将对象关联到目标对象上。我们需要传入4个参数:target objectassociated keyassociated value, objc_AssociationPolicy

objc_AssociationPolicy是一个枚举,可以取值为:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

分别和property的属性定义一一匹配。

当我们为对象设置关联对象的时候,所关联的对象到底存在了那里呢?我们看源码:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
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; // 这是一个单例,内部保存一个全局的static AssociationsHashMap *_map; 用于保存所有的关联对象。
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object); // 取反object 地址 作为accociative key
        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(); // 将object标记为 has AssociatedObjects
            }
        } else { // 如果传入的关联对象值为nil,则断开关联
            // 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); // 释放掉old关联对象。(如果多次设置同一个key的value,这里会释放之前的value)
}

大体流程为:

  • 1.根据关联的policy,调用id new_value = value ? acquireValue(value, policy) : nil;acquireValue 方法会根据poilcyretaincopy,对value做引用+1操作或copy操作,并返回对应的new_value。(如果传入的value为nil,则返回nil,不做任何操作)
    acquireValue实现代码是:
static id acquireValue(id value, uintptr_t policy) {
    switch (policy & 0xFF) {
    case OBJC_ASSOCIATION_SETTER_RETAIN:
        return objc_retain(value);
    case OBJC_ASSOCIATION_SETTER_COPY:
        return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
    }
    return value;
}
  • 2.获取到new_value后,根据是否有new_value的值,进入不同流程。如果 new_value 存在,则对象与目标对象关联。实质是存入到全局单例 AssociationsManager manager 的对象关联表中。 如果new_value不存在,则释放掉之前目标对象及关联 key所存储的关联对象。实质是在 AssociationsManager 中删除掉关联对象。
  • 3.最后,释放掉之前以同样key存储的关联对象。
    其中,起到关键作用的在于AssociationsManager manager, 它是一个全局单例,其成员变量为static AssociationsHashMap *_map,用于存储目标对象及其关联的对象。

仔细看这一段代码,会发现有个问题:当我们第一次为目标对象创建关联对象时,会在AssociationsManager managerObjectAssociationMap 中插入一个以disguised_object为key 的节点,用于存储该目标对象所关联的对象。

但是,上面代码中,仅有释放old_association关联对象的代码,而没有释放保存在AssociationsManager manager中节点的代码。那么,AssociationsManager manager 中的节点是什么时候被释放的呢?

在对象的销毁逻辑里,会调用objc_destructInstance,实现如下:

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); // 调用C++析构函数
        if (assoc) _object_remove_assocations(obj); // 移除所有的关联对象,并将其自身从AssociationsManager的map中移除
        obj->clearDeallocating(); // 清理ARC ivar
    }

    return obj;
}

obj的关联对象会在_object_remove_assocations方法中全部移除,同时,会将obj自身从AssociationsManager的map中移除:

void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (associations.size() == 0) return;
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // copy all of the associations that need to be removed.
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            associations.erase(i);
        }
    }
    // the calls to releaseValue() happen outside of the lock.
    for_each(elements.begin(), elements.end(), ReleaseValue());
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348