Category的本質<一>

雪山飛狐1發表於2018-07-24

一 寫在開頭

Category大家應該用過,它主要是用在為物件的不同型別的功能分塊,比如說人這個物件,我們可以為其建立三個分類,分別對應學習,工作,休息。 下面建立了一個Person類和兩個Person類的分類。分別是Person+Test和Person+Eat。這三個類中各有一個方法。

//Person類
- (void)run;
- (void)run{
    
    NSLog(@"run");
}
複製程式碼
//Person+Test分類
- (void)test;
- (void)test{
    
    NSLog(@"test");
}
複製程式碼
//Person+Eat分類
- (void)eat;
- (void)eat{
    
    NSLog(@"eat");
}
複製程式碼

當我們需要使用這些分類的時候只需要引入這些分類的標頭檔案即可:

#import "Person+Test.h"
#import "Person+Eat.h"

Person *person = [[Person alloc] init];
 [person run];
 [person test];
 [person eat];
複製程式碼

我們都知道,函式呼叫的本質是訊息機制。[person run]的本質就是objc_mgs(person, @selector(run)),這個很好理解,由於物件方法是存放在類物件中的,所以向person物件傳送訊息就是通過person物件的isa指標找到其類物件,然後在類物件中找到這個物件方法。 [person test][person run]都是呼叫分類的物件方法,本質應該一樣。[person test]的本質就是objc_mgs(person, @selector(test)),給例項物件傳送訊息,person物件通過自己的isa指標找到類物件,然後在自己的類物件中查詢這個例項方法,那麼問題來了,person類物件中有沒有儲存分類中的這個物件方法呢?Person+Test這個分類會不會有自己的分類的類物件,將分類的物件方法儲存在這個類物件中呢?

我們要清楚的一點是每個類只有一個類物件,不管這個類有沒有分類。所以分類中的物件方法研究儲存在Person類的類物件中。後面我們會通過原始碼證實這一點。

二 底層結構

我們在第一部分講了,分類中的物件方法和類方法最終會合併到類中,分類中的物件方法合併到類的類物件中,分類中的類方法合併到類的元類物件中。那麼這個合併是什麼時候發生的呢?是在編譯器編譯器就幫我們合併好了嗎?實際上是在執行期,進行的合併。 下面我們通過將Objective-c的程式碼轉化為c++的原始碼窺探一下Category的底層結構。我們在命令列進入到存放Person+Test.m這個檔案的資料夾中,然後在命令列輸入clang -rewrite-objc Person+Test.m,這樣Person+Test.m這個檔案就被轉化為了c++的原始碼Person+Test.cpp。 我們開啟這個.cpp檔案,由於這個檔案非常長,所以我們直接拖到最下面,找到_category_t這個結構體。這個結構體就是每一個分類的結構:

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;  //屬性列表
};
複製程式碼

我們接著往下找到這個結構體的初始化:

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,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,
	0,
	0,
};
複製程式碼

通過結構體名稱_OBJC_$_CATEGORY_Person_$_Test我們可以知道這是Person+Test這個分類的初始化。類名對應的是"Person",物件方法列表這個結構體對應的是&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,類方法列表這個結構體對應的是&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,其餘的初始化都是空。 然後我們找到&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test這個結構體:

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_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_Person_Test_test}}
};
複製程式碼

可以看到這個結構體中包含一個物件方法test,這正是Person+Test這個分類中的物件方法。 然後我們再找到&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test這個結構體:

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_Person_Test_test2}}
};
複製程式碼

同樣可以看到這個結構體,它包含一個類方法test2,這個同樣是Person+Test中的類方法。

三 利用runtime進行合併

由於整個合併的過程是通過runtime進行實現的,所以我們要了解這個過程就要通過檢視runtime原始碼去了解。下面是檢視runtime原始碼的過程:

  • 1.找到objc-os.mm這個檔案,這個檔案是runtime的入口檔案。
  • 2.在objc-os.mm中找到_objc_init(void)這個方法,這個方法是執行時的初始化。
  • 3.在_objc_init(void)中會呼叫_dyld_objc_notify_register(&map_images, load_images, unmap_image);,這個函式會傳入map_images這個引數,我們點進這個引數。
  • 4.點選進去map_images我們發現其中呼叫了map_images_nolock(count, paths, mhdrs);這個函式,我們點進這個函式。
  • 5.map_images_nolock(unsigned mhCount, const char * const mhPaths[], const struct mach_header * const mhdrs[])這個函式非常長,我們直接拉到這個函式最下面,找到_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);這個函式,點選進去。
  • 6.void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)這個方法大概就是讀取模組的意思了。 這個函式也是非常長,我們大概在中間位置找到了這樣一行註釋
