Objective-C基礎之三(深刻理解Category)

什麼是Category?

category是Objective-C 2.0以後的語言特性,咱們一般稱之爲類別。他最主要的做用就是爲已有的類增長方法,屬性,協議等等,可是不能增長實例變量,固然,若是是增長屬性的話,只會增長屬性的聲明,並不會生成屬性的setter和getter方法。html

由於Category的多種特性,因此在開發過程當中Category的用法也多種多樣,好比在組件化中,咱們使用Category來區分各個組件的方法調用,典型的就是Casa大神的CTMediator,除此以外,咱們還使用Category來爲系統的類擴展出各類不一樣的工具方法,例如NSStringNSArrayNSDictionary等等,總之,Category在平常開發中不可或缺,那麼除了會使用Category以外,Category的本質究竟是什麼呢?Category究竟是如何爲類增長方法和協議的呢?經過Category增長的方法和協議是在什麼時機加入到原有類中的呢?下面咱們會經過閱讀源碼來一一解答這些問題。面試

Category的底層結構

經過xcrun來查看編譯以後的Category底層結構

咱們知道,一個類的對象方法和協議是存放在類對象的方法列表和協議列表中的,而類方法則是存放在元類對象的方法列表中,並且這些方法和協議等等都是在編譯時生成的,編譯完成以後會存放到內存當中,等待開發者使用。而經過Category增長的方法和協議並非在編譯期就生成,而是在運行時動態的合併到類對象中,咱們經過一個簡單的Demo來查看一下Category的底層結構。數組

首先,建立一個XLPerson對象,而後爲XLPerson對象增長一個Category,擴展出方法和屬性等等,具體代碼以下bash

#pragma mark - XLPerson
@interface XLPerson : NSObject

@end

@interface XLPerson (Test)


#pragma mark - XLPerson+Test
@property(nonatomic, copy)NSString *name;

- (void)run;
@end

@implementation XLPerson (Test)

- (void)run{
    NSLog(@"%s", __func__);
}

@end
複製代碼

而後經過如下命令,將XLPerson+Test.m文件文件重寫成XLPerson+Test.cpp文件,這個XLPerson+Test.cpp文件其實就至關於編譯以後的產物app

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc XLPerson+Test.m -o XLPerson+Test.cpp
複製代碼

_category_t

而後查看XLPerson+Test.cpp文件,能夠獲得Category在編譯以後的結構以下,結構體中存放着方法列表、屬性列表和協議列表等等iphone

struct _category_t {
	const char *name; //類名      
	struct _class_t *cls; //存放isa和superclass的結構體
	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;  //屬性列表
};
複製代碼

XLPerson+Test.cpp中,咱們還能夠找到一個名爲OBJC$CATEGORY_XLPerson$_Test的靜態結構體變量,它的成員以下:ide

static struct _category_t _OBJC_$_CATEGORY_XLPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"XLPerson",//類名,對應name
	0, // &OBJC_CLASS_$_XLPerson,對應cls
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_XLPerson_$_Test, //實例方法列表
	0,//類方法列表,此處未在Category中添加類方法,因此爲0
	0,//協議列表,此處未添加協議,因此爲0
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_XLPerson_$_Test,//屬性列表
};
複製代碼

因而可知在編譯完成以後,每個Category中的內容包括方法列表、屬性列表等等,都會存放到一個_category_t類型的結構體變量中,而不是在編譯時就直接合併到XLPerson類中去。函數

_method_list_t

而後咱們再以_method_list_t爲例,來窺探一下分類中實例方法的內部結構,以下工具

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method),方法的內存大小
	unsigned int method_count; //參數的個數
	struct _objc_method method_list[1];//方法的具體信息
} _OBJC_$_CATEGORY_INSTANCE_METHODS_XLPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"run", "v16@0:8", (void *)_I_XLPerson_Test_run}}
};
複製代碼

咱們在分類中添加的實例方法,存放在了_objc_method類型的結構體中,在看一下_objc_method的內部結構以下組件化

struct _objc_method {
	struct objc_selector * _cmd; //方法selector
	const char *method_type;    //返回值類型
	void  *_imp;                //方法實現的地址
};
複製代碼

再對比_method_list_t,能夠得出,分類中增長的實例方法,編譯以後會轉換成_objc_method類型,內部存放着selector,返回值類型和方法實現,而後將轉換後的_objc_method存放到結構體變量OBJC$CATEGORY_INSTANCE_METHODS_XLPerson$_Test中去。

