OC底層原理(五):Category實(shí)現(xiàn)原理

平時(shí)我們應(yīng)該會(huì)比較常用分類,今天我們直接分析Category的本質(zhì)原理鸯隅,分析過(guò)后應(yīng)該對(duì)于分類的大部分問(wèn)題都能有一個(gè)自信的答案磅摹。

Category本質(zhì)

我們已經(jīng)OC中類的本質(zhì)都是結(jié)構(gòu)體户誓,分類也不例外幕侠,也是結(jié)構(gòu)體晤硕,直接上源碼結(jié)構(gòu)

struct category_t {
    const char *name; //類的名字
    classref_t cls;   //類
    struct method_list_t *instanceMethods; //實(shí)例方法的列表
    struct method_list_t *classMethods;//類方法的列表
    struct protocol_list_t *protocols; //協(xié)議的列表
    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的真正結(jié)構(gòu),我們會(huì)發(fā)現(xiàn)它只有屬性列表舰褪,沒(méi)有成員變量占拍。那么分類他的實(shí)現(xiàn)原理是什么呢?我們看下它編譯之后的樣子表牢。
先創(chuàng)建這樣一個(gè)分類

//Person+AddCategory.h
@interface Person (AddCategory)<NSCopying>

@property(nonatomic, assign)int age;
- (void)eat;
- (void)run;
+ (void)live;
@end

//Person+AddCategory.m
@implementation Person (AddCategory)
- (void)eat{
    NSLog(@"category-eat");
}
- (void)run{
    NSLog(@"category-run");
}
+ (void)live{
    NSLog(@"category-live");
}
@end

我們通過(guò)clang編譯一下這個(gè).m文件

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+AddCategory.m

在生產(chǎn)的.cpp文件中崔兴,我們可以找到這些代碼
和源碼中看到類似的結(jié)構(gòu)體蛔翅,這里只有主要的方法、協(xié)議折汞、屬性列表等信息

image.png

再向下看_method_list_t類型的結(jié)構(gòu)體爽待,下面={}中的結(jié)構(gòu)和上面的定義結(jié)構(gòu)一致翩腐,并且可以在里面看到我們?cè)贑ategory里面實(shí)現(xiàn)的對(duì)象方法茂卦。
image.png

還有一個(gè)_method_list_t等龙,這里是類方法
image.png

下面是協(xié)議列表:
image.png

然后是屬性列表:
image.png

后面是對(duì)一個(gè)新的_category_t類型的_OBJC_$_CATEGORY_Person_$_AddCategory結(jié)構(gòu)體賦值蛛砰,與_category_t結(jié)構(gòu)體結(jié)構(gòu)一致
image.png

最后泥畅,編譯器在'__DATA'段下的__objc_catlist section里保存了一個(gè)大小為1的category_t的數(shù)組L_OBJC_LABEL_CATEGORY$(如果有多個(gè)category,會(huì)生成對(duì)應(yīng)長(zhǎng)度的數(shù)組)柑贞,用于運(yùn)行期category的加載。后面我們分析Category在運(yùn)行時(shí)的加載棠众。
image.png

Category加載

為了了解清楚分類在runtime到底是怎么加載的摄欲,我們直接看runtime源碼疮薇,runtime初始化入口


image.png

方法調(diào)用順序:進(jìn)入map_images-->map_images_nolock-->_read_images
在_read_images方法里看到注釋有 // Discover categories.
這里代碼太長(zhǎng)按咒,不好截圖(其實(shí)就是懶)

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

    ts.log("IMAGE TIMES: discover categories");

在這段代碼中可以看到有獲取分類列表catList,然后遍歷里面的分類category_t *cat,然后關(guān)鍵位置智袭,對(duì)象方法remethodizeClass(cls);類方法remethodizeClass(cls->ISA());
都是remethodizeClass()方法掠抬,我們?cè)倏催@里的實(shí)現(xiàn)


image.png

