Category相關(guān)探究

1. Category的作用

  • 可以減少單個文件的體積
  • 可以把不同的功能組織到不同的Category中
  • 可以按需加載
  • 聲明私有方法
  • 把framework的私有方法公開
    Tips:比如一個類的方法,沒有在.h文件中暴露出來,我們可以給這個類創(chuàng)建一個分類,在分類的.h文件中,去聲明這個私有方法。

Extension 擴(kuò)展

  • 類的一部分
  • 編譯時期就確定的
  • 隱藏類的私有屬性

Category

  • 運(yùn)行時期確定的
  • 不能添加變量(編譯時期,對象的內(nèi)存布局已經(jīng)確定了,添加變量會破壞類的內(nèi)存布局)

2. Category 底層原理探索

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

struct category_t {
    const char *name; // 所屬類的名稱
    classref_t cls; // 要擴(kuò)展的類對象(編譯時期是沒有值的)
    struct method_list_t *instanceMethods; // 實(shí)例方法
    struct method_list_t *classMethods; // 類方法
    struct protocol_list_t *protocols;    // 協(xié)議
    struct property_list_t *instanceProperties; // 實(shí)例屬性
    // 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的數(shù)據(jù)結(jié)構(gòu),我們就可以看出category的可為(可以添加實(shí)例方法,類方法,甚至可以實(shí)現(xiàn)協(xié)議,添加屬性)和不可為(無法添加實(shí)例變量)。

2.1 代碼調(diào)用流程分析
  • 2.1.1 編譯時期

可以通過clang 來編譯一下:

clang -rewrite-objc DDPerson+Test.m

DDPerson+Test.m就是我隨便創(chuàng)建的一個分類的 .m文件,使用上述命令編譯完成后,會在該文件同級目錄下生成一個DDPerson+Test.cpp文件。

Tips:編譯過后的東西,都會存在Mach-O的可執(zhí)行文件當(dāng)中。

我們可以打開該文件找到分類的結(jié)構(gòu)體定義 _category_t:
再向下可以看到這個分類的結(jié)構(gòu)體到底存放了什么內(nèi)容:

static struct _category_t _OBJC_$_CATEGORY_DDPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
// 這個分類它存放在Mach-O文件__objc_const的字段中
{
    "DDPerson",  //  當(dāng)前類的名稱
    0, // &OBJC_CLASS_$_DDPerson, 指向類對象的指針,運(yùn)行時依賴 當(dāng)前類的名稱進(jìn)行綁定
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_DDPerson_$_Test, // 當(dāng)前分類里面的實(shí)例方法 
    0,
    0,
    0,
};

我們可以看到第三個 &OBJC$CATEGORY_INSTANCE_METHODS_DDPerson... ,是編譯器生成的當(dāng)前分類的實(shí)例方法列表。
因為我們只是給分類添加了一個方法,所以其余的類方法列表,協(xié)議列表,屬性列表,都是0,也就是空的。

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_DDPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
  // 下面三個參數(shù)分別是 方法編號 方法簽名 真實(shí)的函數(shù)地址
    {{(struct objc_selector *)"dd_privateTest", "v16@0:8", (void *)_I_DDPerson_Test_dd_privateTest}}
};

這一步就是生成分類的方法列表,用這個方法列表,初始化分類本身。

到現(xiàn)在為止分類在編譯時期做的事情差不多已經(jīng)完成了,
總結(jié)來說就是把當(dāng)前的分類編譯成結(jié)構(gòu)體,然后把對應(yīng)的字段填充上相應(yīng)的數(shù)據(jù)。

但是,編譯最后的分類數(shù)據(jù)放在哪里呢?我們繼續(xù)向下看:

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
    &_OBJC_$_CATEGORY_DDPerson_$_Test,
};

上面是非常關(guān)鍵的,所有的分類都會存在 __DATA 數(shù)據(jù)段下的的 __objc_catlist section當(dāng)中。