由此咱們能夠得出結論,每一個分類在編譯完成以後都會生成一個_category_t類型的結構體變量,相似上文中的OBJC$CATEGORY_XLPerson$_Test,內部存放着咱們在分類中定義的方法列表、屬性列表和協議列表。

經過runtime源碼來查看編譯以後的Category底層結構

上文中,咱們是經過將代碼文件經過xcrun編譯成cpp文件,而後來查看Category的底層結構,這種方式生成的代碼僅供參考,真正的Category內部結構還得經過查看runtime源碼來進行學習。能夠經過objc4源碼地址下載最新的runtime源碼。

查看源碼中的objc_runtime_new.h文件就能夠看到Category的內部結構以下

struct category_t {
    const char *name; //類名
    classref_t cls;   //cls
    struct method_list_t *instanceMethods;//實例方法列表
    struct method_list_t *classMethods;   //類方法列表
    struct protocol_list_t *protocols;    //協議列表
    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的底層結構category_t和上文中咱們獲得的_category_t結構基本一致,這也驗證了咱們的觀點,在編譯以後,每一個Category確實會生成一個category_t類型的結構體。

Category的加載流程

Category在編譯時生成category_t類型的靜態變量,而後在運行時合併到類中。下面咱們就經過runtime的執行流程來查看Category的合併過程。

具體加載流程

  • 首先,在objc-os.mm中找到runtime的入口,也就是_objc_init函數,它內部會經過dyld去註冊images(模塊)
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);
}
複製代碼

_dyld_objc_notify_register函數中是直接經過map_images的內存地址調用map_images函數。

  • 查看map_images函數,內部調用map_images_nolock函數
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_nolock函數是用來執行全部類註冊和修復操做,並調用+load方法。,它內部會調用_read_iamges函數,核心代碼以下
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    ......
    
    for (EACH_HEADER) {
        //獲取項目中因此的Category,獲得一個二維數組
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();
        //遍歷二維數組,獲得每個category_t類型的結構體變量
        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            //經過cat->cls拿到當前Category所屬的類
            Class cls = remapClass(cat->cls);
            //判斷class是否存在
            bool classExists = NO;
            //若是Category中存在實例方法,協議或者是實例屬性
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                //將cls的未合併的全部Category存放到以cls爲key的一個映射表中去
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    //將Category中的方法、屬性、協議等等附加到cls的實例方法列表、實例屬性列表和協議列表中去
                    remethodizeClass(cls);
                    classExists = YES;
                }
            }
            //若是Category中存在類方法,協議或者是類屬性
            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                //將Category中的類方法、類屬性、協議等等附加到cls的元類的類方法列表、類屬性列表和協議列表中去
                    remethodizeClass(cls->ISA());
                }
            }
        }
    }
    ......
}

複製代碼

在內存中會維護着一張NXMapTable類型的映射表,他以cls爲key,category_list爲value。在編譯以後將全部未進行合併的Category存放到category_list,而後存放到映射表中。等到合適的時機,會將全部的分類取出,將其中的方法、屬性等等合併到類和元類中去。對應的addUnattachedCategoryForClass函數源碼以下

static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
                                          header_info *catHeader)
{
    runtimeLock.assertLocked();

    // DO NOT use cat->cls! cls may be cat->cls->isa instead
    NXMapTable *cats = unattachedCategories();
    category_list *list;

    list = (category_list *)NXMapGet(cats, cls);
    if (!list) {
        list = (category_list *)
            calloc(sizeof(*list) + sizeof(list->list[0]), 1);
    } else {
        list = (category_list *)
            realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
    }
    list->list[list->count++] = (locstamped_category_t){cat, catHeader};
    NXMapInsert(cats, cls, list);
}
複製代碼
  • 繼續查看上文中所提到的remethodizeClass函數,會發現remethodizeClass函數內部調用的attachCategories函數就是真正執行Category合併的核心代碼,以下重要的代碼能夠對照註釋進行查看。
