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 的實現原理
- ① 分類的實現原理取決於運行時決議;
- ② 同名分類方法誰能生效取決於編譯順序,最後參與編譯的分類中的同名方法會最終生效;
- ③ 分類方法會“覆蓋”同名的宿主類(原類)方法,這裏說的“覆蓋”並不是指原來的方法沒了。消息傳遞過程中優先查找宿主類中靠前的元素,找到同名方法就進行調用,但實際上宿主類中原有同名方法的實現仍然是存在的。我們可以通過一些手段來調用到宿主類原有同名方法的實現,如可以通過
Runtime
的class_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.h
和objc-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
的拷貝;- 而
methods
是rw
可讀寫的,將來運行時要合併分類方法列表。
Q:Category 中有 +load 方法嗎?+load 方法是什麼時候調用的?+load 方法能繼承嗎?
- 分類中有
+load
方法; +load
方法在Runtime
加載類、分類的時候調用;+load
方法可以繼承,但是一般情況下不會手動去調用+load
方法,都是讓系統自動調用。