到目前為止,在編譯時期對于分類的操作都已經(jīng)完成了,運(yùn)行時期,App就會從 __objc_catlist section中去讀取所有的分類了。

  • 2.1.2 運(yùn)行時期

運(yùn)行時期就要開始加載編譯時期存儲的所有分類了。

那么Category是如何被加載的呢?

  • dyld 是蘋果的動態(tài)加載器,用來加載 image (不是指圖片,而是指 Mach-O 格式的二進(jìn)制文件)
  • 當(dāng)程序啟動的時候,系統(tǒng)內(nèi)核首先會加載dyld,而dyld會將我們APP所以來的各種庫加載到內(nèi)存空間中,其中就包括libobjc庫(OC和runtime),這些工作,是在APP的main函數(shù)執(zhí)行前完成的。
  • _objc_init 是 OC runtime 的入口函數(shù),在這里面主要功能是讀取Mach-O 文件 OC對應(yīng)的 Segment section,并根據(jù)其中的數(shù)據(jù)代碼信息,完成為OC的內(nèi)存布局,以及初始化runtime相關(guān)的數(shù)據(jù)結(jié)構(gòu)。

我們來看下_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();

    // 我們主要關(guān)注下面這個方法,這個方法注冊了三個回調(diào)函數(shù):
    // dyld將image加載入內(nèi)存的時候(將image映射到內(nèi)存空間的時候)會執(zhí)行 map_images 
    // dyld初始化image模塊完成的時候調(diào)用 load_images(load方法也會在這個時候調(diào)用)
    // dyld將image移出內(nèi)存的時候 unmap_image函數(shù)會被調(diào)用
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

將分類加載到當(dāng)前的類的操作是在 map_images 回調(diào)函數(shù)中完成的,下面我們先去看下這個 回調(diào)函數(shù):
我們忽略不必要的代碼走到下面這個函數(shù)中:

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])

在這個函數(shù)底部,我們可以看到 _read_images的函數(shù):

if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

_read_images 最主要的作用是: 讀取OC相關(guān)的 Section段(Mach-O文件中的數(shù)據(jù)段),并進(jìn)行初始化。

我們繼續(xù)看 _read_images這個函數(shù),直接到分類相關(guān)的部分:

// EACH_HEADER 就是遍歷頭文件
// 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;
            }
      ...

我們拿到的 catlist 就是上節(jié)講到的編譯器為我們生成的category_t數(shù)組,它是通過
_getObjc2ClassList 這個方法拿到的,我們來關(guān)注下 _getObjc2ClassList 這個方法,點(diǎn)進(jìn)去可以看到:


TSECT(_getObjc2NonlazyClassList,    classref_t,      "__objc_nlclslist");
// 看這個
GETSECT(_getObjc2CategoryList,        category_t *,    "__objc_catlist");
GETSECT(_getObjc2NonlazyCategoryList, category_t *,    "__objc_nlcatlist");
GETSECT(_getObjc2ProtocolList,        protocol_t *,    "__objc_protolist");

在編譯時期,我們可以知道,所有的分類都是存放在__objc_catlist 這個section當(dāng)中(或者說對應(yīng)這個key值),所以這里就是讀取這個 __objc_catlist 對應(yīng)的數(shù)據(jù)。

讀取完成之后,我們繼續(xù)往下看,下面這段代碼:

// 處理這個Category
            // 首先,將Category注冊到它的目標(biāo)類。
            // 然后,如果實(shí)現(xiàn)了類,則重構(gòu)類的方法列表(等等)。
            bool classExists = NO;
           // 把category的實(shí)例方法、協(xié)議以及屬性添加到類上
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {// 把分類注冊到它的目標(biāo)類建立一個關(guān)聯(lián)
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    // 重構(gòu)類的方法列表
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

           // 把category的類方法和協(xié)議添加到類的metaclass上
            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);
                }
            }

上面這段代碼,主要的功能如下:
a. 把category的實(shí)例方法、協(xié)議以及屬性添加到類上
b. 把category的類方法和協(xié)議添加到類的metaclass上