//將方法列表、屬性列表、協議列表附加到類中去
//假設cats中的全部的類別都是按順序進行加載和排序的,最先裝載進內存的類別是第一個
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);
    //用來判斷是不是元類
    bool isMeta = cls->isMetaClass();

    //申請連續內存空間,建立一個二維數組,裏面存放着全部的method_list_t
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    //申請連續內存空間,建立一個二維數組,裏面存放着全部的property_list_t
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    //申請連續內存空間,建立一個二維數組,裏面存放着全部的protocol_list_t
    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;
    //獲取到category_list以後,經過逆序遍從來取出Category內部的方法、屬性和協議列表
    while (i--) {
        auto& entry = cats->list[i];
        //遍歷cls全部的category_t,將category_t中的method_list_t取出,存放到二維數組mlists中
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        // 將category_t中的property_list_t取出,存放到二維數組proplists中
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
        //將category_t中的protocol_list_t取出,存放到二維數組protolists中
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
    //拿到類對象cls的class_rw_t類型的成員data,它是可讀可寫的
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    //將方法列表合併到rw的方法列表中去,而且插入到表頭位置
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    //將屬性列表合併到rw的屬性列表中去,而且插入到表頭位置
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
    //將協議列表合併到rw的協議列表中去,而且插入到表頭位置
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}
複製代碼

此處須要注意的是,每一個類的Category附加的順序和Category裝載進內存的順序有關,最早裝載進內存的Category最後進行attach操做。至於class_rw_t和class_ro_t的區別可參考以前的文章。

  • 經過逆序遍歷獲取到的方法列表、屬性列表和協議列表,最後會經過attachLists函數合併到對應的class中去,核心代碼以下:
void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;
    //這裏以方法列表爲例
    //array()->lists表示原來類中的方法列表
    //addedLists表示全部Category中的方法列表
    if (hasArray()) {
        //獲取原來類中方法列表的長度
        uint32_t oldCount = array()->count;
        //獲得方法合併以後的新的數組長度
        uint32_t newCount = oldCount + addedCount;
        //給array從新分配長度爲newCount的內存空間
        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
        array()->count = newCount;
        //將原來array()->lists中的數據移動到數組中oldCount的位置
        //也就是至關於將array()->lists的數據在內存中日後移動了addedCount個位置
        memmove(array()->lists + addedCount, array()->lists,
                oldCount * sizeof(array()->lists[0]));
        //將Category中的方法列表copy到array()->lists中
        //而且是從數組的起始地址開始存放
        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]));
    }
}
複製代碼

下面以合併方法列表爲例,attachLists函數有兩個參數addedListsaddedCountaddedLists表示全部Category中的方法列表。addedCount表示新增長的方法個數。addedLists中存放的內容以下:

addedLists:[[method_t, method_t], 
             [method_t, method_t], 
             [method_t, method_t], 
             ......]
複製代碼

首先會經過原來類方法列表的長度和新添加方法列表的長度,獲得合併以後的數組大小,而後從新爲數組分配新的內存空間。而後調用memmove函數將原來array()->lists中的元素日後移動addedCount個位置,最後調用memcpy函數將addedLists中的元素複製到array()->lists中,從第0個位置開始存放。由此能夠看出,全部分類中的方法,在合併到類原來的方法列表中時,是插入到原來的數據以前。

大體流程圖以下:

總結

經過閱讀runtime的源碼,咱們能夠得出以下結論:

  • 程序在編譯完成以後,會將全部的Category轉換成category_t類型的結構體
  • 而後經過runtime加載某個類的全部Category數據,包括方法列表、屬性列表和協議列表
  • 把Category的方法列表、屬性列表和協議列表存放到一個大的數組中去,這裏須要注意的是,因爲是逆序遍歷,最早裝載進內存的Category數據會存放到數組的最後面。
  • 將合併後的分類數據(方法列表、屬性列表和協議列表)經過memmove函數和memcpy函數插入到類原來的數據以前。所以,若是類和它的分類用於相同名稱的方法,那麼只會調用分類中的方法,不會調用父類中的方法。

OC的方法調用核心其實就是消息發送機制,方法底層會轉換成objc_msgSend函數進行消息發送,若是當前類方法列表沒有找到方法,會經過isa指針到元類對象的方法列表中查找,若是尚未找到會經過superClass到父類的方法列表中查找。一旦找到會當即執行方法。所以,一旦類和分類中有相同方法名的方法,分類的方法會放在類方法列表的最前面,當查找方法時,會直接拿到分類的方法執行。

Category補充

memmove和memcpy

