Category 底層分析

  • 一盗似、Category 淺層分析
  • 二俺亮、Category 底層結(jié)構(gòu)
  • 三、Category 源碼分析(分類方法優(yōu)先調(diào)用)
  • 四罩润、小結(jié)

一玖翅、Category 淺層分析


參照上圖,思考關(guān)于分類的問題。分類有沒有可能同上圖的 classmeta-class 處于并列關(guān)系烧栋,每創(chuàng)建一個(gè)分類分別對(duì)應(yīng)著一個(gè) 分類class分類meta-class 對(duì)象写妥?

這里先說出結(jié)論,在第二小節(jié)再做驗(yàn)證审姓。實(shí)際情況并非如此珍特,為了充分利用資源,一個(gè)類永遠(yuǎn)只存在一個(gè)類對(duì)象魔吐。Category 中的對(duì)象方法會(huì)合并到類(class)對(duì)象方法列表中扎筒,類方法合并到元類(meta-class)類方法列表中。方法合并的時(shí)機(jī)在運(yùn)行時(shí)進(jìn)行酬姆,而非編譯期間嗜桌。

二、Category 底層結(jié)構(gòu)

@implementation Person
- (void)run{
    NSLog(@"run");
}
@end

@implementation Person (Test)
- (void)test{
    NSLog(@"test");
}
+ (void)test{
    
}
@end

@implementation Person (Eat)
- (void)eat{
    NSLog(@"eat");
}
+ (void)eat{
  
}
@end

為驗(yàn)證上述問題辞色,可以編寫如上代碼骨宠,即創(chuàng)建 Person 以及 Person + TestPerson + Eat 分類。然后借助xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 輸出的CPP文件 命令將 Person + Test類轉(zhuǎn)為 C/C++ 代碼相满。

生成的 .cpp 文件中包含 _category_t 結(jié)構(gòu)體层亿,該結(jié)構(gòu)體即為分類底層數(shù)據(jù)結(jié)構(gòu),主要包含類名立美、對(duì)象方法匿又、類方法、協(xié)議以及屬性建蹄。

struct _category_t {
    const char *name;//類名碌更,這里是Person
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;//對(duì)象方法列表
    const struct _method_list_t *class_methods;//類方法列表
    const struct _protocol_list_t *protocols;//協(xié)議列表
    const struct _prop_list_t *properties;//屬性列表
};

生成的.cpp 文件中還存在下下面一段代碼,該段代碼與 _category_t結(jié)構(gòu)體對(duì)應(yīng)洞慎,依次給_category_t 結(jié)構(gòu)體內(nèi)部成員賦值痛单。其中第二、五劲腿、六三個(gè)參數(shù)均為 0桦他。

static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
    "Person",
    0, // &OBJC_CLASS_$_Person,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,
    0,
    0,
};

從上述驗(yàn)證可以看出,編譯期間過后谆棱,每個(gè)分類唯一對(duì)應(yīng)一個(gè) _category_t 結(jié)構(gòu)快压,與原有類是分開的。

三垃瞧、Category 源碼分析(分類方法優(yōu)先調(diào)用)

為進(jìn)一步理解 Category 蔫劣,可以查看在該網(wǎng)站 查看 objc4-723 runtime 底層源碼 「龃樱可以順著如下順序閱讀源碼脉幢,其中 objc-os.mm 文件為運(yùn)行時(shí)的入口文件歪沃。

objc-os.mm ---> _objc_init ---> map_images ---> map_images_nolock --->objc-runtime-new.mm --->_read_images --->remethodizeClass --->attachCategories --->attachLists

_read_images方法中可以發(fā)現(xiàn)這樣一段代碼,代碼上方注釋為 Discover categories , 另外還有個(gè)二維數(shù)組category_t ** catlist嫌松, catlist 數(shù)組中元素為結(jié)構(gòu)體沪曙,存儲(chǔ)著一堆 category_t 結(jié)構(gòu)。remethodizeClass 方法被調(diào)用兩次萎羔,從命名來看意思為:重新方法化液走,兩次傳入?yún)?shù)分別為 clscls->ISA, 即類對(duì)象的元類對(duì)象。

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

            // 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;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing 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);
                }
            }
        }
    }

remethodizeClass 方法內(nèi)部調(diào)用了 attachCategories 方法贾陷,attachCategories 方法前兩個(gè)參數(shù)分別為 class分類數(shù)組cats缘眶。

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

attachCategories 方法內(nèi)部創(chuàng)建了三個(gè)二維數(shù)組method_list_t **mlists,property_list_t **proplists,protocol_list_t **protolists, 分別用于保存方法、屬性和協(xié)議髓废。mlists[mcount++] = mlist; 表示取出每個(gè)分類中的方法列表放入到二維數(shù)組中巷懈;auto rw = cls->data(); 表示取出類對(duì)象中的數(shù)據(jù);rw->methods.attachLists(mlists, mcount);表示將所有分類的對(duì)象方法附加到類對(duì)象方法列表中慌洪。

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
//方法數(shù)組 [[method_t, method_t],[method_t, method_t]]
    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--) {
        auto& entry = cats->list[I];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
          //取出分類中的方法列表放入到二維數(shù)組中
            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;
        }
    }
   //取出類對(duì)象中的數(shù)據(jù)
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    //將所有分類的對(duì)象方法附加到類對(duì)象方法列表中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

