iOS - Category本質探究

iOS - Category

1日缨、Category簡介

Category是Objective-C 2.0之后添加的語言特性陡蝇。

Category可以做什么:
  1. 給目標類添加方法元潘。
  2. 將一個類的實現拆分成多個獨立的源文件文件筏餐。
  3. 聲明私有的方法汽纠; (模擬多繼承,framework私有方法公開)
Category的優(yōu)點:

? 可以減少單個文件的體積丧叽;
? 可以把不同功能的組織到不同的category中骚灸;
? 可以按需求加載想要的category;

Category的缺點:

? Category 非常強大,所以一旦誤用就很可能會造成非常嚴重的后果驴一。

? 覆寫系統(tǒng)類的方法休雌,不管在任何情況下,切記一定不要這么做肝断。

2杈曲、Category與Extension

  1. Extension在編譯期決議,它就是類的一部分胸懈,在編譯期和頭文件里的@interface以及實現文件里的@implement一起形成一個完整的類担扑,它伴隨類的產生而產生,亦隨之一起消亡趣钱。Extension一般用來隱藏類的私有信息涌献,你必須有一個類的源碼才能為一個類添加Extension,所以你無法為系統(tǒng)的類比如NSString添加Extension首有。

  2. Category則完全不一樣燕垃,它是在運行期決議的。

  3. 就Category和Extension的區(qū)別來看绞灼,我們可以推導出一個明顯的事實利术,Extension可以添加實例變量,而Category是無法添加實例變量的低矮。

3印叁、Category本質

在runtime層,category的結構體category_t(在objc-runtime-new.h中可以找到此定義):(源碼網址

struct category_t {

  struct property_list_t *instanceProperties; 
    const char *name; // category的名字
    classref_t cls;
    struct method_list_t *instanceMethods; // category中所有給類添加的實例方法的列表
    struct method_list_t *classMethods; // category中所有添加的類方法的列表
    struct protocol_list_t *protocols; // category實現的所有協(xié)議的列表
    struct property_list_t *instanceProperties; // category中添加的所有屬性
    // 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;
    }

};

Category編譯之后的底層結構是struct category_t军掂,里面存儲著分類的對象方法轮蜕、類方法、屬性蝗锥、協(xié)議信息
在程序運行的時候跃洛,runtime會將Category的數據,合并到類信息中(類對象终议、元類對象中)汇竭。

那我們都知道一個類對象可以寫多個分類穴张,那這些分類是如何加載的呢细燎?

1、Objective-C的運行是依賴OC的runtime的皂甘,而OC的runtime和其他系統(tǒng)庫一樣,是OS X和iOS通過dyld動態(tài)加載的偿枕。

對于OC運行時璧瞬,入口方法如下(在objc-os.mm文件中):

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();
    runtime_init();
    exception_init();
    cache_init();
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

在這個運行時的初始化方法中我們可以看到&map_images户辫,這是一個函數的地址。我們進去看就會發(fā)現其實調用的是如下代碼:

void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}

map_images 方法中其實是調用的map_images_nolock方法,而map_images_nolock這個方法中又會調用一個_read_images的方法酷誓,

_read_images會讀取一些模塊玫氢,比如一些類信息漾峡、分類信息這些東西槽袄。

// 這里加載分類,調用方法(跟之前的源碼是不一樣迂苛,這是寫成了一個方法。)
// Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }

在load_categories_nolock方法中,跟之前實現不一致,之前可能是所有類別都不加判斷的添加铸豁,現在是有一些取舍魄幕,比如說類別的目標勒確實等等一些坛芽。

static void load_categories_nolock(header_info *hi) {
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();

    size_t count;
    auto processCatlist = [&](category_t * const *catlist) {
        for (unsigned i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
            locstamped_category_t lc{cat, hi};

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Ignore the category.
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class",
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category.
            if (cls->isStubClass()) {
                // Stub classes are never realized. Stub classes
                // don't know their metaclass until they're
                // initialized, so we have to add categories with
                // class methods or properties to the stub itself.
                // methodizeClass() will find them and add them to
                // the metaclass as appropriate.
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    objc::unattachedCategories.addForClass(lc, cls);
                }
            } else {
                // First, register the category with its target class.
                // Then, rebuild the class's method lists (etc) if
                // the class is realized.
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    if (cls->isRealized()) {
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }

                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                    if (cls->ISA()->isRealized()) {
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else {
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                    }
                }
            }
        }
    };

    processCatlist(_getObjc2CategoryList(hi, &count));
    processCatlist(_getObjc2CategoryList2(hi, &count));
}