memmove和memcpy兩個函數的做用都是進行內存拷貝,惟一的區別就是當要拷貝的內存區域和目標內存區域有重疊部分的時候,memmove可以保證拷貝以後的結果是正確的,可是memcpy就不能保證拷貝以後的結果是正確的。

memmove

memmove的函數聲明以下

void *memmove(void *dest, const void *src, size_t n);
複製代碼

memmove() 函數從src內存中拷貝n個字節到dest內存區域,可是源和目的的內存能夠重疊,具體拷貝流程以下:

能夠看到,src和dest的區域存在重疊部分,而且dest的區域在src的後面,全部在執行memmove操做時,會從src的最後一個內存中的元素開始,依次日後挪動,因此挪動完成以後就完成了拷貝操做,並且結果是正確的。

memcpy

memcpy的函數聲明以下

void *memcpy(void *dest, const void *src, size_t n);
複製代碼

memcpy()函數從src內存中拷貝n個字節到dest內存區域,可是源和目的的內存區域不能重疊,具體拷貝流程以下

能夠看到src和dest的區域存在重疊部分,在執行memcpy操做時,會從src起始內存地址開始,依次向後copy,在圖中即表示爲將src中第一個元素拷貝到dest第一個地址中,將src第二個元素拷貝到dest的第二個地址中,因此就形成了覆蓋操做。

+load()方法

在OC中,+load()方法會在runtime加載類和分類到內存中的時候調用,並且每一個類或者分類的+load()方法只會調用一次。並且若是同時存在子類和分類的狀況下,會先調用父類的+load()方法,再調用子類的+load()方法,最後調用分類的+load()方法。下面咱們經過源碼來驗證這一結論。

  • 首先,咱們仍是同上文同樣,找到_objc_init函數,此處是runtime的入口函數,省略了部分代碼,只保留咱們須要的代碼。_objc_init函數中的load_images函數就是用來執行全部類的+load()方法。
void _objc_init(void)
{
    ......
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

複製代碼
  • 繼續查看load_images的源碼以下,經過註釋咱們能發現prepare_load_methods函數用來發現全部的+load()方法,而call_load_methods函數則用來調用全部的+load()方法。
void load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}
複製代碼

Discover load methods

  • 查看prepare_load_methods函數,發現它內部是經過調用_getObjc2NonlazyClassList函數來獲取到全部不是懶加載的類,因爲_getObjc2NonlazyClassList不開源,因此咱們猜想經過這個函數獲取到的classlist它的順序和類編譯的順序相同。
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        realizeClassWithoutSwift(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}
複製代碼
  • 當獲取到全部類的集合以後,經過遍從來調用schedule_class_load函數來遞歸調度類的+load方法和全部它的父類的+load方法,並且經過schedule_class_load(cls->superclass);這句能夠看出,在調度+load方法是,是先父類再子類的順序,以下
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
複製代碼
  • 查看add_class_to_loadable_list函數源碼,能夠發現經過遞歸來調度的類以及它的+load方法最後被添加到了一個全局的loadable_classes列表中去了,這個列表中存放的是全部須要被調用+load方法的類,全部的類都被包裝成了struct loadable_class類型的結構體。
struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method if (PrintLoading) { _objc_inform("LOAD: class '%s' scheduled for +load", cls->nameForLogging()); } if (loadable_classes_used == loadable_classes_allocated) { loadable_classes_allocated = loadable_classes_allocated*2 + 16; loadable_classes = (struct loadable_class *) realloc(loadable_classes, loadable_classes_allocated * sizeof(struct loadable_class)); } loadable_classes[loadable_classes_used].cls = cls; loadable_classes[loadable_classes_used].method = method; loadable_classes_used++; } 複製代碼

這裏有個注意點,由於是經過遞歸來添加,因此父類老是會優先調用add_class_to_loadable_list函數,所以在loadable_classes中,父類老是存放在最前面。並且經過查看結構體loadable_class能夠發現,它內部是直接存放了IMP類型的成員變量,也就是說直接保存的+load方法的內存地址,後續能夠直接經過內存地址來調用+load方法

  • 加載完全部類的+load方法以後,接下來會加載全部分類的+load方法,分類同類同樣,也是經過內部函數_getObjc2NonlazyCategoryList獲取到項目中全部的分類列表,而後經過遍歷調用add_category_to_loadable_list函數,將分類中的+load方法加到全局的list中去。以下
