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

在平日編程中或閱讀第三方代碼時(shí)适滓,category可以說是無處不在疹启。category也可以說是OC作為一門動(dòng)態(tài)語言的一大特色辨泳。category為我們動(dòng)態(tài)擴(kuò)展類的功能提供了可能谈截,或者我們也可以把一個(gè)龐大的類進(jìn)行功能分解,按照category進(jìn)行組織芜辕。

關(guān)于category的使用無需多言尚骄,今天我們來深入了解一下,category是如何在runtime中實(shí)現(xiàn)的侵续。

category的數(shù)據(jù)結(jié)構(gòu)

category對應(yīng)到runtime中的結(jié)構(gòu)體是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 的可為:添加實(shí)例方法(instanceMethods)状蜗,類方法(classMethods)需五,協(xié)議(protocols)和實(shí)例屬性(instanceProperties),以及不可為:不能夠添加實(shí)例變量(關(guān)于實(shí)例屬性和實(shí)例變量的區(qū)別轧坎,我們將會(huì)在別的章節(jié)中探討)宏邮。

category的加載

知道了category的數(shù)據(jù)結(jié)構(gòu),我們來深入探究一下category是如何在runtime中實(shí)現(xiàn)的。

原理很簡單:runtime會(huì)分別將category 結(jié)構(gòu)體中的instanceMethods, protocols蜜氨,instanceProperties添加到target class的實(shí)例方法列表械筛,協(xié)議列表,屬性列表中飒炎,會(huì)將category結(jié)構(gòu)體中的classMethods添加到target class所對應(yīng)的元類的實(shí)例方法列表中埋哟。其本質(zhì)就相當(dāng)于runtime在運(yùn)行時(shí)期,修改了target class的結(jié)構(gòu)郎汪。

經(jīng)過這一番修改赤赊,category中的方法,就變成了target class方法列表中的一部分怒竿,其調(diào)用方式也就一模一樣啦~

現(xiàn)在砍鸠,就來看一下具體是怎么實(shí)現(xiàn)的。

首先耕驰,我們在Mach-O格式和runtime 介紹過在Mach-O文件中爷辱,category數(shù)據(jù)會(huì)被存放在__DATA段下的__objc_catlist section中。

當(dāng)OC被dyld加載起來時(shí)朦肘,OC進(jìn)入其入口點(diǎn)函數(shù)_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方法饭弓,重點(diǎn)來看_dyld_objc_notify_register方法。該方法會(huì)向dyld注冊監(jiān)聽Mach-O中OC相關(guān)section被加載入\載出內(nèi)存的事件媒抠。

具體有三個(gè)事件:
_dyld_objc_notify_mapped(對應(yīng)&map_images回調(diào)):當(dāng)dyld已將OC images加載入內(nèi)存時(shí)弟断。
_dyld_objc_notify_init(對應(yīng)load_images回調(diào)):當(dāng)dyld將要初始化OC image時(shí)。OC調(diào)用類的+load方法趴生,就是在這時(shí)進(jìn)行的阀趴。
_dyld_objc_notify_unmapped(對應(yīng)unmap_image回調(diào)):當(dāng)dyld將OC images移除內(nèi)存時(shí)。

而category寫入target class的方法列表苍匆,則是在_dyld_objc_notify_mapped刘急,即將OC相關(guān)sections都加載到內(nèi)存之后所發(fā)生的。

我們可以看到其對應(yīng)回調(diào)為map_images方法浸踩。

map_images 最終會(huì)調(diào)用_read_images 方法來讀取OC相關(guān)sections叔汁,并以此來初始化OC內(nèi)存環(huán)境。_read_images 的極簡實(shí)現(xiàn)版如下检碗,可以看到据块,rumtime是如何根據(jù)Mach-O各個(gè)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調(diào)用_getObjc2XXX格式的方法折剃,依次來讀取對應(yīng)的section內(nèi)容另假,并根據(jù)其結(jié)果初始化其自身結(jié)構(gòu)。

_getObjc2XXX 方法有如下幾種怕犁,可以看到他們都一一對應(yīng)了Mach-O中相關(guān)的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");

可以看到开睡,我們使用的類,協(xié)議和category苟耻,都是在_read_images 方法中讀取出來的。
在讀取cateogry的方法 _getObjc2CategoryList(hi, &count)中扶檐,讀取的是Mach-O文件的 __objc_catlist 段凶杖。