attachLists 方法中調(diào)用memmove方法將類中的原有方法方法放到數(shù)組末尾顶燕,調(diào)用memcpy方法將二維數(shù)組 addedLists 中的每個(gè) Category 的方法列表放置到類中原有方法的前面。

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

四冈爹、小結(jié)

經(jīng)過上述源碼分析涌攻,最終方法在內(nèi)存中的布局結(jié)構(gòu)如下。該布局很好說明了分類方法調(diào)用順序要優(yōu)于原有類方法犯助。因?yàn)檎{(diào)用方法時(shí),會(huì)順序遍歷二維數(shù)組查找方法维咸,當(dāng)查找到目標(biāo)方法后就無需再向后繼續(xù)遍歷查找方法剂买。


補(bǔ)充

1、Category的實(shí)現(xiàn)原理
  • Category編譯之后的底層結(jié)構(gòu)是struct category_t癌蓖,里面存儲(chǔ)著分類的對(duì)象方法瞬哼、類方法、屬性租副、協(xié)議信息等
  • 在程序運(yùn)行的時(shí)候坐慰,runtime 會(huì)將 Category 的數(shù)據(jù),合并到類信息中(類對(duì)象用僧、元類對(duì)象中)
2结胀、Category 和 Class Extension 的區(qū)別?(注意這里的 extension 指的不是 swift 中的 extension)
  • Class Extension 在編譯的時(shí)候责循,它的數(shù)據(jù)就已經(jīng)包含在類信息中糟港。
  • Category 是在運(yùn)行時(shí),才會(huì)將數(shù)據(jù)合并到類信息中院仿。
  • Extension 可以添加成員變量秸抚,Category 只能添加屬性速和,不能添加成員變量。
3剥汤、分類為什么不能加成員變量颠放?

Category 在代碼里添加成員變量是根本編譯不過去的,而添加屬性是可以編譯通過的吭敢。

這要從Category 的原理說起碰凶,應(yīng)用啟動(dòng)時(shí)通過runtime動(dòng)態(tài)地把Category中的方法添加到類中,蘋果在實(shí)現(xiàn)的過程中并未將屬性添加到類中省有,屬性僅僅是聲明了setter和getter方法痒留,而并未實(shí)現(xiàn)。此時(shí)各個(gè)類的內(nèi)存布局已經(jīng)確定了蠢沿,不可以再更改伸头。

Category 可以加屬性,但是沒有對(duì)應(yīng)的成員變量存儲(chǔ)區(qū)域舷蟀。類的成員變量存儲(chǔ)區(qū)在編譯時(shí)就確定了恤磷。所以只能定義成@dynamic,運(yùn)行時(shí)關(guān)聯(lián)一塊內(nèi)存到對(duì)象野宜。在runtime中存在一個(gè)類型為AssociationHashMap的哈希映射表保存著對(duì)象動(dòng)態(tài)添加的屬性扫步,每個(gè)對(duì)象以自身地址為 key 維護(hù)著一個(gè)綁定屬性表,我們動(dòng)態(tài)添加的屬性就都存儲(chǔ)在這個(gè)表里匈子,這也是動(dòng)態(tài)添加property能成功的基礎(chǔ)河胎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市虎敦,隨后出現(xiàn)的幾起案子游岳,更是在濱河造成了極大的恐慌,老刑警劉巖其徙,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胚迫,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡唾那,警方通過查閱死者的電腦和手機(jī)访锻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闹获,“玉大人期犬,你說我怎么就攤上這事”芊蹋” “怎么了哭懈?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)茎用。 經(jīng)常有香客問我遣总,道長(zhǎng)睬罗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任旭斥,我火速辦了婚禮容达,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘垂券。我一直安慰自己花盐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布菇爪。 她就那樣靜靜地躺著算芯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凳宙。 梳的紋絲不亂的頭發(fā)上熙揍,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天昙衅,我揣著相機(jī)與錄音今妄,去河邊找鬼。 笑死赁炎,一個(gè)胖子當(dāng)著我的面吹牛是尖,可吹牛的內(nèi)容都是我干的意系。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼饺汹,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蛔添!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起兜辞,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤迎瞧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后弦疮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體夹攒,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜘醋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年胁塞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片压语。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡啸罢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胎食,到底是詐尸還是另有隱情扰才,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布厕怜,位于F島的核電站衩匣,受9級(jí)特大地震影響蕾总,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜琅捏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一生百、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柄延,春花似錦蚀浆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至滤奈,卻和暖如春摆昧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背僵刮。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工据忘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人搞糕。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓勇吊,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親窍仰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子汉规,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

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