Category快快現(xiàn)真身

哈哈,有點蒙

文中咖啡圖片及第一個圖片來源百度圖片豺撑,如涉及到侵權,請聯(lián)系我刪除圖片

原創(chuàng)文章俩块,轉載請注明:轉自:Try_Try_Try

更新

時間:2018.07.06
內容:添加幾張結論圖,使得結論更加的直觀


背(吐)景(槽)

最近被問到了一個category問題,把我問的暈頭轉向帜矾,當時就很(想)佩(打)服(人),無疑是灰禿禿滾回去跪著搓板屑柔,面壁思過屡萤。

把這一塊知識惡狠狠的補了一桶,奶奶的掸宛,了解完之后死陆,發(fā)現(xiàn)so easy,被自己蠢哭了唧瘾。


引用

網上也有相關的文章寫的很好措译。我完全讀下來的就是美團的那篇深入理解Objective-C:Category别凤。

感覺寫的很好。我寫這篇文章時领虹,對著讀了好多遍规哪。

所以這次徹底對這篇文章分析一下(其實讀很多遍,是因為寫的很精簡塌衰,內部的實現(xiàn)細節(jié)需要自己對照源碼進行一一查看由缆。這樣才能把美團的這篇短短的文章讀成一個體系,然后再進行精簡猾蒂,知識就變成我的了---想太多了可能)均唉。


文章內容結構

  • o 代碼結構
  • 1 分類的結構
    • 1.1 題外話
    • 1.2 撕破你這層面紗(讓你再給我矯情)
    • 1.3 .cpp文件
    • 1.4 添加了category的消息發(fā)送流程總結
  • 2 +load 方法的原理
    • 2.1 題外話
    • 2.2 一探究竟
  • 3 initialize還有誰(有點捏花惹草)
    • 3.1 添加initialize方法進行測試
    • 3.2 猜測
    • 3.3 源碼分析-走-起-來
    • 3.4 正經點
    • 3.5 話外
  • 4 聯(lián)合起來才會更強(關聯(lián)對象)
    • 4.1 查看分類的結構
      • 4.1.1 查看類的結構
      • 4.1.2 分類結構
    • 4.2 關聯(lián)對象搞起來
      • 4.2.1 扒關聯(lián)對象的源碼
      • 4.2.2 內部具體實現(xiàn)細節(jié)分析

0. 代碼結構

0.1 代碼結構

Dog 繼承自Animate類,Dog中也有父類play方法肚菠,其中cat1和cat2中的方法一樣舔箭,都有play方法,只是打印的內容不一致蚊逢;

Animate.h

#import <Foundation/Foundation.h>

@interface Animate : NSObject

- (void)play;

@end

Animate.m

#import "Animate.h"

@implementation Animate

- (void)play
{
    NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
}

@end

Animate+cat1.h

#import "Animate.h"

@interface Animate (cat1)

- (void)play;

@end

Animate+cat1.m

#import "Animate+cat1.h"

@implementation Animate (cat1)

- (void)play
{
    NSLog(@"Animate (cat1)--%@", NSStringFromSelector(_cmd));
}

@end


main.m

#import <Foundation/Foundation.h>
#import "Dog.h"
#import "Dog+cat1.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        Dog *dog = [[Dog alloc] init];
        [dog play];
        
    }
    return 0;
}

此后代碼的分析层扶,也都是基于上述的結構,進行測試烙荷。

  • compile 順序1:


    圖0.2 代碼編譯順序1

圖0.2對應的執(zhí)行結果:

Dog (cat2)--play
  • compile 順序2:


    圖0.3 代碼編譯順序2

圖0.3對應的執(zhí)行結果:

Dog (cat1)--play

ARE YOU READY镜会?接下來是正文:


1. 分類的結構

1.1 題外話

之前看別人的代碼時候,一直出現(xiàn) clang -rewrite-objc filename.m终抽,這里是clang -rewrite-objc Animate+cat1.m命令戳表,然后就出現(xiàn)了神奇的.cpp文件,但是自己在terminal敲了一下昼伴,報了一堆錯(尼瑪匾旭,就失去了對.cpp的興趣了)。