在上述的代碼片段里,addUnattachedCategoryForClass只是把類和category做一個關(guān)聯(lián)映射,而remethodizeClass才是真正去處理添加事宜的功臣。

那好,我們再去分析 remethodizeClass 這個關(guān)鍵方法:

// 在runtime源碼中,我們翻譯一下這個函數(shù)的注釋,就能知道這個函數(shù)的作用了
// 1.將當(dāng)前分類附加到現(xiàn)有類。
// 2.重新排列cls的方法列表、協(xié)議列表和屬性列表。
// 3.更新cls及其子類的方法緩存。
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

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

我們看到上面的方法,主要是調(diào)用 attachCategories 這個方法,
那么我們繼續(xù),在 attachCategories 函數(shù)中看到 prepareMethodList函數(shù):

...
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
...

這個函數(shù)的關(guān)鍵內(nèi)容如下,它只是把所有category的實(shí)例方法列表拼成了一個大的實(shí)例方法列表:

...
  // 添加方法列表到數(shù)組
  // 重新分配方法列表。
    // 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;
        }
    }
...

準(zhǔn)備好了這個方法列表之后之后,我們繼續(xù)看

...
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
...

主要是 rw->methods.attachLists(mlists, mcount); 這個方法

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

這里面主要是使用 memmove memcpy 兩個方法
都是用來做拷貝的操作,用來移動內(nèi)存空間,為將要添加的方法騰出內(nèi)存空間:

void    *memcpy(void *__dst, const void *__src, size_t __n);
void    *memmove(void *__dst, const void *__src, size_t __len);
// 把當(dāng)前 src 指向的位置,拷貝len的長度,放到dst的位置

比如:1 2 3 4 5 6 7
memmove(1的位置,4的位置,長度3)
完成之后就是 4 5 6 4 5 6 7
就是把 456 從 4 的位置,拷貝到 1 的位置
這種場景下,使用memcpy 也可以。

如果是另一個場景:將 1 的位置 長度為 3 拷貝到 3 的位置,
那么**有可能**會出現(xiàn) 12121567 這種情況

所以這種內(nèi)存重疊的情況下,memcpy 函數(shù) 有可能成功,有可能失敗。

然后我們分析下 代碼中

memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));

他的作用:
如果原有方法有10個,新添加的方法有3個,一共有13個,這句代碼的作用就是將原有的10個方法,放到13個方法的后10個
剛開始是
舊 舊 舊 舊 舊 舊 舊 舊 舊 舊 空 空 空
變成
空 空 空 舊 舊 舊 舊 舊 舊 舊 舊 舊 舊
這樣,前面三個方法就空出來了,因為這個場景剛好是上面介紹 memmove memcpy函數(shù)時的第二個場景,有可能會發(fā)生內(nèi)存重疊,所以這里使用的是memmove。

前面3個方法空出來了,此時我們就要把需要添加的三個方法放到這里了,這里不會發(fā)生內(nèi)存重疊,所以我們使用memcpy

memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));

它就是拷貝要添加的方法 addedLists 地址,長度為要添加的長度 addedCount 放到lists中。

從attachLists函數(shù)中我們可以看到,分類的方法覆蓋掉了主類的方法,分類方法會添加到主類方法的前面,這是因為運(yùn)行時在查找方法的時候是順著方法列表的順序查找的,它只要一找到對應(yīng)名字的方法,就會罷休,殊不知后面可能還有一樣名字的方法:

分類方法 分類方法 主類方法 主類方法。
我們可以打印出類的方法,然后通過代碼可以調(diào)用到主類的原有的方法。

我們現(xiàn)在可以總結(jié)一下map_images中對分類做的事情了:

  1. _getObjc2CategoryList方法調(diào)用__objc_catlist section 段記錄的所有Category,存放到category_t*數(shù)組
  2. 依次讀取category_t*數(shù)組
  3. cat和cls(主類)進(jìn)行映射
  4. remethodizeClass (修改method_list的結(jié)構(gòu)(memmove memcpy))

3.關(guān)聯(lián)對象探索

