1. 程式人生 > >Objective-C runtime機制(4)——深入理解Category

Objective-C runtime機制(4)——深入理解Category

在平日程式設計中或閱讀第三方程式碼時,category可以說是無處不在。category也可以說是OC作為一門動態語言的一大特色。category為我們動態擴充套件類的功能提供了可能,或者我們也可以把一個龐大的類進行功能分解,按照category進行組織。

關於category的使用無需多言,今天我們來深入瞭解一下,category是如何在runtime中實現的。

category的資料結構

category對應到runtime中的結構體是struct category_t(位於objc-runtime-new.h):

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的載入

知道了category的資料結構,我們來深入探究一下category是如何在runtime中實現的。

原理很簡單:runtime會分別將category 結構體中的instanceMethods, protocolsinstanceProperties新增到target class的例項方法列表,協議列表,屬性列表中,會將category結構體中的classMethods新增到target class所對應的元類的例項方法列表中。其本質就相當於runtime在執行時期,修改了target class的結構。

經過這一番修改,category中的方法,就變成了target class方法列表中的一部分,其呼叫方式也就一模一樣啦~

現在,就來看一下具體是怎麼實現的。

首先,我們在Mach-O格式和runtime 介紹過在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已將OC images載入入記憶體時。
_dyld_objc_notify_init(對應load_images回撥):當dyld將要初始化OC image時。OC呼叫類的+load方法,就是在這時進行的。
_dyld_objc_notify_unmapped(對應unmap_image回撥):當dyld將OC images移除記憶體時。

而category寫入target class的方法列表,則是在_dyld_objc_notify_mapped,即將OC相關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 方法中讀取出來的。

我們重點關注和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做了一個對映。這樣,以class為key,就可以取到所其對應的所有的category。
6. 做好class和category的對映後,會呼叫remethodizeClass方法來修改class的method list結構,這才是runtime實現category的關鍵所在。

remethodizeClass

既然remethodizeClass是category的實現核心,那麼我們就單獨一節,細看一下該方法的實現:


/***********************************************************************
* 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 sections, init 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];

輸出為:

這裡寫圖片描述

果然,我們呼叫到了原始的sayHi方法。

category和關聯物件

眾所周知,category是不支援向類新增例項變數的。這在原始碼中也可以看出,cateogry僅支援例項方法、類方法、協議、和例項屬性(注意,例項屬性並不等於例項變數)。

但是,runtime也給我提供了一個折中的方式,雖然不能夠向類新增例項變數,但是runtime為我們提供了方法,可以向類的例項物件新增關聯物件。

所謂關聯物件,就是為目標物件新增一個關聯的物件,並能夠通過key來查詢到這個關聯物件。說的形象一點,就像我們去跳舞,runtime可以給我們分配一個舞伴一樣。

這種關聯是物件和物件級別的,而不是類層次上的。當你為一個類例項新增一個關聯物件後,如果你在建立另一個類例項,如果不執行相關方法的話,這個新建的例項是沒有關聯物件的。

我們可以通過重寫set/get方法的形式,來自動為我們的例項新增關聯物件。

MyClass+Category1.h:

#import "MyClass.h"

@interface MyClass (Category1)

@property(nonatomic,copy) NSString *name;

@end

MyClass+Category1.m:

#import "MyClass+Category1.h"
#import <objc/runtime.h>

@implementation MyClass (Category1)

+ (void)load
{
    NSLog(@"%@",@"load in Category1");
}

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

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

@end

程式碼很簡單,我們重點關注一下其背後的實現。

objc_setAssociatedObject

我們要設定關聯物件,需要呼叫objc_setAssociatedObject 方法將物件關聯到目標物件上。我們需要傳入4個引數:target object, associated key, associated 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 方法會根據poilcy是retain或copy,對value做引用+1操作或copy操作,並返回對應的new_value。(如果傳入的value為nil,則返回nil,不做任何操作)
  2. 獲取到new_value 後,根據是否有new_value的值,進入不同流程。如果 new_value 存在,則物件與目標物件關聯。實質是存入到全域性單例 AssociationsManager manager 的物件關聯表中。 如果new_value 不存在,則釋放掉之前目標物件及關聯 key所儲存的關聯物件。實質是在 AssociationsManager 中刪除掉關聯物件。
  3. 最後,釋放掉之前以同樣key儲存的關聯物件。

其中,起到關鍵作用的在於AssociationsManager manager, 它是一個全域性單例,其成員變數為static AssociationsHashMap *_map,用於儲存目標物件及其關聯的物件。_map中的資料儲存結構如下圖所示:

這裡寫圖片描述

仔細看這一段程式碼,會發現有個問題:當我們第一次為目標物件建立關聯物件時,會在AssociationsManager managerObjectAssociationMap 中插入一個以disguised_object為key 的節點,用於儲存該目標物件所關聯的物件。

但是,上面程式碼中,僅有釋放關聯物件的程式碼,而沒有釋放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自身從AssociationsManagermap中移除:

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

參考文獻

深入理解Objective-C:Category