后來發(fā)現(xiàn):電腦上安裝了多個不同版本的Xcode圃郊,又更改了名稱价涝,所以就無法找到。當更換成Xcode的真名時持舆,重新在代碼所處文件clang一下色瘩,神奇的.cpp出來了??(哥終于要研究一番感(懵)人(逼)的c++代碼了)。

生成的文件名稱為Animate+cat1.cpp逸寓,層級結構如下:

圖1.1 編譯之后文件位置

1.2 撕破你這層面紗(讓你再給我矯情)

我了個曹操居兆,竟然96763行左右,要嚇死了(要是工資能達到96k該多好啊席覆,我又做夢了)史辙。這要從哪行開始看啊,拖著滾動條看了一圈,發(fā)現(xiàn)前面都是聲明和定義聊倔,到最后才是真身(這庇護夠強盎薇小!你以為你是孫悟空啊耙蔑,躲在這花果山的最深處)见妒。

查了一下,發(fā)現(xiàn)之所以生成的文件這么大甸陌,可能的原因是不同的arm架構都有须揣。

1.3 .cpp文件

以下就是編譯后的部分關鍵代碼:

 struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

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_Animate_$_cat1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"play", "v16@0:8", (void *)_I_Animate_cat1_play}}
};

static struct _category_t _OBJC_$_CATEGORY_Animate_$_cat1 __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Animate",
    0, // &OBJC_CLASS_$_Animate,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Animate_$_cat1,
    0,
    0,
    0,
};
  • 通過查看 libobjc.order文件,有之后加載的順序钱豁。objc_init->map_images->map_images_nolock->_read_images耻卡。

以下是_read_images中,讀取分類的部分代碼(處理后的):

// Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
            }
        }
    }
  • 為了找到本質牲尺,還需要繼續(xù)沿著方法向下走:
    _read_images->addUnattachedCategoryForClass->remethodizeClass ->attachCategories->attachLists

attachCategories():將分類compile的順序進行逆序重組到數組中,這里決定了最后編譯的分類可能最先執(zhí)行卵酪。

while (i--) {
        auto& entry = cats->list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
    }

attachLists():將class中的方法和分類中的方法進行移動的操作,使得類中的方法放到數組的尾部谤碳,分類放到數組的頭部溃卡;

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

至此,類按照編譯順序逆序的過程結束蜒简,然后開始繼續(xù)消息發(fā)送制瘸羡。(關于消息發(fā)送的具體流程,網上很多資料搓茬,可以自己分析一波犹赖,源碼好像是匯編)

1.4 添加了category的消息發(fā)送流程總結

通過上述分析:這些添加分類神馬的,都是在編譯階段垮兑,編譯器幫我們完成的冷尉。因此至于最終如何執(zhí)行漱挎,就得按照運行時正常的消息發(fā)送流程系枪,添加上分類后即:

  1. 類(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)->
    父類(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)->
    ->
    ......
    ->
    msgForward->......
  2. 先執(zhí)行當前類對應的所有分類中最后編譯的那個分類方法直到結束,否則一直按照數組順序向后找磕谅。父類也類似私爷。

2018.7.6 更新
1.4.1 消息發(fā)送順序

好了,休息一下膊夹。

喝杯咖啡衬浑,緩緩

2. +load 方法的原理

2.1 題外話

如果在上述的6個類中都添加load方法,那么實現(xiàn)的邏輯又是怎樣的放刨?

在6個類中都添加如下代碼:

+ (void)load
{
    NSLog(@"%@--%@", 類名/分類名, NSStringFromSelector(_cmd));
}

運行結果:
圖2.1.1 編譯順序
圖2.1.2 圖2.1.1的運行結果

圖2.1.3 編譯順序
圖2.1.4 圖2.1.3的運行結果

2.2 一探究竟

圖2.2.1 load加載層次

正如圖2.2.1顯示工秩,關鍵代碼如紅色所示,prepare_load_methods加載完才會對call_load_methods進行調用。

如下是prepare_load_methods()部分代碼:

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

    runtimeLock.assertWriting();

    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        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);
    }
}

從上述代碼中可以并不能看出最終加載的順序助币,但是能夠看到class loads的順序浪听。

其中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);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