// Discover categories.
複製程式碼

這個基本上就是我們要找的處理Category的模組了。 我們在這行註釋下面找到這幾行程式碼:

if (cls->isRealized()) {
       remethodizeClass(cls);
       classExists = YES;
                }

 if (cls->ISA()->isRealized()) {
       remethodizeClass(cls->ISA());  //class的ISA指標指向的是元類物件
               }
複製程式碼

這個程式碼裡面有一個關鍵函式remethodizeClass,通過函式名我們大概猜測這個方法是重新組織類中的方法,如果傳入的是類,則重新組織物件方法,如果傳入的是元類,則重新組織類方法。

  • 7.然後我們點進這個方法裡面檢視:
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    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);
    }
}
複製程式碼

我們看到這段程式碼的核心是呼叫了attachCategories(cls, cats, true /*flush caches*/);這個方法。這個方法中傳入了一個類cls和所有的分類cats。

  • 8.我們點進attachCategories(cls, cats, true /*flush caches*/);這個方法。這個方法基本上就是核心方法了。
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);
}
複製程式碼
  • bool isMeta = cls->isMetaClass();判斷是類還是元類。
  • 建立總的方法陣列,屬性陣列,協議陣列
//方法陣列
    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));
複製程式碼

這裡mlists,proplists,protolists都是用兩個*修飾的,說明是申請了一個二維陣列。這三個二維陣列裡面的一級物件分別是方法列表,屬性列表,以及協議列表。由於每一個分類Category都有一個方法列表,一個屬性列表,一個協議列表,方法列表中裝著這個分類的方法,屬性列表中裝著這個分類的屬性。*所以mlists也就是裝著所有分類的所有方法。

  • 給前面建立的陣列賦值
 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;
        }
    }
複製程式碼

這段程式碼就很清楚了,通過一個while迴圈遍歷所有的分類,然後獲取該分類的所有方法,賦值給前面建立的大陣列。

  • rw = cls->data();得到類物件裡面的所有資料。
  • rw->methods.attachLists(mlists, mcount);將所有分類的方法,附加到類的方法列表中。
  • 9.我們點進這個方法裡面看看具體的實現:
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]));
        }
    }
複製程式碼

傳進來的這個addedLists引數就是前面得到的這個類的所有分類的物件方法或者類方法,而addedCount就是addedLists這個陣列的個數。假設這個類有兩個分類,且每個分類有兩個方法,那麼addedLists的結構大概就應該是這樣的: [ [method, method] [method, method] ] addedCount = 2 我們看一下這個類的方法列表之前的結構:

7F4EE0B0-BD7D-4162-AD28-76209E034096.png
所以oldCount = 1

setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
複製程式碼

這一句是重新分配記憶體,由於要把分類的方法合併進來,所以以前分配的記憶體就不夠了,重新分配後的記憶體:

82E1D1CD-096B-4AA8-9B23-96D05CAF7AD3.png

memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
複製程式碼

memmove這個函式是把第二個位置的物件移動到第一個位置。這裡也就是把這個類本來的方法列表移動到第三個位置。

memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
複製程式碼

memcpy這個函式是把第二個位置的物件拷貝到第一個位置,也就是把addedLists拷貝到第一個位置,拷貝之後的記憶體應該是這樣的:

E788A916-FD1B-4E98-9363-7F36AF16C403.png

至此就把分類中的方法列表合併到了類的方法列表中。 通過上面的合併過程我們也明白了,當分類和類中有同樣的方法時,類中的方法並沒有被覆蓋,只是分類的方法被放在了類的方法前面,導致先找到了分類的方法,所以分類的方法就被執行了。

四 總結

1.通過runtime載入某個類的所有Category資料。 2.把所有Category的方法,屬性,協議資料合併到一個大陣列中,後面參與斌編譯的Category資料,會在陣列的前面。 3.將合併後的分類資料(方法,屬性,協議),插入到類原來資料的前面。

相關文章