OC - Category 和 Extension

網絡配圖

1. Category 分類

1.1 Category 的使用場合

  • ① 給一個類添加新的方法,可以爲系統的類擴展功能。
  • ② 分解體積龐大的類文件,可以將一個類按功能拆解成多個模塊,方便代碼管理。
  • ③ 創建對私有方法的前向引用:聲明私有方法,把 Framework 的私有方法公開等。直接調用其他類的私有方法時編譯器會報錯的,這時候可以創建一個該類的分類,在分類中聲明這些私有方法(不必提供方法實現),接着導入這個分類的頭文件就可以正常調用這些私有方法。
  • ④ 向對象添加非正式協議:創建一個 NSObject 或其子類的分類稱爲 “創建一個非正式協議”。
    (正式協議是通過 protocol 指定的一系列方法的聲明,然後由遵守該協議的類自己去實現這些方法。而非正式協議是通過給 NSObject 或其子類添加一個分類來實現。非正式協議已經漸漸被正式協議取代,正式協議最大的優點就是可以使用泛型約束,而非正式協議不可以。)

1.2 Category 中都可以添加哪些內容?

  • 實例方法、類方法、協議、屬性(只生成 setter 和 getter 方法的聲明,不會生成 setter 和 getter 方法的實現以及下劃線成員變量);
  • 默認情況下,因爲分類底層結構的限制,不能添加成員變量到分類中,但可以通過關聯對象來間接實現這種效果。

1.3 Category 的優缺點、特點、注意點

  • 優點:
    ① 見 Category 的使用場合;
    ② 可以按需加載不同的分類。
  • 缺點:
    ① 不能直接添加成員變量,但可以通過關聯對象實現這種效果;
    ② 分類方法會“覆蓋”同名的宿主類方法,如果使用不當會造成問題。
  • 特點:
    ① 運行時決議
    ② 可以有聲明,可以有實現
    ③ 可以爲系統的類添加分類
    (運行時決議:Category 編譯之後的底層結構時struct category_t,裏面存儲着分類的對象方法、類方法、屬性、協議信息,這時候分類中的數據還沒有合併到類中,而是在程序運行的時候通過Runtime機制將所有分類數據合併到類(類對象、元類對象)中去。(這是分類最大的特點,也是分類和擴展的最大區別,擴展是在編譯的時候就將所有數據都合併到類中去了)
  • 注意點:
    ① 分類方法會“覆蓋”同名的宿主類方法,如果使用不當會造成問題;
    ② 同名分類方法誰能生效取決於編譯順序,最後參與編譯的分類中的同名方法會最終生效;
    ③ 名字相同的分類會引起編譯報錯。

1.4 Category 的實現原理

  • ① 分類的實現原理取決於運行時決議;
  • ② 同名分類方法誰能生效取決於編譯順序,最後參與編譯的分類中的同名方法會最終生效;
  • ③ 分類方法會“覆蓋”同名的宿主類(原類)方法,這裏說的“覆蓋”並不是指原來的方法沒了。消息傳遞過程中優先查找宿主類中靠前的元素,找到同名方法就進行調用,但實際上宿主類中原有同名方法的實現仍然是存在的。我們可以通過一些手段來調用到宿主類原有同名方法的實現,如可以通過Runtimeclass_copyMethodList方法打印類的方法列表,找到宿主類方法的imp,進行調用(可以交換方法實現)。

1.4.1 編譯

源碼分析

通過 Clang 將以下分類代碼轉換爲 C++ 代碼,來分析分類的底層實現。

// Clang
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m
#import "Person.h"
@interface Person (Test)<NSCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)eat;
- (void)sleep;
+ (void)run;
+ (void)walk;
@end

#import "Person+Test.h"
@implementation Person (Test)
- (void)eat {
    NSLog(@"eat");
}
- (void)sleep {
    NSLog(@"sleep");
}
+ (void)run {
    NSLog(@"run");
}
+ (void)walk {
    NSLog(@"walk");
}
@end
// Person+Test.cpp
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 /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Person_Test_eat},
	{(struct objc_selector *)"sleep", "v16@0:8", (void *)_I_Person_Test_sleep}}
};