void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();
    ......
    method = _category_getLoadMethod(cat);
    
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}
複製代碼

此處將全部的分類以及它的+load方法封裝成了一個struct loadable_category類型的結構體,而後存放到全局的struct loadable_categories列表中去,實現方法和上文中類的方式相同,惟一不一樣的是Category的加載不是經過遞歸來進行的。

  • 此時在全局列表loadable_classesloadable_categories中就存放了全部須要調用+load方法的類和分類的信息,並且類和它的+load方法直接存放在了struct loadable_class中,而分類和它的+load方法則直接存放在了struct loadable_category中。

Call +load methods

通過prepare_load_methods函數以後,全部類和分類的+load方法分別存放到了loadable_classesloadable_categories中,以後經過調用call_load_methods函數對全部的+load方法進行調用

  • 查看call_load_methods的源碼,發現是經過call_class_loads來調用全部類的+load方法,經過call_category_loads函數來調用全部分類的+load方法
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;
    loadMethodLock.assertLocked();
    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;
    void *pool = objc_autoreleasePoolPush();

    do {
        // 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); objc_autoreleasePoolPop(pool); loading = NO; } 複製代碼

此處有個objc_autoreleasePoolPush()函數和objc_autoreleasePoolPop()函數,這兩個函數的做用其實就是建立一個自動釋放池autoreleasepool,有興趣的能夠查看自動釋放池的底層實現。

  • 首先查看call_class_loads函數的源碼,發現其實就是拿到上文所說的全局列表loadable_classes,而後經過遍歷拿到裏面的loadable_class,依次取出cls和+load方法的內存地址,直接調用+load方法
static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        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_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}
複製代碼
  • call_category_loads函數的實現方式和call_class_loads函數基本相同。

總結

  • +load方法是在runtime加載類、分類是進行調用的
  • 每一個類、分類的+load方法,在程序運行過程當中只會調用一次
  • 若是存在多個子類,則會先調用父類的+load方法,再調用子類的+load方法,子類+load方法的調用順序和子類的編譯順序相同,先編譯的子類優先調用
  • 若是同時存在多個分類和多個子類,那麼首先會調用父類的+load方法,再調用子類的+load方法,最後纔會調用分類的+load方法,多個分類+load方法的調用順序和編譯順序相同,先編譯的分類先調用。
  • 無論是類仍是分類,+load方法都是經過直接拿到方法的內存地址進行調用,而不是經過消息發送機制來調用

+initialize()方法

+initialize執行時機

+initialize方法會在類第一次接收到消息的時候調用,也就是說當咱們調用+initialize方法時,最後都會轉換成objc_msgSend(obj, @selector(initialize)),因此想要查看+initialize方法的調用流程,就能夠查看runtime進行方法查找的源碼。具體查看objc-runtime-new.mm下的class_getInstanceMethod函數,這個函數就是用來查找某一個類下的實例方法

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrNil(cls, sel, nil, 
                   NO/*initialize*/, NO/*cache*/, YES/*resolver*/);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}
複製代碼
  • 經過lookUpImpOrNil函數來查找類的方法列表,在函數內部又經過lookUpImpOrForward函數來進行方法查找
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}
複製代碼
  • 繼續查看lookUpImpOrForward函數的源碼,找到其中的核心代碼,若是函數參數initialize爲YES而且當前類沒有進行過初始化,則會調用initializeAndLeaveLocked函數。
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    ......
    //當參數initialize爲YES,而且cls沒有進行過Initialized操做
    if (initialize && !cls->isInitialized()) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }
    ......
}
複製代碼
  • 查看initializeAndLeaveLocked函數發現它的內部會調用initializeAndMaybeRelock函數來獲取cls的類對象,而且經過isRealized判斷當前類是否初始化。