再看attachCategories方法

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
    //為方法列表两波、屬性列表腰奋、協(xié)議列表分配內(nèi)存    
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--) {//注意這里是倒敘遍歷cat
        auto& entry = cats->list[i];
         //這里是給類中添加分類的對(duì)象方法還是元類中添加分類的類方法,都是走這里嘀倒,我們暫時(shí)認(rèn)為是給類中添加對(duì)象方法
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        //把分類中的對(duì)象方法列表放到mlists中
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        //把分類中的屬性列表放到proplist中
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
         //把分類中的協(xié)議列表放到protolist中
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
    //之前分析類結(jié)構(gòu)的時(shí)候的class_rw_t結(jié)構(gòu)體测蘑。
    auto rw = cls->data();
     //把mlists附加到class_rw_t中的methods上康二,attachLists(mlists, mcount)方法
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    //把proplists附加到class_rw_t中的properties上赠摇,attachLists(proplists, propcount)方法
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
     //把protolists附加到class_rw_t中的protocols上浅蚪,attachLists(protolists, protocount)方法
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

這里總結(jié):就是獲取分類列表烫罩,然后倒敘遍歷取出分類贝攒,然后將所有分類中的方法列表时甚、屬性列表、協(xié)議列表放到一個(gè)數(shù)組(二維數(shù)組:mlist梨熙、proplists刀诬、protolosts)中陕壹。
然后通過(guò)再通過(guò)對(duì)應(yīng)結(jié)構(gòu)的.attachLists方法附加到原來(lái)的類里的class_rw_t結(jié)構(gòu)體的對(duì)應(yīng)列表中。

這里還有比較關(guān)鍵的一點(diǎn)嘶伟,attachLists方法的實(shí)現(xiàn)又碌,看源碼
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;  //類中原數(shù)組,長(zhǎng)度
            uint32_t newCount = oldCount + addedCount; //加上新數(shù)組長(zhǎng)度(分類中的)
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); //重新分配空間
            array()->count = newCount;
            //關(guān)鍵,內(nèi)存移動(dòng)耽装,將原數(shù)組array()->lists(內(nèi)存起始位置)這塊內(nèi)存(大小是oldCount * sizeof(array()->lists[0]))移動(dòng)到array()->lists + addedCount這個(gè)位置掉奄。
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            //關(guān)鍵凤薛,內(nèi)存拷貝,將分類添加的列表數(shù)組addedLists(大小是addedCount * sizeof(array()->lists[0]))copy到array()->lists這個(gè)位置速兔。
            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(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
  • memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));

array()->lists是原列表數(shù)組涣狗,addedLists是分類添加的列表數(shù)組。首先是根據(jù)addedLists擴(kuò)容穗熬,然后把原列表移動(dòng)到列表最后丁溅,然后將addedLists添加到列表開(kāi)頭位置窟赏。

  • 這也就是為什么同樣的方法名,分類的方法會(huì)覆蓋原來(lái)類的方法(PS:并不是真正的覆蓋棍掐,只是分類方法在原來(lái)類的方法列表的前面拷况,在查找方法時(shí),先查找到分類的方法就調(diào)用了最疆,原來(lái)類的方法就不會(huì)被調(diào)用了)蚤告。
  • 那么問(wèn)題又來(lái)了杜恰,多個(gè)分類分同樣方法先調(diào)用誰(shuí)的?我們注意一下分類在編譯的時(shí)候編譯完成就加入到catList中了舔涎,但是之前在遍歷分類的時(shí)候是倒敘遍歷的逗爹,所以也就是說(shuō)后編譯的分類最后attachCategories中組裝的時(shí)候會(huì)在最前面,這也就可以解答這個(gè)問(wèn)題挟冠。
  • 最后袍睡,編譯順序是什么樣的呢?就是我們那在build Phases 中compile Source文件從上到下的順序控淡。
本來(lái)是有面試題的,后來(lái)發(fā)現(xiàn)了解這些后面試題就顯得比較簡(jiǎn)單辫诅,有其他問(wèn)題大家再一起討論吧竹伸。有不對(duì)的請(qǐng)指出簇宽,一起學(xué)習(xí)魏割,共勉!

文章編寫(xiě)參考MJ課程和iOS底層原理總結(jié) - Category的本質(zhì)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拜银,一起剝皮案震驚了整個(gè)濱河市尼桶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泵督,老刑警劉巖庶喜,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異秩冈,居然都是意外死亡斥扛,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門芬失,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)麸折,“玉大人粘昨,你說(shuō)我怎么就攤上這事窜锯∶” “怎么了馁启?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)惯疙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)对碌,這世上最難降的妖魔是什么蒿偎? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任诉位,我火速辦了婚禮,結(jié)果婚禮上叁丧,老公的妹妹穿的比我還像新娘椿息。我一直安慰自己,他們只是感情好条舔,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布孟抗。 她就那樣靜靜地躺著钻心,像睡著了一般。 火紅的嫁衣襯著肌膚如雪摊沉。 梳的紋絲不亂的頭發(fā)上痒给,一...
    開(kāi)封第一講書(shū)人閱讀 52,584評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音姜贡,去河邊找鬼棺棵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛母怜,可吹牛的內(nèi)容都是我干的棒动。 我是一名探鬼主播船惨,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼粱锐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼扛邑!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起恶座,我...
    開(kāi)封第一講書(shū)人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤跨琳,失蹤者是張志新(化名)和其女友劉穎桐罕,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體溅潜,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡滚澜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年设捐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片巴碗。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡橡淆,死狀恐怖母赵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情师倔,我是刑警寧澤周蹭,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布凶朗,位于F島的核電站,受9級(jí)特大地震影響搓萧,放射性物質(zhì)發(fā)生泄漏宛畦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望斯够。 院中可真熱鬧读规,春花似錦、人聲如沸铃在。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)揣炕。三九已至畸陡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間曹动,已是汗流浹背牲览。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工第献, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赊级。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像橡伞,于是被迫代替她去往敵國(guó)和親兑徘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

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