// 類方法列表
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"run", "v16@0:8", (void *)_C_Person_Test_run},
	{(struct objc_selector *)"walk", "v16@0:8", (void *)_C_Person_Test_walk}}
};

// 協議列表
static struct /*_protocol_list_t*/ {
	long protocol_count;  // Note, this is 32/64 bit
	struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	1,
	&_OBJC_PROTOCOL_NSCopying
};

// 屬性列表
static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	2,
	{{"name","T@\"NSString\",C,N"},
	{"age","Ti,N"}}
};

// Person+Test 分類編譯的底層結構
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,
	(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Test,
};

從以上可以看到,Category 編譯之後的底層結構時struct category_t
objc4源碼鏈接:https://opensource.apple.com/tarballs/objc4/
下面我們進入Runtime的最新源代碼objc4-756.2進行分析。在源代碼中與 Category 相關的代碼基本都放在objc-runtime-new.hobjc-runtime-new.mm兩個文件中。我們先來看一下 Category 在源代碼中的定義struct category_t

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);
};

從以上 Category 的底層結構來看,分類中可以添加實例方法、類方法、協議、屬性,但是不能添加成員變量,因爲沒有存儲成員變量對應的指針變量。

1.4.2 加載處理過程

在編譯時,Category 中的數據還沒有合併到類中,而是在程序運行的時候通過Runtime機制將所有分類數據合併到類(類對象、元類對象)中去。下面我們來看一下 Category 的加載處理過程。

  • ① 通過Runtime加載某個類的所有 Category 數據;
  • ② 把所有的分類數據(方法、屬性、協議),合併到一個大數組中;
    (後面參與編譯的 Category 數據,會在數組的前面)
  • ③ 將合併後的分類數據(方法、屬性、協議),插入到宿主類原來數據的前面。
    (所以會優先調用最後參與編譯的分類中的同名方法)
源碼分析

加載函數調用棧:

  • objc-os.mm
    ① _objc_init:Runtime的入口函數,進行一些初始化操作
    ② map_images:加鎖
    ③ map_images_nolock:程序或內存鏡像的處理
  • objc-runtime-new.mm
    ④ _read_images:讀取鏡像,完成類、分類、協議的加載等
    remethodizeClass(核心函數):分類的加載都在這個函數裏開始
    ⑥ attachCategories:將分類中的所有信息(方法、屬性、協議列表)都合併到對應的二維數組中
    ⑦ attachLists:將這些信息合併到宿主類中去
    ⑧ realloc、memmove、memcpy

下面我們通過⑤⑥⑦三個函數來分析分類中實例方法的添加邏輯:
remethodizeClass

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();
    /*
     我們只分析分類中實例方法的添加邏輯
     因此這裏假設 isMeta = NO
     */
    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    // 獲取 cls 中所有未完成整合的分類
    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:所有未完成整合的分類
         */
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

attachCategories

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);
    /*
     我們只分析分類中實例方法的添加邏輯
     因此這裏假設 isMeta = NO
     */
    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    /* 二維數組
       [[method_t,method_t,...], [method_t], [method_t,method_t,method_t,...]]
     */
    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;
        }
    }
    //獲取宿主類的 class_rw_t 數據
    auto rw = cls->data();
    //主要是針對 分類中有關於內存管理相關方法情況下的 一些特殊處理
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    /*
     rw->methods:宿主類的方法列表
     mcount:含有方法列表的分類個數
     mlists:所有分類的方法列表二維數組
            [[method_t,method_t,...], [method_t], [method_t,method_t,method_t,...]]
             -----------------------  ----------  --------------------------------
                分類A的方法列表(A)           B                      C
     attachLists:將含有 mcount 個元素的 mlists 合併到 rw->methods 中
     */
    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