static Class initializeAndMaybeRelock(Class cls, id inst,
                                      mutex_t& lock, bool leaveLocked)
{
    lock.assertLocked();
    assert(cls->isRealized());
    
    if (cls->isInitialized()) {
        if (!leaveLocked) lock.unlock();
        return cls;
    }
    Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);

    //isRealized()方法用來判斷當前類中cls->data是class_rw_t仍是class_ro_t
    //也就是判斷類是否被初始化
    if (nonmeta->isRealized()) {
        lock.unlock();
    } else {
        nonmeta = realizeClassMaybeSwiftAndUnlock(nonmeta, lock);
        cls = object_getClass(nonmeta);
    }

    // runtimeLock is now unlocked, for +initialize dispatch
    assert(nonmeta->isRealized());
    //調用+initialize方法
    initializeNonMetaClass(nonmeta);

    if (leaveLocked) runtimeLock.lock();
    return cls;
}
複製代碼
  • 最後,initializeNonMetaClass函數就是最核心的函數,它的做用就是根據須要向任意的未初始化的類發送一個「+initialize」消息,而且會首先執行超類的初始化,具體實現查看如下源碼,源碼中省略了部分關於初始化狀態設置的一些代碼,保留了核心的函數調用,若是想看完整代碼,能夠自行查看最新的objc4的源碼
void initializeNonMetaClass(Class cls)
{
    assert(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;
    //此處經過cls->superclass來找到cls的父類,而後經過遞歸來查看父類是否被初始化,從而確保在初始化cls以前,它的父類已經初始化完畢
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
    {
        monitor_locker_t lock(classInitLock);
        //若是當前類並未初始化,則設置類的狀態爲「正在初始化」
        if (!cls->isInitialized() && !cls->isInitializing()) {
            cls->setInitializing();
            reallyInitialize = YES;
        }
    }
    ......
    //若是當前的類未進行初始化,則調用callInitialize進行初始化
    if (reallyInitialize) {
        callInitialize(cls);
        return;
    }
    ......
}
複製代碼
  • 能夠看到,首先會經過遞歸找到cls的父類,判斷父類是否進行過初始化,若是父類未進行初始化,則經過callInitialize函數調用父類的+initialize,而後再調用子類的+initialize。而且callInitialize內部其實也是經過objc_msgSend來發送+initialize消息。

總結

  • +initialize方法是在類第一次接收到消息時調用
  • 若是存在多個子類,而且實現了+initialize方法,會先初始化父類,調用父類的+initialize方法,而後再初始化子類,調用子類的+initialize方法
  • 若是子類沒有實現+initialize方法,則會調用父類的+initialize方法,因此父類的+initialize可能會被調用屢次。
  • 若是分類實現了+initialize方法,則會覆蓋類自己的+initialize調用,由於+initialize方法的調用本質上是經過objc_msgSend來發送一個+initialize消息,因此一旦分類實現了+initialize方法,則會將分類的+initialize方法插入到類方法列表的最前面,會覆蓋原來類的+initialize方法。

給分類添加成員變量?(關聯對象)

默認狀況下,因爲分類的底層結構的限制,不能在分類中添加成員變量,可是咱們能夠經過runtime提供的Api爲類增長關聯對象。

關聯對象的實現

關聯對象主要經過如下三個函數進行實現

  • 添加關聯對象
void objc_setAssociatedObject(id object, const void * key,
                                id value, objc_AssociationPolicy policy)
複製代碼
  • 獲取關聯對象
id objc_getAssociatedObject(id object, const void * key)
複製代碼
  • 移除全部的關聯對象
void objc_removeAssociatedObjects(id object)
複製代碼

以一個簡單的例子來看關聯對象的實現

@interface XLPerson (Test)

@property(nonatomic, copy)NSString *name;

@end

@implementation XLPerson (Test)

- (NSString *)name{
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY);
}
@end
複製代碼

這裏key須要傳入一個內存地址,能夠本身定義,只要保證在setter和getter中用的是相同的key就行。最經常使用的方式就是使用當前getter方法的內存地址,也就是上文中的@selector(name),_cmd其實和@selector(name)等同,都是表示指向name()方法的指針。

  • 使用關聯對象時會用到objc_AssociationPolicy,它其實和咱們OC中的屬性修飾符一一對應,使用什麼修飾符,取決於你定義的屬性的類型,對應關係以下
objc_AssociationPolicy 修飾符
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong, atomic
OBJC_ASSOCIATION_COPY copy, atomic

關聯對象的實現原理

objc_setAssociatedObject