我們重點關注 attachCategories方法,看一下這個方法的實現:

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     只有少數類在啟動期間有超過64個類別队丝。這使用了一個小堆棧靡馁,并避免了malloc。
     *
     *類別必須按正確的順序添加机久,即從后到前臭墨。為了完成分塊操作,我們從前向后迭代cats_list膘盖,向后構建本地緩沖區(qū)胧弛,并在塊上調用attachLists。attachLists將列表放在前面侠畔,因此最終結果將按照預期的順序進行结缚。
     
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    // 方法數組
    method_list_t   *mlists[ATTACH_BUFSIZ];
    // 屬性數組
    property_list_t *proplists[ATTACH_BUFSIZ];
    // 協(xié)議
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rwe = cls->data()->extAllocIfNeeded();
    /*
     
    這里之前是 while 循環(huán),倒序的软棺,現在是一個正序的
    while (i--) {
        
    }
    */
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];
        // 取出對應的方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
                // 將所有分類對象的方法红竭,附加到類對象方法列表中。
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            // 本次是從后往前添加 64 - 1
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) flushCaches(cls);
    }

    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

attachCategory做的工作相對比較簡單,把所有Category的方法茵宪、屬性最冰、協(xié)議數據,合并到一個大數組中后面參與編譯的Category數據稀火,會在數組的前面暖哨,然后轉交給了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;
            // array()->lists 原來的方法列表,將array()->lists向后移動addedCount
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            // addedLists 所有分類的方法列表凰狞,將addedLists 拷貝到array()->lists的頭位置
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));

attachLists:將合并后的分類數據(方法篇裁、屬性、協(xié)議)赡若,插入到類原來數據的前面

所以我們可以得出結論:

1.category的方法沒有“完全替換掉”原來類已經有的方法达布,也就是說如果category和原來類都有methodA,那么category附加完成之后斩熊,類的方法列表里會有兩個methodA;

2.category的方法被放到了新方法列表的前面往枣,而原來類的方法被放到了新方法列表的后面,這也就是我們平常所說的category的方法會“覆蓋”掉原來類的同名方法粉渠,這是因為運行時在查找方法的時候是順著方法列表的順序查找的分冈,它只要一找到對應名字的方法,就會罷休霸株,殊不知后面可能還有一樣名字的方法雕沉。

4、Category + load + initialize

+load 方法

+load方法會在runtime加載類去件、分類時調用坡椒。每個類、分類的+load尤溜,在程序運行過程中只調用一次倔叼。

load方法的調用順序:

1.先調用類的+load
按照編譯先后順序調用(先編譯,先調用)
調用子類的+load之前會先調用父類的+load
2.再調用分類的+load
按照編譯先后順序調用(先編譯宫莱,先調用)

大家思考一下丈攒,上面我們看了category 本質的時候發(fā)現,如果分類中方法名和類中方法名一樣授霸,不是只執(zhí)行分類的方法嗎巡验?為什么load 方法不是這樣?

同樣看源碼我們也能找到答案碘耳。這里就不看這個源碼了显设。給大家列出查看源碼的順序:

objc-os.mm

1、初始化方法_objc_init辛辨,查看load_images

2捕捂、call_load_methods方法

do {
        // 這里也可以看出先調用類的load 方法瑟枫,再調用分類的load 方法
        // 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);

3、call_class_loads绞蹦、call_category_loads

  // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        // 直接找到load 方法的地址
        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方法
        (*load_method)(cls, @selector(load));
    }
    

通過查看源碼我們可以得出:

+load方法是根據函數地址直接調用力奋,并不是經過objc_msgSend函數調用(消息機制調用,isa 找到類幽七,然后找到方法列表去遍歷)

+initialize 方法

+initialize方法會在類第一次接收到消息時調用。
調用順序
先調用父類的+initialize溅呢,再調用子類的+initialize
(先初始化父類澡屡,再初始化子類,每個類只會初始化1次)

上源碼調用順序:

1咐旧、objc-runtime-new.mm中class_getInstanceMethod驶鹉,尋找方法。

2铣墨、找到 lookUpImpOrForward方法室埋,initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass

判斷是否初始化過,保證只有第一次執(zhí)行伊约。

3姚淆、initializeNonMetaClass方法,callInitialize屡律,然后objc_msgSend腌逢。

* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first
*父類優(yōu)先初始化。
這也是一個遞歸實現超埋。

對比:

+initialize和+load的很大區(qū)別是搏讶,+initialize是通過objc_msgSend進行調用的,所以有以下特點:
1霍殴、如果子類沒有實現+initialize媒惕,會調用父類的+initialize(所以父類的+initialize可能會被調用多次)

