手把手帶你探索Category底層原理

前言

Category在iOS開發中使用很是的頻繁,特別是在爲系統類進行拓展的時候,咱們能夠不用繼承系統類,直接給系統類添加方法,最大程度的體現了Objective-C的動態語言特性。安全

本文篇幅較長,但內容完整,建議能跟隨文章內容探索一遍,畢竟實踐出真知。bash

Catergory的做用

  1. 將類的實現分散到多個不一樣的文件或多個不一樣的框架中。以下:不一樣的功能模塊用不一樣的Category處理 數據結構

  2. 能夠在不修改原來類的基礎上,爲一個類添加擴展方法。如咱們須要給系統自帶的類添加方法。框架

  3. 會覆蓋原類中方法名相同的方法,多個Category的同名方法,會按照編譯順序,執行最後編譯的Category裏的方法。 ide

  4. Extension(擴展)的區別:函數

    • Category 是運行期決定的,Extension 是編譯期決定,是類的一部分。
    • Category 不能添加實例變量(只能經過runtime添加),Extension 能夠添加實例變量。(由於在運行期,對象的內存結構是已經肯定的,若是添加實例變量就會破壞類的內部結構)
    • Category 和 Extension 均可以用@property添加屬性,可是Category添加的屬性不能生成成員變量和getter,setter方法的實現,即不能經過_var調用,不過能夠手動經過objc_get/setAssociatedObject手動實現。

Category編譯期探索

上面說了一大堆,如今正式開始探索歷程,探索上面解釋的正確性,先來看看編譯期幹了什麼吧。工具

  • 新建了一個測試工程,建立了一個實體類,和一個Test分類 , 分類裏聲明一個Test方法,並cmd+B編譯一下 佈局

  • 打開命令行工具,cd到當前目錄下,執行clang -rewrite-objc Person+Test.m命令,而後找到當前工程文件夾,找到編譯後的 Person+Test.cpp文件並打開測試

  • 文件內容很是多,因爲知道category的結構體是_categoy_t, 全局搜索找到了它的結構體ui

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,找到了咱們的測試的分類
static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	"Person",
	0, // &OBJC_CLASS_$_Person,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
	0,
	0,
	0,
};
複製代碼
  • 這裏的section ("__DATA,__objc_const"),是一個段標識,它會存放到咱們的dyld加載Macho可執行文件裏的這個section段裏(關於dyld及Macho文件內容較多,這裏暫時不作詳細解釋,這裏瞭解到它會在編譯的時候會存放到Macho文件裏便可)
  • 對比着上面_category_t的結構體,發現這裏的類名是Person,實例方法列表裏存放了一個Test方法
  • 具體搜索這個實例方法,看到它就在上面幾行的代碼裏
_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"Test", "v16@0:8", (void *)_I_Person_Test_Test}}
};
複製代碼
  • 這裏保存了這個方法的大小,第二個值是方法類型, 經過下面找到的_objc_method結構體對照着能夠看到
  • 第三個參數imp裏包含了Sel方法編號,方法簽名及真實的函數地址
struct _objc_method {
	struct objc_selector * _cmd;
	const char *method_type;
	void  *_imp;
};
複製代碼
  • 咱們瞭解到上面的全部內容就是在編譯器主要是把category編譯成結構體,而後把對應的值填充到結構體裏,保存在Macho可執行文件裏
  • 繼續搜索_category_t,發現還有以下代碼
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_Person_$_Test,
};
複製代碼
  • 這裏實際上是把咱們APP的全部category方法都保存到__objc_catlist裏,也就是在加載到Macho文件裏的對應的section段裏

編譯期總結

在編譯期,把category編譯成對應的結構體保存到Macho文件裏的section段,把全部的分類方法都保存到Macho文件__objc_catlist這個section段裏

Category運行期探索

瞭解到編譯器主要作了保存的操做,那麼運行期毫無疑問是作的加載操做,須要把剛剛編譯期保存的內容都進行加載。

dyld加載

先來看看Category是如何被加載的

  1. dyld是蘋果的動態加載器,用來加載image(image指的是Mach-O格式的二進制文件,不是圖片)

  2. 當程序啓動時,系統內核首先會加載dyld,而dyld會將咱們APP所依賴的各類庫加載到內存中,其中就包括libobjc庫(OC和runtime),這些工做,是在APP的main函數執行以前完成的

  3. _objc_init是Object-C runtime 的入口函數,在這裏主要是讀取Mach-O文件OC對應的Segment section,並根據其中的數據代碼信息,完成爲OC的內存佈局,以及初始化runtime相關的數據結構。

先驗證一下_objc_init是不是入口函數,打開剛纔的測試工程,添加一個符號斷點_objc_init,而後運行工程

這裏驗證了咱們上面的說法,入口函數是 _objc_init,接下來纔會執行 dyld_start加載函數

探索_objc_init

源碼工程objc4網盤連接 密碼:tuw8

  • 這裏須要用到下載的源碼,打開objc4,直接搜索_objc_init,找到其函數實現
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);
}
複製代碼
  • 前面都是一些init函數,這裏重點是_dyld_objc_notify_register,註冊了三個回調函數
  • &map_images將image加載進內存
  • load_imagesdyld初始化加載image方法
  • unmap_images移除內存

咱們要探索Category是如何被加載進內存的,因此要看&map_images到底作了什麼,點進這個方法

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);
}
複製代碼
  • 繼續過渡,找到關鍵函數_read_images