我們在開發(fā)中經(jīng)常使用關(guān)聯(lián)對象,在分類中添加一個屬性:

@interface DDPerson (Test)

@property (nonatomic, copy) NSString *name;

@end

我們知道在主類中的屬性其實(shí)包含三個東西:getter setter ivar, 這種在分類中聲明的屬性是沒有ivar的,只是生成了一個存取方法,以及存儲當(dāng)前value的容器(ObjcAssociation 下面會講到),因為上面也提到過,分類是不能添加成員變量的。

3.1 設(shè)置關(guān)聯(lián)對象

在給分類關(guān)聯(lián)對象的時候,我們會調(diào)用這個方法:

objc_setAssociatedObject(關(guān)聯(lián)到哪個對象, 存儲的key, 要關(guān)聯(lián)的對象, 關(guān)聯(lián)策略)

下面,我們就以這個方法為切入點(diǎn),從源碼中看一下:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}

實(shí)際上是調(diào)用了 _object_set_associative_reference 這個方法,繼續(xù)看:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    ObjcAssociation old_association(0, nil);
    // 取了一個新的value,根據(jù)策略設(shè)置
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        // 關(guān)聯(lián)對象的管理者
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        // 當(dāng)前要關(guān)聯(lián)到的對象的地址按位取反(作為當(dāng)前hashmap當(dāng)中的key)
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // c++ 語法,獲取一個迭代器,找到 disguised_object 作為key時對應(yīng)的value,里面的 j->first 指的是key j->second 指的是key對應(yīng)的value
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) { // 如果disguised_object 對應(yīng)的value有值,說明該對象有過關(guān)聯(lián)
                // 拿到 disguised_object 作為key 對應(yīng)的value,也是一個map
                ObjectAssociationMap *refs = i->second;
                // 再以key值獲取一個新的迭代器,這個key是我們傳進(jìn)來的,是我們所定義的
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) { // 如果用戶傳進(jìn)的key有對應(yīng)的value
                    // 獲取到舊的value,賦給old_association,最后會被釋放掉
                    old_association = j->second;
                    // 賦值新的value,ObjcAssociation 來存儲我們的關(guān)聯(lián)屬性
                    j->second = ObjcAssociation(policy, new_value);
                } else { // 如果用戶傳進(jìn)的key沒有對應(yīng)的value
                    // 直接賦值新的value
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // 如果這個對象是第一次關(guān)聯(lián),會走到這里,先創(chuàng)建一個Map,以disguised_object 作為key存進(jìn)manager管理的hashmap,再將new_value 和 policy生成的 ObjcAssociation 對象,以key存入 Map
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                // 設(shè)置當(dāng)前object的isa指針,添加一個標(biāo)識,說明這個對象有關(guān)聯(lián)對象,在這個對象銷毀的時候,去銷毀這些關(guān)聯(lián)對象
                object->setHasAssociatedObjects();
            }
        } else {
            // 如果是nil,則斷開關(guān)聯(lián)
            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);
                }
            }
        }
    }
    // 釋放 old_association
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

代碼很長,我們可以從上到下一點(diǎn)一點(diǎn)的分析:

a.先進(jìn)行內(nèi)存管理

// 這個方法是判斷value有值的話,就去調(diào)用acquireValue 生成一個新的對象,進(jìn)行內(nèi)存管理
 id new_value = value ? acquireValue(value, policy) : nil;
// 看下源碼
 acquireValue(value, policy) =>
// 清晰明了,根據(jù)關(guān)聯(lián)策略,對value進(jìn)行操作, objc_retain ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy); 這兩個操作就很熟悉了,strong copy關(guān)鍵字的內(nèi)部實(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;
}

b. AssociationsManager 初始化其管理的hashmap

 // 管理關(guān)聯(lián)對象,其內(nèi)部有一個static的 hashmap,用來管理關(guān)聯(lián)對象的
 AssociationsManager manager;
// 這個是初始化hashMap, 下面代碼中有這個方法的實(shí)現(xiàn)
 AssociationsHashMap &associations(manager.associations());