瞭解了關聯對象的使用,接着咱們經過runtime源碼來更深層次的瞭解關聯對象的實現原理。

  • 首先在objc-runtime.mm文件中找到objc_setAssociatedObject函數調用
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
複製代碼
  • 再次查看_object_set_associative_reference函數,就能看到完整的設置關聯對象的流程
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    if (!object && !value) return;
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        //關聯對象的管理類
        AssociationsManager manager;
        //關聯對象的哈希表,裏面存放着的key爲object的地址,value爲ObjectAssociationMap類型的映射表
        AssociationsHashMap &associations(manager.associations());
        //經過DISGUISE,傳入object,計算出所須要的disguised_object值
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            //若是傳過來的value有值,經過object計算出來的disguised_object,到哈希表中找到對應的ObjectAssociationMap類型的映射表
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                //找到object對應的映射表
                ObjectAssociationMap *refs = i->second;
                //經過傳遞過來的key,到refs中找到對應的ObjcAssociation對象
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    //這裏保留舊的ObjcAssociation對象,以後須要釋放
                    old_association = j->second;
                    //經過policy和new_value建立新的ObjcAssociation對象,替換映射表中舊的對象
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    //若是經過key未找到對應的ObjcAssociation對象,則新建一個ObjcAssociation對象插入到映射表中
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                //若是經過disguised_object找不到對應的映射表,則建立新的映射表插入到哈希表associations中
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                //建立新的ObjcAssociation對象存放到映射表中
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            //若是傳遞過來的value爲ni,則經過disguised_object查找到object對應的映射表
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                //遍歷映射表,找到key對應的ObjcAssociation,執行擦除操做
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    //若是存在舊的ObjcAssociation對象,則釋放舊的對象
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
複製代碼

上述源碼中,有幾個很是重要的類,AssociationsManagerAssociationsHashMapObjectAssociationMapObjcAssociation

  • 首先AssociationsManager是一個關聯對象的管理類,它內部有一個AssociationsHashMap類型的靜態變量
class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    //構造函數
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    //析構函數
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};
複製代碼
  • AssociationsHashMap是一個哈希表,經過當前的object調用DISGUISE(object)來獲得索引值,而後經過索引值來獲取或者存放ObjectAssociationMap類型的映射表。
//關聯對象哈希表disguised_ptr_t表示索引,ObjectAssociationMap則存放着全部關聯對象的具體信息
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };
複製代碼
  • ObjectAssociationMap是一個映射表,是以參數中傳過來的key值做爲映射表的key,以ObjcAssociation類型的對象做爲映射表的value
//映射表的key是void *類型,value是ObjcAssociation類型的對象
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };
複製代碼
  • ObjcAssociation對象中其實只有兩個屬性,_policy_value,分別對應參數中的value和policy。
class ObjcAssociation {
        uintptr_t _policy;  //策略
        id _value;          //當前關聯對象的值
    public:
        ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
        ObjcAssociation() : _policy(0), _value(nil) {}

        uintptr_t policy() const { return _policy; }
        id value() const { return _value; }
        
        bool hasValue() { return _value != nil; }
    };
複製代碼

objc_getAssociatedObject

相對於objc_setAssociatedObject函數來講,objc_getAssociatedObject的實現要簡單的多

id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}
複製代碼
  • 查看核心函數_object_get_associative_reference的實現以下
id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    //策略默認是OBJC_ASSOCIATION_ASSIGN
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        //拿到manager中的哈希表
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        //經過object生成哈希表的索引
        disguised_ptr_t disguised_object = DISGUISE(object);
        //經過索引disguised_object去哈希表中找到object對應的映射表
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            //經過傳遞過來的key去映射表中找到對應的ObjcAssociation類型的關聯對象
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                //獲取到ObjcAssociation類型的對象,拿到其中存儲的value和policy
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();、
                //若是是OBJC_ASSOCIATION_GETTER_RETAIN策略,則對value進行一次retain操做
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    //若是value存在,而且policy爲OBJC_ASSOCIATION_GETTER_AUTORELEASE,則對value進行一次autorelease操做,將其放入自動釋放池中
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}
複製代碼

objc_removeAssociatedObjects

以前說到調用objc_setAssociatedObject函數函數時若是value傳nil,就會從映射表中移除關聯對象,可是這一次只能移除一個關聯對象,而objc_removeAssociatedObjects函數則能夠移除一個object的全部關聯對象。

void objc_removeAssociatedObjects(id object) 
{
    //判斷當前object是否存在關聯對象
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}
複製代碼

_object_remove_assocations中就是完整的移除關聯對象的操做