我們重點(diǎn)關(guān)注和category相關(guān)的代碼:

    // 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中有實(shí)例方法,協(xié)議款筑,實(shí)例屬性智蝠,會(huì)改寫target class的結(jié)構(gòu)
            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中有類方法,協(xié)議奈梳,或類屬性(目前OC版本不支持類屬性), 會(huì)改寫target class的元類結(jié)構(gòu)
            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. 先調(diào)用_getObjc2CategoryList讀取__objc_catlist seciton下所記錄的所有category杈湾。并存放到category_t *數(shù)組中。
  2. 依次讀取數(shù)組中的category_t * cat
  3. 對每一個(gè)cat攘须,先調(diào)用remapClass(cat->cls)漆撞,并返回一個(gè)objc_class *對象cls。這一步的目的在于找到到category對應(yīng)的類對象cls于宙。
  4. 找到category對應(yīng)的類對象cls后浮驳,就開始進(jìn)行對cls的修改操作了。首先捞魁,如果category中有實(shí)例方法至会,協(xié)議,和實(shí)例屬性之一的話谱俭,則直接對cls進(jìn)行操作奉件。如果category中包含了類方法,協(xié)議昆著,類屬性(不支持)之一的話县貌,還要對cls所對應(yīng)的元類(cls->ISA())進(jìn)行操作。
  5. 不管是對cls還是cls的元類進(jìn)行操作宣吱,都是調(diào)用的方法addUnattachedCategoryForClass窃这。但這個(gè)方法并不是category實(shí)現(xiàn)的關(guān)鍵,其內(nèi)部邏輯只是將class和其對應(yīng)的category做了一個(gè)映射征候。這樣杭攻,以class為key,就可以取到所其對應(yīng)的所有的category疤坝。
  6. 做好class和category的映射后兆解,會(huì)調(diào)用remethodizeClass方法來修改class的method list結(jié)構(gòu),這才是runtime實(shí)現(xiàn)category的關(guān)鍵所在跑揉。

remethodizeClass

既然remethodizeClass是category的實(shí)現(xiàn)核心锅睛,那么我們就單獨(dú)一節(jié)埠巨,細(xì)看一下該方法的實(shí)現(xiàn):


