iOS Category的使用及原理

??Category是我們在開發(fā)中經(jīng)常用到的,它可以在我們不改變原有類的前提下來動(dòng)態(tài)地給類添加方法,通過這篇文章赠潦,我們一起來了解一下Category涉兽。
下面我們列一下本文目錄招驴,以方便了解本文的主要內(nèi)容。

  • Category的介紹
  • Category中的數(shù)據(jù)結(jié)構(gòu)
  • Category的原理
  • Category的使用
  • Category和Extension的區(qū)別

Category的介紹

??Category是Objective-C 2.0之后添加的語言特性枷畏,它可以在不改變或不繼承原類的情況下别厘,動(dòng)態(tài)地給類添加方法。我們平常說的分類或者類別就是說的Category拥诡。

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

Category定義的源碼我們可以在runtime的源碼中找到,我們先來看一下Category的定義:

///runtime.h
/// An opaque type that represents a category.
typedef struct objc_category *Category;
struct objc_category {
    ///Category名稱
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    ///類名
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    ///實(shí)例方法列表
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    ///類方法列表
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    ///協(xié)議列表
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}    

??通過定義我們可以看到触趴,Category是指向objc_category結(jié)構(gòu)體的指針,而objc_category結(jié)構(gòu)體中包含了當(dāng)前Category的名稱(category_name)渴肉、類名稱(class_name)冗懦、實(shí)例方法列表(instance_methods)、類方法列表(class_methods)仇祭、協(xié)議列表(protocols)披蕉,我們看到objc_category結(jié)構(gòu)體的定義中并沒有屬性列表,這也就是為什么我們用Category不能給類添加實(shí)例變量的原因乌奇。

Category的原理

??Category既然可以動(dòng)態(tài)的給類添加方法嚣艇,那么它的方法又是在什么時(shí)候添加的?把方法添加到哪里面去了呢华弓?我們在調(diào)用Category中方法的時(shí)候又是怎樣調(diào)用的呢食零?下面我們通過分析Category的源碼來看一下它是怎樣運(yùn)行的。
??因?yàn)槲覀兪窃趓untime的源碼中找到Category的源碼的寂屏,那么我們猜想Category中的方法是不是在運(yùn)行時(shí)添加的呢贰谣?下面我們看一下它的源碼:
Category在objc-runtime-new.h中的定義是category_t結(jié)構(gòu)體娜搂。在查找Category執(zhí)行時(shí)后我并不知道該如何查起,所以我就在objc-runtime-new.mm搜了一下category_t吱抚,我在2703行發(fā)現(xiàn)這樣一段代碼百宇,并有說明:

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

通過注釋我們知道在這發(fā)現(xiàn)了categories,當(dāng)時(shí)我覺得這個(gè)方法應(yīng)該就是秘豹,然后我就看了一下這個(gè)方法携御,發(fā)現(xiàn)這正是Category中的方法添加。在這里也給大家提供一個(gè)比較笨的方法既绕,當(dāng)我們查看源碼時(shí)啄刹,只知道方法或者結(jié)構(gòu)體的定義時(shí),可以在當(dāng)前的類中搜索凄贩,由于C語言的方法定義問題(方法的聲明必須在調(diào)用之前誓军,否則會(huì)因?yàn)闆]定義方法而報(bào)錯(cuò),這里它并不像我們OC中的方法定義一樣疲扎,可以在任意地方定義昵时,在任意地方調(diào)用,C語言的方法定義必須在調(diào)用之前定義好椒丧,否則會(huì)提示方法沒有定義而報(bào)錯(cuò))壹甥,我們都可以很容易的在當(dāng)前文件中找到,而且源碼中的注釋寫的也很明白壶熏。下面我們分析一下這段代碼:

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

首先我們看到的是一個(gè)for循環(huán)句柠,而for循環(huán)的單次表達(dá)式、表達(dá)式和末尾循環(huán)體是一個(gè)EACH_HEADER宏久橙,我們找到該宏的定義:

#define EACH_HEADER \
    hIndex = 0;         \
    hIndex < hCount && (hi = hList[hIndex]); \
    hIndex++