void map_images_nolock(unsigned mhCount, const char * const mhPaths[], const struct mach_header * const mhdrs[]) {
    //其他無關代碼已省略
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

    firstTime = NO;
}
複製代碼
  • _read_images函數裏的內容較多,找到與category相關的代碼,這段代碼較長
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
//其他代碼已省略
       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);
                }
            }
        }
    }
複製代碼
  • 首先_getObjc2CategoryList函數是讀取全部的category方法,點進該方法能夠看到它其實就是讀取咱們編譯期時的_objc_catlistsection段內容
GETSECT(_getObjc2CategoryList,        category_t *,    "__objc_catlist");
複製代碼
  • 而後遍歷全部的分類方法並處理,if (cat->instanceMethods)能夠看到這裏判斷了當前category的方法是類方法仍是實例方法,並分別作不一樣的處理
  • addUnattachedCategoryForClass(cat, cls, hi);這裏是把category與該class原類關聯映射起來,能夠點進去該方法看內容
  • 看到這裏還有一個remethodizeClass(cls);,看名字像是從新設置類裏面的函數,點進去看看具體函數內容
static void remethodizeClass(Class cls) {
    //多餘代碼省略
    category_list *cats;
        // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}
複製代碼
  • 繼續點進去看看attachCategories關聯分類函數的具體實現
static void attachCategories(Class cls, category_list *cats, bool flush_caches) {
    //省略無關代碼
    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));
        
    //省略無關代碼
           prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
}
複製代碼
  • 這裏初始化了方法列表,協議列表和集合列表,着重找mlists
  • 能夠看到這裏有兩個重要的方法prepareMethodListsattachLists
  • 先點進去看看prepareMethodLists
//省略其餘無關代碼
for (int i = 0; i < addedCount; i++) {
        //把要添加原類的方法列表取出來
        method_list_t *mlist = addedLists[i];
        assert(mlist);

        // Fixup selectors if necessary
        if (!mlist->isFixedUp()) {
            fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
        }
    }
複製代碼
  • 取出來方法列表後,調用了fixupMethodList,點進去看看

  • 這裏作的是把方法列表裏的方法都註冊到原類裏

  • 總之,prepareMethodLists作的是添加方法列表前的準備工做

  • 回到外面,點擊進入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;
            /** 拷貝的操做 void *memmove(void *__dst, const void *__src, size_t __len); */
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
複製代碼
  • 能夠看到這裏是經過獲取到要添加的分類方法列表和它的大小,而後拷貝到原有的方法列表裏
  • 這裏最終通過了memmovememcpy函數完成了拷貝操做,那麼這兩個函數具體究竟是作了什麼呢?

memmove是把原類裏的方法列表,向後移動了要添加的方法列表的大小的距離

memcopy是把要添加的方法列表拷貝到原類剛剛的方法列表裏空出來的位置上

  • 探索到這裏,已經明白爲何分類的方法能覆蓋原類的方法了,它只是比原類的方法先調用而已,實際上並無覆蓋這個方法。也明白爲何多個分類同名方法會執行最後編譯的分類裏的方法裏,也是調用順序的問題。
  • 這個還和方法調用流程有關,二分查找法,因此會優先調用前面的方法

探索Category關聯對象objc_get/setAssociatedObject

衆所周知,在分類裏能夠經過objc_get/setAssociatedObject來模擬添加屬性,那麼它究竟是如何實現的呢?

  • 繼續打開剛纔的源碼,搜索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,這裏的代碼較多,逐行分析

  • acquireValue是進行內存管理,能夠點進去看一下

  • 這裏有一個AssociationsManager,看到它裏面有個AssociationsHashMap,而且訪問會加鎖,是線程安全的

  • disguised_ptr_t disguised_object = DISGUISE(object);這裏使用對象取反後的值做爲key,迭代器裏的value 是ObjectAssociationMap

  • 再看ObjectAssociationMap,它的key是用戶傳進來的自定義key,它的value是ObjcAssociation

  • 還有最後一個重要的方法setHasAssociatedObjects,這裏把屬性和類關聯起來,而且設置isa指針的標識isa.has_assoc,以便釋放的時候使用

  • 同理,objc_getAssociatedObject也是從這裏取出來值的

關聯屬性是何時移除的?

在上面咱們知道屬性是經過類的isa關聯起來的,那麼理應在這個對象銷燬的時候一塊兒移除該屬性。 一樣的在當前objc源碼裏搜索dealloc,找到了它的實現

- (void)dealloc {
    _objc_rootDealloc(self);
}
複製代碼
  • 繼續跟蹤
void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}
複製代碼
  • 繼續跟蹤找到了rootDealloc
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
複製代碼
  • 在這裏先作了一些isa標識的判斷,沒有找到咱們想要的
  • 繼續跟蹤object_dispose
id object_dispose(id obj) {
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
複製代碼
  • 繼續跟蹤,終於在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;
}
複製代碼
  • 這裏有經過isa.has_assoc標識符判斷當前對象是否有關聯屬性,若是有就調用上面代碼裏的_object_remove_assocations移除關聯屬性

  • 繼續看_object_remove_assocations,和咱們設置關聯屬性的代碼相似,不過這裏是取出來而後delete refs刪除

總結

以上就是探索Category底層原理的整個過程,也使得文章開頭的Category的做用獲得驗證。整個過程是枯燥和冗長的,可是探索完仍是有很大的收穫。本文篇幅很長,但願你們也能親自試着探索一遍,能不只僅知足於知道這是什麼,還要去探究爲何會是這樣。

相關文章
相關標籤/搜索