Objective-C(十)Category

本文是Objective-C系列的第10篇,主要講述了category的實現原理和相關特性。git

1、概述

1.1 category簡介

category是Objective-C 2.0以後添加的語言特性,分類、類別其實都是指的category。category的主要做用是爲已經存在的類添加方法。github

能夠把類的實現分開在幾個不一樣的文件裏面。這樣作有幾個顯而易見的好處。數組

  • 把不一樣的功能組織到不一樣的category裏,減小單個文件的體積,且易於維護;
  • 能夠由多個開發者共同完成一個類;
  • 能夠按需加載想要的category;
  • 聲明私有方法;

不過除了apple推薦的使用場景,還衍生出了category的其餘幾個使用場景:緩存

  1. 模擬多繼承(另外能夠模擬多繼承的還有protocol)
  2. 把framework的私有方法公開

1.2 extension

​ extension被開發者稱之爲擴展、延展、匿名分類。extension看起來很像一個匿名的category,可是extension和category幾乎徹底是兩個東西。bash

​ 和category不一樣的是extension不但能夠聲明方法,還能夠聲明屬性、成員變量。extension通常用於聲明私有方法,私有屬性,私有成員變量。app

​ 使用 extension 必須有原有類的源碼。extension 聲明的方法、屬性和成員變量必須在類的主 @implementation 區間內實現,能夠避免使用有名稱的 category 帶來的多個沒必要要的 implementation 段。iphone

​ extension 很常見的用法,是用來給類添加私有的變量和方法,用於在類的內部使用。例如在 interface 中定義爲 readonly 類型的屬性,在實現中添加 extension,將其從新定義爲 readwrite,這樣咱們在類的內部就能夠直接修改它的值,然而外部依然不能調用 setter 方法來修改。佈局

1.3 category與extension的區別

category和extension的區別來看,post

  • extension能夠添加實例變量,而category是沒法添加實例變量的。
  • extension在編譯期決議,是類的一部分,category則在運行期決議。extension在編譯期和頭文件裏的@interface以及實現文件裏的@implement一塊兒造成一個完整的類,extension伴隨類的產生而產生,亦隨之一塊兒消亡。
  • extension通常用來隱藏類的私有信息,你必須有一個類的源碼才能爲一個類添加extension,因此你沒法爲系統的類好比NSString添加extension,除非建立子類再添加extension。而category不須要有類的源碼,咱們能夠給系統提供的類添加category。
  • extension和category均可以添加屬性,可是category的屬性不能生成成員變量和getter、setter方法的實現。

2、category編譯期

首先,咱們編寫下面這些類,源碼在:01-principleui

image-20181127203306009

重編譯爲C++代碼:

$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc BFPerson+Work.m
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc BFPerson+Study.m
複製代碼

Study分類爲例,看看C++中的結構體:

image-20181128113531637

其中對應初始化中的部分結構體以下:

image-20181128114306403

咱們看到在編譯期中針對分類中的,實例方法、類方法以及屬性(假若有協議一樣會生成,示例代碼中未繼承協議)都生成了對應的結構體。

最後造成的結構體就是上圖中的_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;			//屬性列表
};
複製代碼

3、category運行期

源碼在:01-principle

3.1 分類在運行期作了什麼

  • runtime會加載某個類的全部category數據;

  • 將全部category的方法(對象方法、類方法)、屬性、協議數據,合併到一個大數組中

    • 後面參與編譯的category數據,會在合併數組的前面;
  • 將合併後的分類數據(方法、屬性、協議),插入到類原來數據的前面;

下面咱們從objc源碼開始追蹤這些結論的體現。

3.2 category_t結構體

//from objc-runtime-new.h
struct category_t {
    const char *name;
    classref_t 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);
};
複製代碼

3.3 源碼導讀

下面源碼,咱們以分類中的對象方法爲例。其類方法、協議或屬性,和對象方法處理流程相似,遵循一樣的規律。

3.3.1 導讀圖

源碼執行流程以下:

image-20181130104455128

下面是分類方法合併到原類中的流程圖:

image-20181130142737137

3.3.2 核心流程

咱們讀取最核心重佈局對象方法的環節:

a. 肯定分類編譯順序

項目編譯中,Compile Sources會決定分類編譯順序,下圖中Study分類在前,Work分類在後。

image-20181130110037825

b. 獲取分類列表cats

獲得列表,Study在前,Work在後。

cats = [

​ category_t (BFPerson+Study),

​ category_t (BFPerson+Work)

]

c. 將cats方法合併到mlists

將分類cats 倒序抽取出每一個分類的方法,放入二維數組mlists中,此時Work對象方法在前:

mlists = [ ​ [method_t work, method_t test], ​ [method_t study, method_t test], ]

d. mlists插入rw

mlists方法插入rw對象方法列表method_array_t

  1. 將rw原有方法列表[method_t test]移動到method_array_t最後;
  2. 將分類mlists方法拷貝到method_array_t頭部。

e. 對象方法結果

最後,咱們得到的rwmethod_array_t爲:

method_array_t = [

​ [method_t study, method_t test], <-->BFPerson+Work

​ [method_t work, method_t test], <-->BFPerson+Study

​ [ method_t test], <-->BFPerson

]

3.3.3 核心源碼截取

下面是抽取的部分源碼:

3.3.3.1 分類方法處理

// cls - BFPerson
// *cats = [category_t (BFPerson+Study), category_t (BFPerson+Work) ]
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    // 分配內存:針對未進行重佈局的分類列表
    /*mlists 方法二維數組*/
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    int i = cats->count;
    
    while (i--) {
        // i-- 在category list最後面的,先添加到mlists
        // 取出分類列表中的分類
        // 最後參與編譯的,先取出category_t BFPerson+Work
        // 編譯順序:在編譯log中查看,在Compile Sources中更改
        auto& entry = cats->list[i];
        // 取出分類的對象方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            // 將對應分類的方法列表添加到mlists
            /* 以 *cats = [category_t (BFPerson+Study), category_t (BFPerson+Work)] 執行完while 循環後,mlists爲 [ [method_t work, method_t test], [method_t study, method_t test], ] */
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    
    // 將從新佈局的category list添加到 rw中
    // 將全部分類的對象方法,附加到類對象的方法列表中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    // 刷新方法緩存列表
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
}
複製代碼

3.3.3.2 方法列表處理

而後開始從新編排rw中的方法列表:

/** @param addedLists 分類方法列表————二維數組 [ [method_t work, method_t test], [method_t study, method_t test] ] @param addedCount 分類列表長度————即分類的數目 */
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 原來的方法列表
        // *memmove(void *__dst, const void *__src, size_t __len);
        // 將原方法列表array()->lists開始,移動oldCount長度,移動到array()->lists + addedCount(分類個數)
        // 此處,假如原單位佔用1個,分類此處有兩個(Study、Work)
        // 【1】【】【】-> 【】【】【1】
        memmove(array()->lists + addedCount, array()->lists, 
                oldCount * sizeof(array()->lists[0]));
        // *memcpy(void *__dst, const void *__src, size_t __n);
        // 將addedLists開始的addedCount長度,拷貝到array()->lists位置
        // 【1】【】【】-> 【】【】【1】-> 【2-Work】【3-Study】【1】
        memcpy(array()->lists, addedLists, 
               addedCount * sizeof(array()->lists[0]));
    }
}
複製代碼

參考

連接

  1. Apple souce objc4

示例代碼

  1. 01-principle
相關文章
相關標籤/搜索