/*
 addedLists:所有分類的方法列表二維數組
   [[method_t,method_t,...], [method_t], [method_t,method_t,method_t,...]]
    -----------------------  ----------  --------------------------------
       分類A的方法列表(A)           B                      C
 addedCount:含有方法列表的分類個數,即 addedLists 的元素個數,假設 
 addedCount = 3
*/
void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;

    if (hasArray()) {
        // many lists -> many lists
        //宿主類rw->methods方法列表中原有元素總數,假設 oldCount = 2
        uint32_t oldCount = array()->count;
        //合併之後的元素總數 oldCount + addedCount = 5
        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]));
        /*
         內存拷貝:(類似於 memmove() ,將 addedLists 拷貝到類中原來的方法列表指向的位置)
         [
            A   --->   [addedLists中的第一個元素],即最後參與編譯的分類的方法列表
            B   --->   [addedLists中的第二個元素],即倒二參與編譯的分類的方法列表
            C   --->   [addedLists中的第三個元素],
            [原有的第一個元素],
            [原有的第二個元素]
         ]
     
         這也就是分類方法會“覆蓋”宿主類方法的原因
         */
        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]));
    }
}

2. Extension 擴展

2.1 Extension 是什麼?

  • ① Extension 有一種說法叫“匿名分類”,因爲它很像分類,但沒有分類名。嚴格來說要叫類擴展。
  • ② Extension 的作用是將原來放在 .h 中的數據放到 .m 中去,私有化,變成私有的聲明。
  • ③ Extension 是在編譯的時候就將所有數據都合併到類中去了(編譯時決議),而 Category 是在程序運行的時候通過Runtime機制將所有數據合併到類中去(運行時決議)。

2.2 Extension 一般用來做什麼?

  • ① 聲明私有屬性
  • ② 聲明私有方法
  • ③ 聲明私有成員變量

2.3 Extension 的特點以及 Extension 與 Category 的區別

  • ① 編譯時決議(在編譯的時候就將擴展的所有數據都合併到類中去了)
  • ② 只以聲明的形式存在,多數情況下寄生於宿主類的.m中
  • ③ 不能爲系統類添加擴展
Category Extension
運行時決議 編譯時決議
可以有聲明,可以有實現 只以聲明的形式存在,多數情況下寄生於宿主類的.m中
可以爲系統的類添加分類 不能爲系統類添加擴展

3. 相關面試題

Q:Category 能否添加成員變量?如果可以,如何給 Category 添加成員變量?

因爲分類底層結構的限制,不能直接給 Category 添加成員變量,但是可以通過關聯對象間接實現 Category 有成員變量的效果。
傳送門:OC - Association 關聯對象

Q:爲什麼分類中屬性不會自動生成 setter、getter 方法的實現,不會生成成員變量,也不能添加成員變量?

因爲類的內存佈局在編譯的時候會確定,但是分類是在運行時才加載,在運行時Runtime會將分類的數據,合併到宿主類中。

Q:爲什麼將以前的方法列表挪動到新的位置用 memmove 呢?

爲了保證挪動數據的完整性。而將分類的方法列表合併進來,不用考慮被覆蓋的問題,所以用 memcpy 就好。

Q:爲什麼優先調用最後編譯的分類的方法?

attachCategories()方法中,從所有未完成整合的分類取出分類的過程是倒序遍歷,最先訪問最後編譯的分類。然後獲取該分類中的方法等列表,添加到二維數組中,所以最後編譯的分類中的數據最先加到分類二維數組中,最後插入到宿主類的方法列表前面。而消息傳遞過程中優先查找宿主類中靠前的元素,找到同名方法就進行調用,所以優先調用最後編譯的分類的方法。

Q:objc_class 結構體中的 baseMethodList 和 methods 方法列表的區別?

回答此道問題需要先了解Runtime的數據結構objc_class
傳送門:深入淺出 Runtime(二):數據結構

  • baseMethodList基礎的方法列表,是ro只讀的,不可修改,可以看成是合併分類方法列表前的methods的拷貝;
  • methodsrw可讀寫的,將來運行時要合併分類方法列表。

Q:Category 中有 +load 方法嗎?+load 方法是什麼時候調用的?+load 方法能繼承嗎?

  1. 分類中有+load方法;
  2. +load方法在Runtime加載類、分類的時候調用;
  3. +load方法可以繼承,但是一般情況下不會手動去調用+load方法,都是讓系統自動調用。

傳送門:OC - load 和 initialize

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章