一、概述
Category又叫分類,類別,類目,作為Objective-C 2.0之後新增的語言特性,Category在如今的OC工程中隨處可見,它可以在即使不知道原始碼的情況下為類新增方法,根據官方文件Category其主要作用有:
- 分離類的實現到獨立的檔案,這樣做的好處有:
- 減小單個檔案的程式碼量(維護一個2000程式碼的類和維護四個500的程式碼的類差別還是比較明顯的)。
- 把不同功能組織到不同的Category。
- 方便多人維護一個類
- 按需載入想要的Category
- 定義私有方法
- 模擬多繼承
- 把framework的私有方法公開
注:Category 有一個非常容易誤用的場景,那就是用 Category 來覆寫父類或主類的方法。雖然目前 Objective-C 是允許這麼做的,但是這種使用場景是非常不推薦的。使用 Category 來覆寫方法有很多缺點,比如不能覆寫 Category 中的方法、無法呼叫主類中的原始實現等,且很容易造成無法預估的行為。
二、底層實現
在objc4-723中Category的定義如下:
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);
};
複製程式碼
- name
分類名 - cls
類 - instanceMethods
例項方法列表 - classMethod
類方法列表 - protocols
遵守的協議列表 - instanceProperties
例項屬性列表 - _classProperties
類屬性列表
從Category的結構可見:
- 它可以新增例項方法,類方法,甚至可以實現協議,新增屬性(但是這裡的屬性不會自動生成例項變數和對應的set、get方法需要通過關聯物件實現)
- 不可以新增例項變數。
三、Category載入
對於OC執行時,在入口方法中:
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);
}
複製程式碼
category被附加到類上面是在map_images的時候發生的,在new-ABI的標準下,_objc_init裡面的呼叫的map_images最終會呼叫objc-runtime-new.mm裡面的_read_images方法,而在_read_images方法的結尾,有以下的程式碼片段:
// Discover categories.
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);
}
}
}
}
複製程式碼
這裡做的就是:
- 把category的例項方法、協議以及屬性新增到類上
- 把category的類方法和協議新增到類的metaclass上
接著往裡看,category的各種列表是怎麼最終新增到類上的,就拿例項方法列表來說吧: 在上述的程式碼片段裡,addUnattachedCategoryForClass只是把類和category做一個關聯對映,而remethodizeClass才是真正去處理新增事宜的功臣。
/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
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);
}
}
複製程式碼
可以看到:在remethodizeClass內部核心方法是:attachCategories,它才是真正地新增方法
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
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);
}
複製程式碼
需要注意的有兩點:
- category的方法沒有“完全替換掉”原來類已經有的方法,也就是說如果category和原來類都有methodA,那麼category附加完成之後,類的方法列表裡會有兩個methodA。
- category的方法被放到了新方法列表的前面,而原來類的方法被放到了新方法列表的後面,這也就是我們平常所說的category的方法會“覆蓋”掉原來類的同名方法,這是因為執行時在查詢方法的時候是順著方法列表的順序查詢的,它只要一找到對應名字的方法,就會罷休^_^,殊不知後面可能還有一樣名字的方法。
四、Category和Extension
-
Extension
- 在編譯期決議,是類的一部分,在編譯器和標頭檔案的@interface和實現檔案裡的@implement一起形成了一個完整的類。
- 伴隨著類的產生而產生,也隨著類的消失而消失。 Extension一般用來隱藏類的私有訊息,你必須有一個類的原始碼才能新增一個類的Extension,所以對於系統一些類,如NSString,就無法新增類擴充套件
-
Category
- 是執行期決議的
- 類擴充套件可以新增例項變數,分類不能新增例項變數
因為在執行期,物件的記憶體佈局已經確定,如果新增例項變數會破壞類的內部佈局,這對編譯性語言是災難性的。
更多資料:
Category
深入理解Objective-C:Category
Objective-C Category 的實現原理
iOS Category詳解