void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        //拿到manager中的哈希表
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (associations.size() == 0) return;
        //經過object生成哈希表的索引
        disguised_ptr_t disguised_object = DISGUISE(object);
        //經過索引disguised_object去哈希表中找到object對應的映射表
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            //經過傳遞過來的key去映射表中找到對應的ObjcAssociation類型的關聯對象
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            //遍歷映射表,移除全部的對象
            associations.erase(i);
        }
    }
    // the calls to releaseValue() happen outside of the lock.
    for_each(elements.begin(), elements.end(), ReleaseValue());
}
複製代碼

關聯對象總結

  • 使用runtime的Api能夠在分類中爲類設置關聯對象。
  • 關聯對象並非存放在被關聯對象自己的內存中,而是存儲在一個全局統一的AssociationsManager
  • 每個類都有一個對應的映射表ObjectAssociationMap,存放在全局的哈希表AssociationsHashMap中,經過類對象的內存地址計算出哈希表的索引。
  • 類的每個關聯對象的值都封裝成了一個ObjectAssociation對象,存放在映射表中。

關聯對象的關係圖以下

Category面試題

一、Category有哪些使用場景?

  • 若是一個類相對龐大,可使用Category對類進行功能分類,減少類的體積
  • 給系統類入NSString、NSObject等等增長方法,協議,也能夠經過關聯對象增長屬性
  • 能夠建立私有方法的前向引用,例如在類中有個私有方法,外部須要使用,那麼就能夠經過給類定義一個Category,在Category中聲明方法不作實現便可。原理其實仍是利用消息轉發機制來進行方法調用。
  • 能夠用來添加非正式的協議

二、Category的內部實現原理是什麼?

  • Category在程序編譯以後會生成category_t類型的結構體,內部存放着分類中的對象方法、類方法、屬性列表和協議列表等等
  • 在程序運行時,runtime會將全部的category_t中的方法列表、屬性列表和協議列表存放到一個大的數組中,而後經過逆序變量合併到類或者元類信息中(類方法存放到元類方法列表中,對象方法存放到類方法列表中)

三、Category和Class Extension的區別是什麼?

  • Category中的方法在編譯時會生成一個靜態的結構體變量,在運行時纔會將內部數據合併到類信息中
  • Class Extension中所定義的方法、屬性和協議在編譯的時候就已經包含在了類信息當中

四、在Category中+load()方法何時調用?

  • 若是一個類存在多個Category,那麼在runtime加載類的時候,會首先調用類的+load方法,而後會調用Category的+load方法,Category的+load方法的調用順序和Category編譯順序有關,先編譯的Category優先調用。
  • 若是類存在多個子類和多個Category,那麼在runtime加載類的時候會先調用父類的+load方法,而後調用子類的+load方法,調用順序和子類的編譯順序相同,最後會調用分類的+load方法。

五、+load()和+initialize()方法的區別是什麼?若是存在分類,它們的調用順序是怎樣的?若是存在繼承,它們的調用順序又是怎樣的?

  • +load()方法是在runtime第一次加載類的時候調用
  • +initialize()方法是在類第一次接收到消息時調用
  • 若是存在分類,會首先經過內存地址直接調用父類的+load方法,而後調用分類的+load方法,分類+load的方法的調用順序和編譯順序相同。
  • 若是存在分類,會覆蓋類自己的+initialize方法,只會調用分類的+initialize方法
  • 若是存在繼承,會首先經過內存地址直接調用父類的+load方法,而後調用子類的+load方法,子類的調用順序和編譯順序相同
  • 若是存在繼承,會首先調用父類的+initialize方法,而後調用子類的+initialize方法,若是子類沒有實現+initialize方法,則會調用父類的+initialize方法,所以,父類的+initialize方法可能會被調用屢次。

六、Category可否添加成員變量?怎麼添加?

  • 能夠給Category添加屬性,可是添加的屬性是沒法生成成員變量的。所以,咱們在分類中添加的屬性是沒法使用的。想要在分類中給類增長相似屬性的效果,可使用關聯對象來間接實現。就是經過runtime提供的objc_setAssociatedObject函數和objc_getAssociatedObject函數來實現。
  • 關聯對象的原理實際上是將全部的關聯對象存放到了一個全局的映射表中,以咱們傳過去的key爲映射表的key。
相關文章
相關標籤/搜索