Category
Objective-C 中的 Category 是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。
分类可以拓展类的属性、方法、协议等信息
一、使用场景
根据苹果官方文档对 Category 的描述,它的使用场景主要有三个:
- 给现有的类添加方法;
- 将一个类的实现拆分成多个独立的源文件;
- 声明私有的方法。
其中,第 1 个是最典型的使用场景,应用最广泛。
注:Category 有一个非常容易误用的场景,那就是用 Category 来覆写父类或主类的方法。虽然目前 Objective-C 是允许这么做的,但是这种使用场景是非常不推荐的。使用 Category 来覆写方法有很多缺点,比如不能覆写 Category 中的方法、无法调用主类中的原始实现等,且很容易造成无法预估的行为。
二、底层结构
打开 runtime 源码工程,在文件 objc-runtime-new.mm
中找到以下函数:
/***********************************************************************
* _read_images
* Perform initial processing of the headers in the linked
* list beginning with headerList.
*
* Called by: map_images_nolock
*
* Locking: runtimeLock acquired by map_images
**********************************************************************/
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
...
// Discover categories.
// 分类相关代码
for (EACH_HEADER) {
// 通过 _getObjc2CategoryList 函数获取到分类列表
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
// 循环遍历
for (i = 0; i < count; i++) {
// 分类的底层结构体
category_t *cat = catlist[i];
// (classref_t) cls = 0x0000000100b0a140 -》NSObject
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 和它的主类(或元类)注册到哈希表中;
- 如果主类(或元类)已实现,那么重建它的方法列表。
在这里分了两种情况进行处理:Category 中的实例方法和属性被整合到主类中;而类方法则被整合到元类中。另外,对协议的处理比较特殊,Category 中的协议被同时整合到了主类和元类中。
我们注意到,不管是哪种情况,最终都是通过调用 static void remethodizeClass(Class cls)
函数来重新整理类的数据的。
/***********************************************************************
* 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.assertLocked();
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);
}
}
这个函数的主要作用是将 Category 中的方法、属性和协议整合到类(主类或元类)中,更新类的数据字段 data() 中 method_lists
(或 method_list)、properties
和 protocols
的值。进一步,我们通过 attachCategoryMethods
函数的源码可以找到真正处理 Category 方法的 attachMethodLists
函数:
// 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.
/**
* @brief 将 Category 的方法列表、协议列表、属性列表附加到类对象中。一个类可以有多个分类,所以这里是 cats 数组
*/
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_t, method_t], @[method_t .....] ] */
method_list_t **mlists = (method_list_t **)malloc(cats->count * sizeof(*mlists));
/* 属性数组 @[ @[property_t, property_t], @[property_t .....] ] */
property_list_t **proplists = (property_list_t **)malloc(cats->count * sizeof(*proplists));
/* 协议数组 @[ @[peotocol_t, peotocol_t], @[peotocol_t .....] ] */
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;
// cls 类的分类个数
int i = cats->count;
bool fromBundle = NO;
// 遍历拿到每个分类,取出所有分类的方法、属性、协议,并将它们各自添加到一个二维数组里,最后再通过 attachLists 将它们添加到类对象中。
while (i--) {
auto& entry = cats->list[i];
// 将所有分类的实例方法,添加到 mlist 数组中
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 将所有分类的属性,添加到 proplist 数组中
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
// 将所有分类的协议,添加到 protolist 数组中
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
// rw:class_rw_t 结构体,class 结构体中用来存储对象方法、属性、协议的结构体
auto rw = cls->data();
// 将 mlists 数组传入 rw->method 的 attachLists 函数,然后释放 mlists
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
// 将 proplists 数组传入 rw->properties 的 attachLists 函数,然后释放 proplists
rw->properties.attachLists(proplists, propcount);
free(proplists);
// 将 protolists 数组传入 rw->protocols 的 attachLists 函数,然后释放 protocols
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
它的主要作用就是取出所有分类的方法、属性、协议,并将他们各自添加到一个二维数组里,最后再通过 attachLists 将他们添加到类对象中。通过探究这个处理过程,我们也印证了一个结论,那就是主类中的方法和 Category 中的方法在 runtime 看来并没有区别,它们是被同等对待的,都保存在主类的方法列表中。
严格意义上讲 Category 中的 +load 方法跟普通方法一样也会对主类中的 +load 方法造成覆盖,只不过 runtime 在自动调用主类和 Category 中的 +load 方法时是直接使用各自方法的指针进行调用的。所以才会使我们觉得主类和 Category 中的 +load 方法好像互不影响一样。因此,当我们手动给主类发送 +load 消息时,调用的一直会是分类中的 +load 方法。
在 objc-4 的源码中,搜索 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_t 就是一个分类的结构体,而我们所创建的的一个分类其实就是一个 category_t 的结构体,category_t 里面的结构跟类对象的结构很相似,包含了 name(名称,类名),instanceMethods(对象方法)、classMethods(类方法)、protocols(协议)、属性等。
在编译的时候,分类的属性、方法、协议等会先存储在这个结构体里面,在运行的时候,使用 runtime 动态的把分类里面的方法、属性、协议等添加到类对象(元类对象)中,具体源码可以查看。源码解读顺序:
objc-os.mm
- _objc_init()
- map_images()
- map_images_nolock()
objc-runtime-new.mm
- _read_images()
- remethodizeClass()
- attachCategories()
- attachLists()
- realloc、memmove、memcpy
三、Category 和 Class Extension 的区别
Class Extension:
@interface Person ()
@property (nonatomic, assign) int sex;
- (void)isBig;
@end
将属性、方法等封装在 .m 文件里面,类似 private 的应用。 区别:Class Extension 在编译的时候,数据就已经包含类信息里了;Category 是在运行时,通过 runtime 将数据合并到类信息中。
四、objc_msgSend() 方法实现
在 objc4 源码中搜索 objc_msgSend 发现这个方法是由汇编实现的
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd, ...);
* IMP objc_msgLookup(id self, SEL _cmd, ...);
*
* objc_msgLookup ABI:
* IMP returned in x17
* x16 reserved for our use but not used
*
********************************************************************/
.data
.align 3
.globl _objc_debug_taggedpointer_classes
_objc_debug_taggedpointer_classes:
.fill 16, 8, 0
.globl _objc_debug_taggedpointer_ext_classes
_objc_debug_taggedpointer_ext_classes:
.fill 256, 8, 0
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative)
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
但是可以大概猜出它的实现思路:
- 由于 initialize 是第一次接受到消息调用,所以 initialize 的调用是在 objc_msgSend 方法里,所以它的调用顺序应该是在最前面,而且是只调用一次的判断;
- 通过 isa 寻找类/元类对象,寻找方法调用;
- 如果 isa 没有寻找到对应的方法,则通过 superClass 寻找父类是否有这个方法,调用。