這里hCounthList都是調(diào)用_read_images該方法時(shí)傳過來的參數(shù)俄占,hi是名稱為header_info結(jié)構(gòu)體管怠,通過定義我們大約可以猜測出來淆衷,這是在遍歷類的頭文件,而hi中就是類的頭文件的信息渤弛。然后我們通過_getObjc2CategoryList方法獲取到hicategory_t列表(里面包含了當(dāng)前類的所有category_t)和長度祝拯。

category_t **catlist = 
            _getObjc2CategoryList(hi, &count);

拿到列表和長度后下面又是通過for循環(huán)來進(jìn)行遍歷,獲取到每一個(gè)category_t,并且根據(jù)category_tcls指針來獲取到對應(yīng)的類:

category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);

獲取到category_t和對應(yīng)的類后她肯,通過addUnattachedCategoryForClass方法將Category和類先關(guān)聯(lián)起來(這里只是關(guān)聯(lián)起來佳头,并沒有做其他的操作),下面通過remethodizeClass方法來整理相應(yīng)的類:

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

remethodizeClass方法中晴氨,我們看見通過unattachedCategoriesForClass方法將剛才關(guān)聯(lián)的類的Category獲取到康嘉,獲取到category_list類型的列表后(category_list類型的列表包含了當(dāng)前類所有的Category),執(zhí)行了attachCategories方法整理類的方法籽前、屬性和協(xié)議列表:

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

    // fixme rearrange to remove these intermediate allocations
    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) {
            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;
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    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);
}

執(zhí)行attachCategories首先執(zhí)行下列代碼重新分配分類的內(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));

分配完內(nèi)存地址后就開始遍歷所有的分類亭珍,將每一個(gè)分類的方法敷钾、屬性和協(xié)議添加到對應(yīng)的mlistsproplistsprotolists中肄梨。添加完成后通過data()方法來拿到類對象的class_rw_t結(jié)構(gòu)體類型的rw阻荒,之后通過調(diào)用rw中的方法列表、屬性列表和協(xié)議列表的attachLists函數(shù)众羡,將所有分類的方法侨赡、屬性和協(xié)議列表數(shù)組添加進(jìn)去。
下面我們來看一下attachLists中做了什么:

    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]));
        }
    }
addLists:調(diào)用方法時(shí)傳過來的粱侣,也就是要添加的分類的方法或?qū)傩粤斜?/h5>
array()->lists:類中原有的方法或?qū)傩粤斜?/h5>

下面我們看一下方法中的兩個(gè)函數(shù):memmovememcpy
這兩函數(shù)都是C語言中的函數(shù)庫羊壹,作用都是拷貝一定長度的內(nèi)存內(nèi)容,其長度由第三參數(shù)決定甜害,他們兩個(gè)唯一的區(qū)別是:當(dāng)內(nèi)存發(fā)生局部重疊時(shí)舶掖,memmove能夠保證拷貝結(jié)果的正確行,而memcpy不能保證拷貝結(jié)果是正確的(在這里這兩個(gè)方法就不多介紹了有興趣的同學(xué)可以參考一下這里尔店,關(guān)于memmove 和 memcpy的區(qū)別)眨攘。
??通過上面兩個(gè)方法我們就把Category里面的方法放到了類中原方法的前面,所以當(dāng)我們調(diào)用的時(shí)候會(huì)優(yōu)先調(diào)用Category中的方法嚣州。(注:有些人說Category里面的方法把類中原有的方法覆蓋了鲫售,其實(shí)這是錯(cuò)誤的。Category中的方法并沒有覆蓋類中原有的方法该肴,只是Category中的方法在類中原有的方法前面情竹,當(dāng)Runtime通過方法名查找的時(shí)候找到第一個(gè)方法就去執(zhí)行了它的實(shí)現(xiàn),并沒有繼續(xù)往下查找匀哄,關(guān)于方法的執(zhí)行可以參考這里)秦效。我們可以通過以下代碼來調(diào)用類中原來的方法:

///Cat為自己創(chuàng)建的類,在類中聲明一個(gè)sleep方法涎嚼,創(chuàng)建一個(gè)cat的Category阱州,聲明并實(shí)現(xiàn)sleep方法
Class currentClass = [Cat class];
    Cat *cat = [[Cat alloc] init];
    [cat sleep];
    if (currentClass) {
        unsigned int methodCount;
        Method *methodList = class_copyMethodList(currentClass, &methodCount);
        IMP lastImp = NULL;
        SEL lastSel = NULL;
        for (NSInteger i = 0; i < methodCount; i++) {
            Method method = methodList[i];
            NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method))
                                                      encoding:NSUTF8StringEncoding];
            if ([@"sleep" isEqualToString:methodName]) {
                lastImp = method_getImplementation(method);
                lastSel = method_getName(method);
            }
        }
        typedef void (*fn)(id,SEL);
        
        if (lastImp != NULL) {
            fn f = (fn)lastImp;
            f(cat,lastSel);
        }
        free(methodList);
    }