=> 點(diǎn)進(jìn)去看下 AssociationsManager:
// 從下面的注釋中我們也可以看到 這個hashMap 以對象的指針作為key的,value仍然是一個hashMap ,我們先了解下 AssociationsManager 的作用就是維護(hù)了一個hashmap.
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;
    }
};

存的過程,可以通過下面這張圖來理解,就會非常的清楚:


屏幕快照 2019-09-08 下午12.07.33.png
3.2 取關(guān)聯(lián)對象

理解了怎么存的也就明白如何取的了,貼上源碼:

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        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()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}

3.3 關(guān)聯(lián)對象如何釋放呢?

是在關(guān)聯(lián)到的對象釋放的時候,去釋放它的關(guān)聯(lián)對象。
我們可以從dealloc 方法入手去探究一下,這個隱藏的很深:

- (void)dealloc
↓
_objc_rootDealloc(self);
obj->rootDealloc();
object_dispose((id)this);
objc_destructInstance(obj);

終于到了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);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }
    return obj;
}

我們可以看到是判斷 assoc 這個變量為真時,去調(diào)用 _object_remove_assocations 移除關(guān)聯(lián)對象的方法,獲取 assoc 值的方法是
obj->hasAssociatedObjects(); 這個方法中其實(shí)就是獲取在設(shè)置關(guān)聯(lián)對象時,給isa設(shè)置的那個標(biāo)志。

我們繼續(xù)看 _object_remove_assocations 這個方法:


void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        // 獲取manager
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        // 如果size為0,說明沒有關(guān)聯(lián)屬性,直接return
        if (associations.size() == 0) return;
        // 對象的地址取反
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            
            // 獲取到disguised_object 對應(yīng)的map
            ObjectAssociationMap *refs = i->second;
            // 獲取到所有的關(guān)聯(lián)對象,放入elements
            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);
        }
    }
    // 釋放掉
    for_each(elements.begin(), elements.end(), ReleaseValue());
}

4. load方法探究

調(diào)用規(guī)則

  • 一個類的load方法在所有父類load 方法調(diào)用之后調(diào)用
  • 分類的load方法在當(dāng)前類的load方法調(diào)用之后調(diào)用
  • 分類load的調(diào)用順序和編譯順序有關(guān)

在前面,我們有講到objc_init是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();

    // 我們主要關(guān)注下面這個方法,這個方法注冊了三個回調(diào)函數(shù):
    // dyld將image加載入內(nèi)存的時候(將image映射到內(nèi)存空間的時候)會執(zhí)行 map_images 
    // dyld初始化image模塊完成的時候調(diào)用 load_images(load方法也會在這個時候調(diào)用)
    // dyld將image移出內(nèi)存的時候 unmap_image函數(shù)會被調(diào)用
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

load方法也是在 load_images 中調(diào)用的,我們現(xiàn)在就去從load_images方法入手去看一看:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
       // 準(zhǔn)備
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant) 調(diào)用
    call_load_methods();
}

這個方法分為兩塊,prepare_load_methods()和 call_load_methods();

4.1 prepare_load_methods

我們這邊先分析 prepare_load_methods (),他是先把各個類或分類load方法都準(zhǔn)備好:

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, I;

    runtimeLock.assertLocked();

  // 從Mach-o 文件加載類的列表
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 數(shù)組[<cls,method>,<cls,method>,<cls,method>] 有順序
        // 調(diào)整類的順序
        schedule_class_load(remapClass(classlist[i]));
    }

// 針對分類的操作
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[I];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}
4.1.1 針對類的load方法的操作

我們?nèi)タ聪?schedule_class_load 這個方法:

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    // 根據(jù)cls,將cls的load方法添加到一個全局的數(shù)組中
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

這個方法是遞歸調(diào)用,目的就是遵循上面的第一條規(guī)則,父類的load方法,在子類的load方法之前調(diào)用,也就是將類和類的load方法,這樣存入全局的一個容器(可以理解為數(shù)組)中:
[<父類,父類load方法>,<父類,父類load方法>,<子類,子類load方法>]