schedule_class_load代碼分析:

  1. 遞歸調用相當于數據結構中的的操作結構眉菱。棧底存放的可以看做是schedule_class_load(cls)迹栓,每次將cls->supercls作為schedule_class_load的參數,然后將其入棧俭缓,繼續(xù)判斷cls是否為nil克伊,如果為nil,則出棧华坦,進行處理add_class_to_loadable_list, 然后繼續(xù)出棧愿吹,直到棧空為止惜姐。

  2. 從源碼可以看出來:關于類的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
     ...  
     ...
     ...
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

從上述代碼中耘拇,有一點是注意的,if (!method) return;如果該類中沒有實現(xiàn)load方法宇攻,則直接返回惫叛,進行出棧的其他操作。

上述schedule_class_load結束之后逞刷,開始add_category_to_loadable_list嘉涌。該方法的加載就是按照編譯時的順序進行存儲。

當上述操作完成后夸浅,load的預加載也結束了仑最;接下來就是真正的call_load_methods的調用。

call_load_methods才能決定真正的調用流程有沒有在這一步分生變化帆喇,如下所示(精簡):

void call_load_methods(void)
{
    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

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

}

從上述的call_load_methods方法可以看出警医,關鍵的代碼是外層的do-while語句。

明顯的可以看出來坯钦,是先call_class_loads预皇,然后call_category_loads。因此婉刀,可以確定:cls -> category為大致的順序吟温。

通過查看這兩個函數的源碼,內部是很常規(guī)的for循環(huán)突颊,從頭到尾鲁豪。這里至少說是沒有顛倒順序的操作潘悼。

還發(fā)現(xiàn)了另一種情況。就是:load方法是主動執(zhí)行的爬橡,就算什么消息都不手動發(fā)送挥等,當程序運行起來的時候,它也會執(zhí)行堤尾。畢竟如果按照消息發(fā)送機制的邏輯肝劲,得我調你,你才執(zhí)行啊郭宝。它是運行時辞槐,系統(tǒng)進行調用的。

綜上所述粘室,可以得出結論:

  1. load方法的調用順序為: 類->分類;
  2. 類中l(wèi)oad調用順序為:父類->類榄檬;
  3. 分類load調用順序為:按照編譯的順序;
  4. 類(call[supercls,curcls])->分類(call[cmpl0.....cmplN])衔统。

綜上鹿榜,就是load的完整流程。

2018.7.6 更新
2.2.2 load調用順序

好了锦爵,休息一下舱殿。


來來來纽帖,一起休息一下

3. initialize還有誰(有點捏花惹草)

3.1 添加initialize方法進行測試

在前邊6個類中分別添加如下的測試代碼

+ (void)initialize
{
    NSLog(@"%@--%@", 類名/分類名, NSStringFromSelector(_cmd));
}

運行結果:
圖3.1.1 編譯順序
圖3.1.2 圖3.1.1的運行結果
圖3.1.3 編譯順序
圖3.1.4 圖3.1.3的運行結果

3.2 猜測

從結果可以預估:

  1. initialize先執(zhí)行父類镰吵,再執(zhí)行類孩革。

  2. 而且都只是執(zhí)行了分類爷耀,且執(zhí)行的分類的順序是按照編譯的逆序進行的,且只執(zhí)行了一次豺瘤。

  3. 從1的分析可以看出來加載load的影子家淤。如果按照先加載父類方法這個尿性的話乎莉,是不是內部也通過一個遞歸實現(xiàn)的埠啃。那么它和load的遞歸有區(qū)別嗎死宣?(媽的,別再yy了碴开。滾去看蘋果粑粑的源碼??)

  4. 從2的分析可以看出,是消息發(fā)送的機制叹螟。 如果真真的如猜測的一樣,關于3的消息發(fā)送機制罢绽,其實還有個小陷阱良价。即如果當前類沒有實現(xiàn)initialize方法,那么按照消息機制的尿性明垢,是不是要找他爹給擺平(畢竟官場氣息太重,這社會沒爹也是不行啊抵蚊。沒想到代碼中早已告訴了我這個道理溯革。[蠢哭])贞绳。

  5. 綜上來說,initialize確實有點騷致稀。這里摸一下冈闭,那里摸一下。所以一會得對照源碼進行分析一波(希望臉不要太疼)抖单。