??以上就是Category的實(shí)現(xiàn)原理。通過這些我們也就了解了一些法梯,Category中的屬性苔货、方法、協(xié)議是在運(yùn)行時(shí)被添加到了類的屬性列表立哑、方法列表夜惭、協(xié)議列表中,既然它里面的方法是被添加到了類的方法列表中铛绰,那么它里面方法的調(diào)用和類中方法的調(diào)用是一樣的诈茧。

Category的使用

  • 添加方法
  • 聲明私有方法
  • 分解體積龐大的類文件
    以上是Apple推薦的使用方法,當(dāng)然還有一些人開發(fā)出了其他的用法捂掰,比如:模擬多繼承敢会、把framework私有方法公開镊叁,感興趣的朋友可以去了解一下。

Category和Extension的區(qū)別

  • Category中原則上只能增加方法走触,不能增加屬性(通過Runtime也可以實(shí)現(xiàn))晦譬;Extension既可以增加方法,還可以增加實(shí)例變量(該實(shí)例變量和方法默認(rèn)是私有的互广,只能在本類中調(diào)用)
  • Category中的方法是在運(yùn)行時(shí)決議的敛腌,沒有實(shí)現(xiàn)也可以運(yùn)行,而Extension中的方法是在編譯器檢查的惫皱,沒有實(shí)現(xiàn)會(huì)報(bào)錯(cuò)
  • Category可以給任意類添加方法像樊,而Extension的添加必須有這個(gè)類的源碼,對于一些系統(tǒng)類旅敷,如NSString類是無法添加Extension的生棍,但是可以添加Category。

結(jié)束

??以上就是有關(guān)Category的原理和使用媳谁,Category還是基于在Runtime上實(shí)現(xiàn)的涂滴,如果沒有Runtime的支持Category就不能夠?qū)崿F(xiàn)。Category在我們開發(fā)中的使用還是很多的晴音,大家可以通過源碼去學(xué)習(xí)研究柔纵。
??文章若有不足之處還請不吝賜教,大家互相學(xué)習(xí)锤躁。如果您覺得我的文章有用搁料,點(diǎn)一下喜歡就可以了哦。

參考文章

iOS底層原理總結(jié) - Category的本質(zhì)
iOS-分類(Category)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末系羞,一起剝皮案震驚了整個(gè)濱河市郭计,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌椒振,老刑警劉巖昭伸,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異杠人,居然都是意外死亡勋乾,警方通過查閱死者的電腦和手機(jī)宋下,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門嗡善,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人学歧,你說我怎么就攤上這事罩引。” “怎么了枝笨?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵袁铐,是天一觀的道長揭蜒。 經(jīng)常有香客問我,道長剔桨,這世上最難降的妖魔是什么屉更? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮洒缀,結(jié)果婚禮上瑰谜,老公的妹妹穿的比我還像新娘。我一直安慰自己树绩,他們只是感情好萨脑,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著饺饭,像睡著了一般渤早。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瘫俊,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天鹊杖,我揣著相機(jī)與錄音,去河邊找鬼扛芽。 笑死仅淑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的胸哥。 我是一名探鬼主播涯竟,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼空厌!你這毒婦竟也來了庐船?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤嘲更,失蹤者是張志新(化名)和其女友劉穎筐钟,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赋朦,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡篓冲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宠哄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片壹将。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖毛嫉,靈堂內(nèi)的尸體忽然破棺而出诽俯,到底是詐尸還是另有隱情,我是刑警寧澤承粤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布暴区,位于F島的核電站闯团,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏仙粱。R本人自食惡果不足惜房交,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望伐割。 院中可真熱鬧涌萤,春花似錦、人聲如沸口猜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽济炎。三九已至川抡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間须尚,已是汗流浹背崖堤。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留耐床,地道東北人密幔。 一個(gè)月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像撩轰,于是被迫代替她去往敵國和親胯甩。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

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