hello category

Litt1er發表於2017-12-25

事由:之前去面試,然後面試官問了我一些關於runtime的用法,我有說到Method Swizzling。通過在categoryload中去修改我們呼叫的方法,來達到全域性修改的目的。隨後面試官問到關於category的實現,哇! 尷尬,我好像從來沒有想過這個問題。現在有時間就給整理一下,水平有限,肯定會有很多不足。希望大家多多指點!多謝 zzz


categoryObjective-C 2.0之後新增的語言特性. 一般我們使用它有以下兩種場景

  • 給系統類新增方法和屬性
  • 通過組合的設計模式把類的實現分開成多個category在幾個不同的檔案裡面
    • 可以減少單個檔案的體積
    • 可以把不同的功能組織到不同的category
    • 可以由多個開發者共同完成一個類
    • 可以只載入自己想要的category,達到業務分離

關於問題(因為確實不知道該從什麼地方開始看起,所以強迫試的給自己定了幾個問題。讓自己去弄明白)

  • category是什麼東西
  • category是怎樣載入的
  • category方法為什麼可以覆蓋宿主類的方法
  • category的屬性跟方法是怎麼新增到宿主類的
  • category為什麼可以新增屬性,方法,協議。缺不能新增成員變數

category是什麼東西

objc所有類和物件都是c結構體,category也一樣。我們可以通過clang去看一下

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_t裡面有名字、宿主類的物件、例項方法列表、類方法列表、協議方法列表、屬列表性。可以看到是沒有成員變數列表的,因為category是依賴runtime的,而在執行時物件的記憶體佈局已經確定,如果新增例項變數就會破壞類的內部佈局,這對編譯型語言來說是災難性的。這就是為什麼我們沒有在_category_t裡面找到成員變數列表和category不可以新增成員變數的原因


category是怎樣載入的

上面我們提到過category是依賴runtime的。那我們來看一下runtime的載入過程。下面用到的runtime原始碼都來自於點這! 我下載的是723最新的版本

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();
    lock_init();
    exception_init();
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
複製程式碼

開始是一些初始化方法

map_images方法表示將檔案中的二進位制檔案對映到記憶體,category被新增到宿主類就發生在這個方法裡面。我們來看一下這個方法實現

void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    // 加鎖操作 保證在對映到dyld過程呼叫ABI是安全的
    rwlock_writer_t lock(runtimeLock);
    //函式在加鎖後就轉向了 map_images_nolock 函式
    return map_images_nolock(count, paths, mhdrs);
}
複製程式碼

map_images_nolock方法程式碼太長,我就不粘過來了。它主要做的操作是在函式中,它檢查傳入的每個 image,如果 image有需要的資訊,就將它記錄在hList中,並將hCount 加一,最終判斷 hCount>0來呼叫_read_images讀取 image 中的資料 。


我們再來看_read_images,方法有點長。我給跟category相關程式碼擷取出來了

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;
            // 從這開始,正式對category開始處理
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                //為類新增未依附的分類,把Category和類關聯起來
                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)) 
            {
                //為類新增未依附的分類,把Category和metaClass關聯起來。因為類方法是存在元類中的
                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來呼叫category的幕後大佬----attachCategories方法,從名字就可以看出來它的作用,新增category


plz show me the code !!!

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
    /*
     #warning attachCategories
     
     category的載入順序是通過編譯順序決定的
     
     這樣倒序遍歷,保證先將陣列內元素(category)的方法從後往前新增到新陣列
     
     這樣編譯在後面的category方法會在陣列的前面
     
     */
    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);
}
複製程式碼

得到重新組合的陣列在呼叫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;
        
        //C 庫函式 void *realloc(void *ptr, size_t size) 嘗試重新調整之前呼叫 malloc 或 calloc 所分配的 ptr 所指向的記憶體塊的大小
        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
        array()->count = newCount;
        /*
         1.memmove
         
         函式原型:void *memmove(void *dest, const void *source, size_t count)
         
         返回值說明:返回指向dest的void *指標
         
         引數說明:dest,source分別為目標串和源串的首地址。count為要移動的字元的個數
         
         函式說明:memmove用於從source拷貝count個字元到dest,如果目標區域和源區域有重疊的話,memmove能夠保證源串在被覆蓋之前將重疊區域的位元組拷貝到目標區域中。
         
         
         
         2.memcpy
         
         
         
         函式原型:void *memcpy(void *dest, const void *source, size_t count);
         
         返回值說明:返回指向dest的void *指標
         
         函式說明:memcpy功能和memmove相同,但是memcpy中dest和source中的區域不能重疊,否則會出現未知結果。
 
         3.兩者區別
         
         函式memcpy()   從source  指向的區域向dest指向的區域複製count個字元,如果兩陣列重疊,不定義該函式的行為。
         而memmove(),如果兩函式重疊,賦值仍正確進行。
         
         memcpy函式假設要複製的記憶體區域不存在重疊,如果你能確保你進行復制操作的的記憶體區域沒有任何重疊,可以直接用memcpy;
         如果你不能保證是否有重疊,為了確保複製的正確性,你必須用memmove。
         */
        
        /*
         這樣就完成將category方法列表裡面的方法 加到 class的方法列表裡面而且是前面。等到我們再去呼叫class的方法時候,我們通過去遍歷class的方法列表去查到SEL,找到就會呼叫相應方法。由於category的方法在前面---導致所以會覆蓋宿主類本來的方法(這就是為什麼category方法的優先順序高於宿主類方法)
         屬性和協議同理!!!!!!!!!!!
         */
        
        //相當於給array()->lists 重新放在起始地址 = array()->lists的起始地址 + addedCount
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
        
        //相當於給addedLists 這個category新陣列加到array()->的起始地址
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
        
    }
複製程式碼

以上應該可以回答,之前提的所有問題。如有疑問,可以聯絡我。




筆者是一個剛入門iOSer

這次關於category的管中窺豹,一定有很多的不足,希望大家不吝賜教!

有任何問題可以留言,或者直接聯絡QQ:346658618

希望可以相互學習,一起進步!