3.3 源碼分析-走-起-來

皮一下
寫到這個標題萎攒,突然想到了我大渤哥(黃渤)在18年春晚的那首跳起來。寫到這矛绘,我腦袋里毅然神浮了這個魔曲耍休。

我屁股就坐在辦公的凳子上,一邊敲著代碼货矮,一邊左右擺動的甩了起來羹应。突然,被地上的幾個輪子親了一下次屠,疼的腳想踹人园匹。

畢竟我再晃兩下,我司的工學辦公椅劫灶,它那僅留的兩個輪子終將被我通通拋棄??(看來越發(fā)展裸违,這美曰其名的物件,壞在了質量做工啊供汛。是道德的淪喪怔昨,還是人性的缺失趁舀?歡迎收看今晚的xxxx)矮烹。

3.4 正經點

當我運行initialize方法時,發(fā)現(xiàn)與load方法不一樣卤唉。load方法是在運行時桑驱,主動調用的熬的。而initialize悦析,如果將main.m的代碼段注釋后强戴,是不會執(zhí)行骑歹。

這說明從調用的機制可以看出來兩者加載的方式是不一樣的道媚。當還原.m后最域,類第一次發(fā)送消息時镀脂,又開始調用了薄翅。從這可以看出翘魄,是進行了_objc_msgSend()調用暑竟,和剛才的猜想相照應光羞。

通過查找源碼中的libobjc.order文本可以看出來具體的執(zhí)行順序纱兑。對于我等屌絲來說潜慎,他可是個萬能的寶(寶铐炫?我信了你的襪)倒信。


圖3.4.1 initialize 調用的流程
  1. 從圖圖3.4.1中發(fā)現(xiàn)綠色部分代碼鳖悠,也使用了迭代操作乘综。(哈哈卡辰,此刻也應征了它確實偷偷摸了一下load這家伙)九妈。

_class_lookupMethodAndLoadCache3方法中的關鍵代碼如下:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

lookUpImpOrForward方法中的關鍵代碼如下:

if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

_class_initialize() 的關鍵代碼如下:

void _class_initialize(Class cls)
{
    assert(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;

    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }
    
    // Try to atomically set CLS_INITIALIZING.
    {
        monitor_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {
            cls->setInitializing();
            reallyInitialize = YES;
        }
    }
    callInitialize(cls);
}

該方法主要是為了讓父類先執(zhí)行,直到沒有父類呆贿,或者父類初始化完成if (supercls && !supercls->isInitialized()),然后執(zhí)行callInitialize(cls)冒晰。

而callInitialize(cls)代碼如下所示:

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

哇塞(賽哇)壶运,真面目出來了蒋情,最后一步就是objc_msgSend,哈哈辕翰,看你再躲喜命。因此接下來走消息發(fā)送的流程(分類initialize-> else -> 類)壁榕。這樣所有問題到這里就又結束了牌里。