/***********************************************************************
* 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,然后調(diào)用attachCategories將這些category附加到class上现拒。

attachCategories 的實(shí)現(xiàn)如下:

// 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 *的數(shù)組空間印蔬,數(shù)組大小等于category的個(gè)數(shù)
    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--) {  // 依次讀取每一個(gè)category勋桶,將其methods,property侥猬,protocol添加到mlists例驹,proplist,protolist中存儲(chǔ)
        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()數(shù)據(jù)退唠,其實(shí)是class_rw_t * 指針鹃锈,其對應(yīng)結(jié)構(gòu)體實(shí)例存儲(chǔ)了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); // 如果需要,同時(shí)刷新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被加入到了對應(yīng)class的方法,協(xié)議以及屬性列表中秸苗。

最后我們再看一下attachLists方法是如何將兩個(gè)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]));
        }
    }

仔細(xì)看會(huì)發(fā)現(xiàn)召娜,attachLists方法其實(shí)是使用的‘頭插’的方式將新的list插入原有l(wèi)ist中的。即惊楼,新的list會(huì)插入到原始list的頭部玖瘸。

<font color=orange>這也就說明了,為什么category中的方法檀咙,會(huì)‘覆蓋’class的原始方法雅倒。其實(shí)并沒有真正的‘覆蓋’,而是由于cateogry中的方法被排到了原始方法的前面弧可,那么在消息查找流程中蔑匣,會(huì)返回首先被查找到的cateogry方法的實(shí)現(xiàn)。</font>

category和+load方法

在面試時(shí)棕诵,可能被問到這樣的問題:

在類的+load方法中裁良,可以調(diào)用分類方法嗎?

要回答這個(gè)問題校套,其實(shí)要搞清load方法的調(diào)用時(shí)機(jī)和category附加到class上的先后順序价脾。

如果在load方法被調(diào)用前,category已經(jīng)完成了附加到class上的流程笛匙,則對于上面的問題侨把,答案是肯定的犀变。

我們回到runtime的入口函數(shù)來看一下,

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在入口點(diǎn)分別向dyld注冊了三個(gè)事件監(jiān)聽:mapped oc sections秋柄, init oc section 以及 unmapped oc sections获枝。

而這三個(gè)事件的順序是: mapped oc sections -> init oc section -> unmapped oc sections

mapped oc sections 事件中,我們已經(jīng)看過其源碼骇笔,runtime會(huì)依次讀取Mach-O文件中的oc sections映琳,并根據(jù)這些信息來初始化runtime環(huán)境。這其中就包括cateogry的加載蜘拉。

之后,當(dāng)runtime環(huán)境都初始化完畢有鹿,在dyld的init oc section 事件中旭旭,runtime會(huì)調(diào)用每一個(gè)加載到內(nèi)存中的類的+load方法。

這里我們注意到葱跋,+load方法的調(diào)用是在cateogry加載之后的持寄。因此,在+load方法中娱俺,是可以調(diào)用category方法的稍味。

調(diào)用已被category‘覆蓋’的方法

前面我們已經(jīng)知道,類中的方法并不是真正的被category‘覆蓋’荠卷,而是被放到了類方法列表的后面模庐,消息查找時(shí)找不到而已。我們當(dāng)然也可以手動(dòng)來找到并調(diào)用它油宜,代碼如下:

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

// 分別調(diào)用sayHi 和 saySonHi
Son *mySon1 = [Son new];
[mySon1 sayHi];
[mySon1 saySonHi];

輸出為:

這里寫圖片描述

果然,我們調(diào)用到了原始的sayHi方法慎冤。

category和關(guān)聯(lián)對象

眾所周知疼燥,category是不支持向類添加實(shí)例變量的。這在源碼中也可以看出蚁堤,cateogry僅支持實(shí)例方法醉者、類方法、協(xié)議披诗、和實(shí)例屬性(注意撬即,實(shí)例屬性并不等于實(shí)例變量)。

但是藤巢,runtime也給我提供了一個(gè)折中的方式搞莺,雖然不能夠向類添加實(shí)例變量,但是runtime為我們提供了方法掂咒,可以向類的實(shí)例對象添加關(guān)聯(lián)對象才沧。

所謂關(guān)聯(lián)對象迈喉,就是為目標(biāo)對象添加一個(gè)關(guān)聯(lián)的對象,并能夠通過key來查找到這個(gè)關(guān)聯(lián)對象温圆。說的形象一點(diǎn)挨摸,就像我們?nèi)ヌ瑁瑀untime可以給我們分配一個(gè)舞伴一樣岁歉。

這種關(guān)聯(lián)是對象和對象級(jí)別的得运,而不是類層次上的。當(dāng)你為一個(gè)類實(shí)例添加一個(gè)關(guān)聯(lián)對象后锅移,如果你再創(chuàng)建另一個(gè)類實(shí)例熔掺,這個(gè)新建的實(shí)例是沒有關(guān)聯(lián)對象的。

我們可以通過重寫set/get方法的形式非剃,來自動(dòng)為我們的實(shí)例添加關(guān)聯(lián)對象置逻。

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

代碼很簡單,我們重點(diǎn)關(guān)注一下其背后的實(shí)現(xiàn)备绽。

objc_setAssociatedObject

我們要設(shè)置關(guān)聯(lián)對象券坞,需要調(diào)用objc_setAssociatedObject 方法將對象關(guān)聯(lián)到目標(biāo)對象上。我們需要傳入4個(gè)參數(shù):target object肺素, associated key恨锚, associated value, objc_AssociationPolicy。

objc_AssociationPolicy是一個(gè)枚舉倍靡,可以取值為:

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的屬性定義一一匹配猴伶。

當(dāng)我們?yōu)閷ο笤O(shè)置關(guān)聯(lián)對象的時(shí)候,所關(guān)聯(lián)的對象到底存在了那里呢塌西?我們看源碼:

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; // 這是一個(gè)單例蜗顽,內(nèi)部保存一個(gè)全局的static AssociationsHashMap *_map; 用于保存所有的關(guān)聯(lián)對象。
        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標(biāo)記為 has AssociatedObjects
            }
        } else { // 如果傳入的關(guān)聯(lián)對象值為nil雨让,則斷開關(guān)聯(lián)
            // 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關(guān)聯(lián)對象雇盖。(如果多次設(shè)置同一個(gè)key的value,這里會(huì)釋放之前的value)
}

大體流程為:

  1. 根據(jù)關(guān)聯(lián)的policy,調(diào)用id new_value = value ? acquireValue(value, policy) : nil; 栖忠,acquireValue 方法會(huì)根據(jù)poilcy是retain或copy崔挖,對value做引用+1操作或copy操作,并返回對應(yīng)的new_value庵寞。(如果傳入的value為nil狸相,則返回nil,不做任何操作)
    acquireValue實(shí)現(xiàn)代碼是:
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;
}
  1. 獲取到new_value 后捐川,根據(jù)是否有new_value的值脓鹃,進(jìn)入不同流程。如果 new_value 存在古沥,則對象與目標(biāo)對象關(guān)聯(lián)瘸右。實(shí)質(zhì)是存入到全局單例 AssociationsManager manager 的對象關(guān)聯(lián)表中娇跟。 如果new_value 不存在,則釋放掉之前目標(biāo)對象及關(guān)聯(lián) key所存儲(chǔ)的關(guān)聯(lián)對象太颤。實(shí)質(zhì)是在 AssociationsManager 中刪除掉關(guān)聯(lián)對象苞俘。
  2. 最后,釋放掉之前以同樣key存儲(chǔ)的關(guān)聯(lián)對象龄章。

其中吃谣,起到關(guān)鍵作用的在于AssociationsManager manager癌蚁, 它是一個(gè)全局單例驴党,其成員變量為static AssociationsHashMap *_map钟哥,用于存儲(chǔ)目標(biāo)對象及其關(guān)聯(lián)的對象肤视。_map中的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)如下圖所示:

這里寫圖片描述

仔細(xì)看這一段代碼,會(huì)發(fā)現(xiàn)有個(gè)問題:當(dāng)我們第一次為目標(biāo)對象創(chuàng)建關(guān)聯(lián)對象時(shí)塞弊,會(huì)在AssociationsManager managerObjectAssociationMap 中插入一個(gè)以disguised_object為key 的節(jié)點(diǎn)菠发,用于存儲(chǔ)該目標(biāo)對象所關(guān)聯(lián)的對象艺蝴。

但是惋鸥,上面代碼中,僅有釋放old_association關(guān)聯(lián)對象的代碼悍缠,而沒有釋放保存在AssociationsManager manager 中節(jié)點(diǎn)的代碼卦绣。那么,AssociationsManager manager 中的節(jié)點(diǎn)是什么時(shí)候被釋放的呢飞蚓?

在對象的銷毀邏輯里滤港,會(huì)調(diào)用objc_destructInstance,實(shí)現(xiàn)如下:

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); // 調(diào)用C++析構(gòu)函數(shù)
        if (assoc) _object_remove_assocations(obj); // 移除所有的關(guān)聯(lián)對象趴拧,并將其自身從AssociationsManager的map中移除
        obj->clearDeallocating(); // 清理ARC ivar
    }

    return obj;
}

obj的關(guān)聯(lián)對象會(huì)在_object_remove_assocations方法中全部移除溅漾,同時(shí),會(huì)將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());
}

參考文獻(xiàn)

深入理解Objective-C:Category

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末著榴,一起剝皮案震驚了整個(gè)濱河市添履,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌脑又,老刑警劉巖暮胧,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異问麸,居然都是意外死亡往衷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門严卖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來席舍,“玉大人,你說我怎么就攤上這事哮笆±床” “怎么了汰扭?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長脚曾。 經(jīng)常有香客問我东且,道長,這世上最難降的妖魔是什么本讥? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任珊泳,我火速辦了婚禮,結(jié)果婚禮上拷沸,老公的妹妹穿的比我還像新娘色查。我一直安慰自己,他們只是感情好撞芍,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布秧了。 她就那樣靜靜地躺著,像睡著了一般序无。 火紅的嫁衣襯著肌膚如雪验毡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天帝嗡,我揣著相機(jī)與錄音晶通,去河邊找鬼。 笑死哟玷,一個(gè)胖子當(dāng)著我的面吹牛狮辽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播巢寡,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼喉脖,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了抑月?” 一聲冷哼從身側(cè)響起树叽,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谦絮,沒想到半個(gè)月后菱皆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡挨稿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年仇轻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奶甘。...
    茶點(diǎn)故事閱讀 38,724評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡篷店,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情疲陕,我是刑警寧澤方淤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站蹄殃,受9級(jí)特大地震影響携茂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜诅岩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一讳苦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吩谦,春花似錦鸳谜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至滑废,卻和暖如春蝗肪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蠕趁。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工薛闪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人妻导。 一個(gè)月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像怀各,于是被迫代替她去往敵國和親倔韭。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容