上面說到添加到一個全局的容器中,這一步是如何操作的呢?就是add_class_to_loadable_list:

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    
    if (loadable_classes_used == loadable_classes_allocated) {
        // 字節(jié)對齊(???)
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    // loadable_classes 就是一個全局的容器
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

到現(xiàn)在 schedule_class_load(remapClass(classlist[i]));
這個方法我們就分析完了,它就是調(diào)整順序,先將所有父類的load方法存入全局容器中,再存入當(dāng)前類的load方法。

4.1.2 針對分類load方法的操作

接下來,就到了對于分類的操作了,還回到prepare_load_methods 這個方法

void prepare_load_methods(const headerType *mhdr)
{
...
// 針對分類的操作
    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[I];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

我們知道分類load的調(diào)用順序是 和編譯順序有關(guān),上面的代碼中我們可以看到,當(dāng)從Mach-o文件中讀取到 categorylist 的時候,順序就已經(jīng)決定了,誰在前面就會先去調(diào)用誰的load方法,所以在下面就會直接調(diào)用 add_category_to_loadable_list(cat)方法:

void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

這個方法跟上面對于類的load方法操作一樣,初始化了loadable_categories 這么一個容器,存儲分類的load方法:
[<分類,load方法>,<分類,load方法>,<分類,load方法>]

到此,prepare_load_methods 的方法也就執(zhí)行完畢了,它就是準(zhǔn)備好了兩個全局容器loadable_classes和loadable_category,分別存儲類的load方法,分類的load方法。

4.2 call_load_methods

準(zhǔn)備好了之后,就要開始調(diào)用了:

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            // 先調(diào)用類的load方法
            call_class_loads();
        }

        // 2. Call category +loads ONCE 再調(diào)用分類的load方法
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

在上面可以看到,是先 call_class_loads 調(diào)用類的load方法,再 call_category_loads 調(diào)用分類的load方法。

4.2.1 call_class_loads 調(diào)用類的load方法
static void call_class_loads(void)
{
    int I;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}
4.2.1 call_category_loads 調(diào)用分類的load方法
static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, SEL_load);
            cats[i].cat = nil;
        }
    }

    // Compact detached list (order-preserving)
    shift = 0;
    for (i = 0; i < used; i++) {
        if (cats[i].cat) {
            cats[i-shift] = cats[I];
        } else {
            shift++;
        }
    }
    used -= shift;

    // Copy any new +load candidates from the new list to the detached list.
    new_categories_added = (loadable_categories_used > 0);
    for (i = 0; i < loadable_categories_used; i++) {
        if (used == allocated) {
            allocated = allocated*2 + 16;
            cats = (struct loadable_category *)
                realloc(cats, allocated *
                                  sizeof(struct loadable_category));
        }
        cats[used++] = loadable_categories[I];
    }

    // Destroy the new list.
    if (loadable_categories) free(loadable_categories);

    // Reattach the (now augmented) detached list. 
    // But if there's nothing left to load, destroy the list.
    if (used) {
        loadable_categories = cats;
        loadable_categories_used = used;
        loadable_categories_allocated = allocated;
    } else {
        if (cats) free(cats);
        loadable_categories = nil;
        loadable_categories_used = 0;
        loadable_categories_allocated = 0;
    }

    if (PrintLoading) {
        if (loadable_categories_used != 0) {
            _objc_inform("LOAD: %d categories still waiting for +load\n",
                         loadable_categories_used);
        }
    }

    return new_categories_added;
}

現(xiàn)在,我們可以對load方法的調(diào)用做下總結(jié)了:


屏幕快照 2019-09-08 下午1.11.21.png

調(diào)用順序也就是跟我們上面說的調(diào)用規(guī)則一樣了。

上面關(guān)于給分類設(shè)置關(guān)聯(lián)對象的時候,我們給當(dāng)前對象的isa設(shè)置了一個標(biāo)志,這里做一下補(bǔ)充:


屏幕快照 2019-09-08 下午1.16.13.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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