綜上得出以下結論:

  1. 父類(分類 else 類)->類(分類 else 類)`牡辽;
  2. 其中()的內容 array[cmplN,cmplN_1,cmplN_2.....,cls]->method催享。
  3. 即:supercls(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)-curcls(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)

2018.7.6 更新
3.4.2 initialize調用順序

3.5 話外

等等......

還有一個問題,就是剛才在代碼中看到的一幕票髓。

  • 1.在上述lookUpImpOrForward貼出的源碼中的注釋洽沟,引起了我的興趣(我相信也引起了你的興趣裆操。如果沒有引起踪区,再去看一遍??):如果當前發(fā)的消息不是[[Dog alloc] init]缎岗,而換成[Dog initialize]
    結果如下所示:
    圖3.5.1 主動調用initialize

對于圖3.5.1所示鸭巴,多出了一次鹃祖,無疑是狗狗主動發(fā)送initialize引起的惯豆。

其實源碼中的注釋也解釋的非常清楚了(我從上邊搬到了下邊)。

if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

得出如下結論:initialize方法的調用是在該類在第一次使用時华临,調用的雅潭。而后該類再次使用時扶供,是不會調用的(好像如有所思)椿浓。


等等......(艸扳碍,我的橫杠分隔符都打上了笋敞,你才說還有)

還有一個問題(別墨跡夯巷,快說...)趁餐?

如果當前的子類澎怒、子分類中都沒有實現(xiàn)initialize方法喷面,只有父類惧辈、父分類中實現(xiàn)了initialize方法盒齿,那么運行結果如下圖所示:

圖3.5.2 只有父類翎承、父分類實現(xiàn)initialize

至于出現(xiàn)這種情況的原因叨咖,也是很容易分析的甸各。

其實就是源碼中一個很小的細節(jié)趣倾。這也是和load方法的區(qū)別儒恋。在遞歸調用方法時有一個條件判斷碧浊;

在load中遞歸調用到最頂層時箱锐,開始執(zhí)行add_class_to_loadable_list方法驹止,其中有一個代碼片段是如下情況:

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method

在load中臊恋,如果父類中沒有l(wèi)oad方法抖仅,就直接返回撤卢,出棧子類放吩,進行下次的操作渡紫。而看一下initialize遞歸處相應的源碼:

callInitialize(cls);
void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

從initialize源碼中可以看到惕澎,并沒有出現(xiàn)load中的沒有該方法時跳出的情況集灌,而是直接繼續(xù)暢通無阻的執(zhí)行(因為我是消息發(fā)送機制靶佬)。從而一路到達objc_msgSend锈锤。

這樣即使子分類久免、子類都沒有阎姥,此時也可以繼續(xù)尋找父類.

??呼巴,這次應該不用等了衣赶,沒了府瞄。

以上。

好了敲街,休息一下多艇。


果汁咖啡也挺好喝的

4 聯(lián)合起來才會更強(關聯(lián)對象)

在Animate+cat1.h中添加如下的測試代碼:

#import "Animate.h"

@interface Animate (cat1)

/** name */
@property (nonatomic, copy) NSString *name;

- (void)play;

@end

Animate+cat1.m


#import "Animate+cat1.h"
#import <objc/runtime.h>

@implementation Animate (cat1)

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

- (NSString *)name
{
    return (NSString *)objc_getAssociatedObject(self, "name");
}
@end

上述的第二個參數"name"也可以換成@selector(name),因為第二個參數的類型是:const void * _Nonnull key拨匆, 是void *類型惭每,所以在C中台腥,可以看做執(zhí)行函數的指針類型黎侈,因此可以直接換成OC中的SEL類型峻汉,且這樣寫會有提示休吠。

在main.m中進行簡單的測試瘤礁,賦值蔚携,取值操作酝蜒,就可以看出來使用起來和屬性差不多亡脑。

4.1 查看分類的結構

 struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

上述category結構中包含了:名稱、所屬類別拍屑、對象方法列表僵驰、類方法列表蒜茴、協(xié)議列表粉私、屬性列表诺核。

于是可以給分類中添加方法窖杀,協(xié)議和屬性陈瘦。但是好像沒有實例變量列表,那是不是說明分類中不可以添加實例變量呢锅风?

可以從以下兩方面入手:

4.1.1 查看類的結構

如果在Animate基類中皱埠,添加一個屬性和一個實例變量边器。代碼如下:

Animate.h

#import <Foundation/Foundation.h>

@interface Animate : NSObject

/** name */
@property (nonatomic, copy) NSString *name;

- (void)play;

@end

Animate.m

#import "Animate.h"

@interface Animate()
{
    NSString *_height;
    
}
@end

@implementation Animate

+ (void)load
{
    NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
}

+ (void)initialize
{
    NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
}

- (void)play
{
    NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
}

@end

從代碼中恒界,可以看到添加了一個屬性name和一個實例變量_height十酣,其中還包含有2個類方法,1個實例方法兴泥。

由屬性name的特性可知搓彻,會自動生成實例變量:_name旭贬,-setName:和-name方法的聲明及其實現(xiàn)骑篙。

因此:包含的內容應該是:1個屬性靶端,2個實例變量杨名,2個類方法台谍,3個對象方法的實現(xiàn)趁蕊。

通過將Animate.m文件進行clang掷伙,可以查看生成的c++關鍵源碼如下(精簡后):

struct _class_t OBJC_CLASS_$_Animate  = {
    0, // &OBJC_METACLASS_$_Animate,
    0, // &OBJC_CLASS_$_NSObject,
    0, // (void *)&_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_CLASS_RO_$_Animate,
};

struct _class_t {
    struct _class_t *isa;
    struct _class_t *superclass;
    void *cache;
    void *vtable;
    struct _class_ro_t *ro;
};

struct _class_ro_t {
    unsigned int flags;
    unsigned int instanceStart;
    unsigned int instanceSize;
    unsigned int reserved;
    const unsigned char *ivarLayout;
    const char *name;
    const struct _method_list_t *baseMethods;
    const struct _objc_protocol_list *baseProtocols;
    const struct _ivar_list_t *ivars;
    const unsigned char *weakIvarLayout;
    const struct _prop_list_t *properties;
};

從源碼中可以看出任柜,結構體的聲明和賦值操作宙地;我們關心的內容在結構體中_class_ro_t中:方法列表宅粥、協(xié)議列表蓖柔、屬性列表风纠、實例變量列表竹观、屬性列表臭增;然后看一下對應的各個列表的源碼:

  • 屬性列表:
static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Animate  = {
    sizeof(_prop_t),
    1,
    {{"name","T@\"NSString\",C,N,V_name"}}
};

struct _prop_t {
    const char *name;
    const char *attributes;
};

看到存儲了一個結構體的屬性數組中存放著唯一的name屬性列牺。

  • 實例變量列表:
static struct /*_ivar_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count;
    struct _ivar_t ivar_list[2];
} _OBJC_$_INSTANCE_VARIABLES_Animate  = {
    sizeof(_ivar_t),
    2,
    {{(unsigned long int *)&OBJC_IVAR_$_Animate$_height, "_height", "@\"NSString\"", 3, 8},
     {(unsigned long int *)&OBJC_IVAR_$_Animate$_name, "_name", "@\"NSString\"", 3, 8}}
};

struct _ivar_t {
    unsigned long int *offset;  // pointer to ivar offset location
    const char *name;
    const char *type;
    unsigned int alignment;
    unsigned int  size;
};

看到存儲了2個結構體的數組包含了原有的_height和屬性生成的_name瞎领。

  • 對象方法列表:
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[3];
} _OBJC_$_INSTANCE_METHODS_Animate __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    3,
    {{(struct objc_selector *)"play", "v16@0:8", (void *)_I_Animate_play},
    {(struct objc_selector *)"name", "@16@0:8", (void *)_I_Animate_name},
    {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Animate_setName_}}
};

struct _objc_method {
    struct objc_selector * _cmd;
    const char *method_type;
    void  *_imp;
};

存儲了3個結構體的數組九默,是原有的play方法和屬性生成的setName和name方法驼修。

也可以看到其他的類方法列表存儲在metacls中等乙各。通過查看類的結構觅丰,就能對之前的猜測進行有力的驗證简卧。

接下來就據此迷捧,對比的看一下,如果在分類中添加屬性列表和實例變量唇牧,又如何呢?

4.1.2 分類結構
  • 布置內容

首先在Animate+cat1.h中添加屬性name杆查。當我嘗試添加實例變量時亲桦,發(fā)現(xiàn)沒法添加客峭,一寫就報錯??舔琅;代碼如下:
Animate+cat1.h

#import "Animate.h"

@interface Animate (cat1)

/** name */
@property (nonatomic, copy) NSString *name;

- (void)play;

@end
  • 通過clang查看Animate+cat1.m的源碼
    我們只關心屬性列表洲劣、方法列表闪檬、方法實現(xiàn)
static struct /*_prop_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count_of_properties;
    struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Animate_$_cat1  = {
    sizeof(_prop_t),
    1,
    {{"name","T@\"NSString\",C,N"}}
};

wtf虚循,瞅了一圈横缔,愣是沒有看到實例變量列表結構茎刚,不死心又去查看了一波實例方法列表如下:

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_Animate_$_cat1  = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"play", "v16@0:8", (void *)_I_Animate_cat1_play}}
};

好吧膛锭,啥都沒有初狰,只有之前寫的play方法奢入。服了腥光,其實這也可以理解的。

4.1 查看分類的結構開頭议双,就直接說明了_category_t結構包含的內容聋伦。里邊確實是沒有ivar_lists觉增。這樣一來逾礁,即時結構中給了一個屬性列表嘹履,用處也不是很大啊砾嫉。

沒法使用其中的set和get方法進行操作焕刮,不能夠存儲內容配并。直接輸入_name或者self->_name也是行不通的溉旋。因此為了能夠存儲數據观腊,蘋果粑粑又跳出來了恕沫。

4.2 關聯(lián)對象搞起來

蘋果粑粑托夢

粑粑:小子,我給你屬性列表了偷霉,你只需要重寫相應的set和get方法。

:好像是啊??叙身。那......信轿,那我該怎么實現(xiàn)呢财忽?

我又沒辦法定義一個實例變量即彪,莫非再讓我定義一個屬性隶校,這樣有沒有set深胳,get方法舞终,這樣又開始循環(huán)了(子子孫孫权埠,無窮匱也H帘巍)满俗。
但我又沒辦法聲明一個實例變量唆垃。在分類中聲明一個實例變量辕万,想想就別扭啊醉途。

如果在.m中聲明一個實例變量隘擎,一般都是extention货葬,()中也沒有名稱震桶。在分類中尼夺,()中又是有內容的淤堵。如果這樣寫拐邪,又報了一對錯誤(啊啊啊啊扎阶,我瘋了)东臀。

粑粑:傻兒子惰赋,繼續(xù)想赁濒。拿出你C語言中長久不用的大招拒炎。

: 啥击你?奧(dingdong)我知道了,你讓我用全局變量嗎球切,這樣也行。但是以后我每新加了一個屬性户辱,都要重新定義一個全局變量庐镐。這樣粑粑會不會打死我必逆,搶你太多的飯了(內存)名眉。

那我定義一個字典就好了八鹇!(等待粑粑夸我)福压。

粑粑:不要搶老子的飯荆姆。

: 大哭(心想:屎粑粑胆筒,你那么有錢腐泻,已經從你的開發(fā)者中通過內購剝削了3分派桩,還這樣對我......)铆惑。

粑粑:好吧丑蛤,不逗你了受裹,其實我已經給你提供了一個關聯(lián)對象棉饶,方便你管理分類中的屬性照藻。至于在哪幸缕,你小子自己去找吧发乔。畢竟粑粑有些東西是不能夠給你說太清楚的列疗,否則都要來我這里更改東西了抵栈。

: 恩古劲,謝謝粑粑(??产艾,沒有我找不到的東西)。

4.2.1 扒關聯(lián)對象的源碼

通過搜索associated關鍵字可以找到疑故,步驟:objc_setAssociatedObject()->_object_set_associative_reference(), 如下:

/**********************************************************************
* Associative Reference Support
**********************************************************************/

id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}

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

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}

跳進_object_get_associative_reference踱阿,可以看到里邊出現(xiàn)了四個類:AssociationsManager、AssociationsHashMap才漆、ObjectAssociationMap醇滥、ObjcAssociation腺办。查看它們的結構如下:

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

class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };

class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };

class ObjcAssociation {
        uintptr_t _policy;
        id _value;
    public:
        ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
        ObjcAssociation() : _policy(0), _value(nil) {}

        uintptr_t policy() const { return _policy; }
        id value() const { return _value; }
        
        bool hasValue() { return _value != nil; }
    };

這段代碼怎么理解?

可以看到里邊有兩個map船响,類似于OC中的字典见间,后邊加了類似泛型的東西米诉。

再仔細一看史侣,這不就是一個二維數組嗎惊橱?

奶奶的税朴,代碼寫這么多正林,為啥不加上一個注釋說:都看好了觅廓,這一堆代碼像極了一個二維數組哪亿∮蓿總結之后篡殷,結構如下圖所示:

圖4.2.1.1 4個類大致結構關系

可以看出:

  • AssociationsManager中有一個對象AssociationsHashMap指針奇瘦,它是二維數組的地址耳标,相當于二位數組的名稱次坡。該值也是AssociationsManager的地址。他管理著內存中所有的關聯(lián)對象症脂。

  • 縱坐標為當前的分類對象object,object下標對應的整行為ObjectAssociationMap兴蒸。而該內部就是該分類對象下所有的關聯(lián)對象橙凳〉盒ィ可能有name的關聯(lián)對象坚踩,age瞬铸,size等等嗓节。

  • 橫坐標為當前的具體key拦宣。而如果能夠查找到鸵隧,該單元格就是ObjcAssociation珊蟀。它是value與policy通過運算的出的值系洛。
    以上就是基本的結構。

  • 如果繼續(xù)擴展趟薄,可以將它看到兩個表的組合杭煎。而object是最外層表的主鍵也切,而內層可以看成內部表的主鍵雷恃。這樣也能說得通附井。

  • 也可以將他看成一張表,只不過是雙主鍵罷了(object,key)太雨。

綜上狭瞎,通過上邊的圖碗殷,很容易看清楚其結構。至于其添加值员咽,獲取值捡偏,銷毀對象的過程傅物,通過上表也可以很容易的分析卒暂。

4.2.2 內部具體實現(xiàn)細節(jié)分析

以objc_setAssociatedObject()為例進行分析:

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

 // 其中acquireValue()函數是為了對value根據相應的內存策略進行處理
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
    // DISGUISE() ,是將object轉換為另一種類型:disguised_ptr_t
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

前邊的代碼加上了注釋谭跨。從if開始繼續(xù)分析:

  • 如果new_value有值,則要存進去挂捅;否則相當于清空表中對應的數據;

  • if分析:如果AssociationsHashMap列表中有disguised_object這條記錄蒙谓。取出該條記錄對應的ObjectAssociationMap指針舵揭,再根據key取得ObjcAssociation對象。然后將新的 ObjcAssociation(policy, new_value)填充到該位置即可墅垮。如果沒有根據找到該key對應的值灾梦,則直接手動添加即可寞宫。

  • else分析:相當于拿到清空原先關聯(lián)對象的值(或者成為初始化)篷就。

相應的objc_getAssociatedObject钾麸、objc_removeAssociatedObjects也是如此,可以自己查看相應源碼進行分析负敏。
好了,休息一下。


這個小熊虎的可愛
  • 本文相關的demo已放置github.
  • 在閱讀中,如果發(fā)現(xiàn)文章有不合理的地方,歡迎提出疑問至郵箱:B12050217@163.com.
  • 原創(chuàng)文章,轉載請注明:轉自:Try_Try_Try
  • OK, game over.
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末饼疙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子肾请,更是在濱河造成了極大的恐慌饵逐,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件苍息,死亡現(xiàn)場離奇詭異挑格,居然都是意外死亡破讨,警方通過查閱死者的電腦和手機铅忿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門夹孔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赴精,“玉大人,你說我怎么就攤上這事莉钙√枷耄” “怎么了案铺?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拖吼。 經常有香客問我,道長,這世上最難降的妖魔是什么眯牧? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任酒唉,我火速辦了婚禮,結果婚禮上媒役,老公的妹妹穿的比我還像新娘穿仪。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布睡互。 她就那樣靜靜地躺著妻怎,像睡著了一般榛丢。 火紅的嫁衣襯著肌膚如雪宾肺。 梳的紋絲不亂的頭發(fā)上增拥,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天澄耍,我揣著相機與錄音选酗,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼薛耻,長吁一口氣:“原來是場噩夢啊……” “哼缕溉!你這毒婦竟也來了泉褐?” 一聲冷哼從身側響起矩欠,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤夕膀,失蹤者是張志新(化名)和其女友劉穎易猫,沒想到半個月后炮赦,有當地人在樹林里發(fā)現(xiàn)了一具尸體看幼,經...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年溜腐,在試婚紗的時候發(fā)現(xiàn)自己被綠了望众。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出柱告,到底是詐尸還是另有隱情坡锡,我是刑警寧澤禽额,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布讯沈,位于F島的核電站挤茄,受9級特大地震影響,放射性物質發(fā)生泄漏评凝。R本人自食惡果不足惜谬返,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一状原、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧藕甩,春花似錦默怨、人聲如沸集绰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膨疏。三九已至,卻和暖如春均芽,著一層夾襖步出監(jiān)牢的瞬間湃鹊,已是汗流浹背芯义。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人培廓。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓踩窖,卻偏偏與公主長得像啥供,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子美旧,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內容