? 子類的isa 指針找到元類對象沒有+initialize方法,然后superClass 中去找来庭。

2妒蔚、如果分類實現了+initialize,就覆蓋類本身的+initialize調用

+Load +initialize
調用時機 Runtime加載類巾腕、分類的時候調用 收到第一條消息時面睛,可能永遠不調用
調用方式 根據函數地址直接調用 通過objc_msgSend調用
調用順序 父類->子類->分類 父類->子類
是否需要顯式調用父類實現 1次 可能會調用多次
分類中的實現 類和分類都執(zhí)行 覆蓋類中的方法,只執(zhí)行分類的實現

5尊搬、Category 關聯(lián)對象

默認情況下叁鉴,因為分類底層結構的限制,不能添加成員變量到分類中佛寿。但可以通過關聯(lián)對象來間接實現幌墓。

// 項目寫法
- (BYLoadingView *)loadingView {
    BYLoadingView *loadingView = objc_getAssociatedObject(self, &kLoadingView);
    return loadingView;
}

- (void)setLoadingView:(BYLoadingView *)loadingView {
    objc_setAssociatedObject(self, &kLoadingView, loadingView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

關聯(lián)對象提供了以下API
添加關聯(lián)對象
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)

獲得關聯(lián)對象
id objc_getAssociatedObject(id object, const void * key)

移除所有的關聯(lián)對象
void objc_removeAssociatedObjects(id object)

我們思考一下:

但是關聯(lián)對象又是存在什么地方呢但壮? 如何存儲? 對象銷毀時候如何處理關聯(lián)對象呢常侣?

在objc-references.mm文件中有個方法_object_set_associative_reference:

我們可以看到所有的關聯(lián)對象都由AssociationsManager管理蜡饵,而AssociationsManager定義如下:

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;

public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }

    AssociationsHashMap &get() {
        return _mapStorage.get();
    }

    static void init() {
        _mapStorage.init();
    }
};

AssociationsManager中有一個AssociationsHashMap:

//AssociationsHashMap 中有一個 ObjectAssociationMap
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

//ObjectAssociationMap 中 ObjcAssociation
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;

// ObjcAssociation的定義如下
uintptr_t _policy;
id _value;

所以我們也可以得到:

關聯(lián)對象并不是存儲在被關聯(lián)對象本身內存中。

關聯(lián)對象存儲在全局的統(tǒng)一的一個AssociationsManager中胳施。

設置關聯(lián)對象為nil溯祸,就相當于是移除關聯(lián)對象。

 if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                object->setHasAssociatedObjects();
            }

            /* establish or replace the association */
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
如何銷毀某一個:

objc-runtime-new.mm中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;
}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末焦辅,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子椿胯,更是在濱河造成了極大的恐慌筷登,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哩盲,死亡現場離奇詭異前方,居然都是意外死亡,警方通過查閱死者的電腦和手機廉油,發(fā)現死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門惠险,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人娱两,你說我怎么就攤上這事莺匠。” “怎么了十兢?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵趣竣,是天一觀的道長。 經常有香客問我旱物,道長遥缕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任宵呛,我火速辦了婚禮单匣,結果婚禮上,老公的妹妹穿的比我還像新娘宝穗。我一直安慰自己户秤,他們只是感情好,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布逮矛。 她就那樣靜靜地躺著鸡号,像睡著了一般。 火紅的嫁衣襯著肌膚如雪须鼎。 梳的紋絲不亂的頭發(fā)上鲸伴,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天府蔗,我揣著相機與錄音,去河邊找鬼汞窗。 笑死姓赤,一個胖子當著我的面吹牛,可吹牛的內容都是我干的仲吏。 我是一名探鬼主播不铆,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼裹唆!你這毒婦竟也來了狂男?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤品腹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后红碑,有當地人在樹林里發(fā)現了一具尸體舞吭,經...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年析珊,在試婚紗的時候發(fā)現自己被綠了羡鸥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡忠寻,死狀恐怖惧浴,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情奕剃,我是刑警寧澤衷旅,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站纵朋,受9級特大地震影響柿顶,放射性物質發(fā)生泄漏。R本人自食惡果不足惜操软,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一嘁锯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧聂薪,春花似錦家乘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至笆载,卻和暖如春扑馁,著一層夾襖步出監(jiān)牢的瞬間涯呻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工腻要, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留复罐,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓雄家,卻偏偏與公主長得像效诅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子趟济,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355