category是Objective-C 2.0之后添加的语言特性,其主要设计思想就是对装饰模式的一种具体实现,可以动态地为已有类添加新行为。本文将从多方面整理分类相关知识点
分类的作用
苹果官方推荐的用法如下:
- 给现有的类添加方法;
- 可以把类的实现分开在几个不同的文件里面,这样做的好处如下:
- 可以减少单个文件的体积
- 可以把不同的功能组织到不同的category里
- 可以由多个开发者共同完成一个类
- 可以按需加载想要的category 等等。
- 声明私有方法
除此之外,广大开发者还衍生出了category的其他几个使用场景:
- 模拟多继承
- 把framework的私有方法公开:在分类中声明framework的私有方法,使用者即可调用对应的私有方法,目前这个场景已经废了,苹果审核不允许开发者调用他们的私有方法。 具体可以参看文章分类Category & 扩展 Extension
分类的实现
category 的底层是结构体,具体的代码可以在runtime源码中的<objc-runtime-new.h>文件中找到,它包含了:
- 类的名字(name)
- 类(cls)
- category中所有给类添加的实例方法的列表(instanceMethods)
- category中所有添加的类方法的列表(classMethods)
- category实现的所有协议的列表(protocols)
- category中添加的所有属性(instanceProperties)
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的定义也可以看出category的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)和不可为(无法添加实例变量)。
分类的加载
category被附加到类上面是在map_images的时候发生的,_objc_init里面的调用的map_images最终会调用objc-runtime-new.mm里面的_read_images方法,而在_read_images方法的结尾,有以下的代码片段:
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
class_t *cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = NULL;
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 (isRealized(cls)) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
getName(cls), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
/* || cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->isa, hi);
if (isRealized(cls->isa)) {
remethodizeClass(cls->isa);
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
getName(cls), cat->name);
}
}
}
}
这段代码很容易理解,此时编译器主要做了两件事情:
- 1)、把category的实例方法、协议以及属性添加到类上
- 2)、把category的类方法和协议添加到类的metaclass上
上述代码中addUnattachedCategoryForClass只是把类和category做一个关联映射,而remethodizeClass才是真正去处理添加事宜的功臣,以添加实例方法举例
static void remethodizeClass(class_t *cls)
{
category_list *cats;
BOOL isMeta;
rwlock_assert_writing(&runtimeLock);
isMeta = isMetaClass(cls);
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls))) {
chained_property_list *newproperties;
const protocol_list_t **newprotos;
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
getName(cls), isMeta ? "(meta)" : "");
}
// Update methods, properties, protocols
BOOL vtableAffected = NO;
//添加分类方法
attachCategoryMethods(cls, cats, &vtableAffected);
//添加分类属性
newproperties = buildPropertyList(NULL, cats, isMeta);
if (newproperties) {
newproperties->next = cls->data()->properties;
cls->data()->properties = newproperties;
}
//添加分类协议
newprotos = buildProtocolList(cats, NULL, cls->data()->protocols);
if (cls->data()->protocols && cls->data()->protocols != newprotos) {
_free_internal(cls->data()->protocols);
}
cls->data()->protocols = newprotos;
_free_internal(cats);
// Update method caches and vtables
flushCaches(cls);
if (vtableAffected) flushVtables(cls);
}
}
对于添加类的实例方法而言,又会去调用attachCategoryMethods这个方法,我们去看下attachCategoryMethods:
static void
attachCategoryMethods(class_t *cls, category_list *cats,
BOOL *inoutVtablesAffected)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
BOOL isMeta = isMetaClass(cls);
method_list_t **mlists = (method_list_t **)
_malloc_internal(cats->count * sizeof(*mlists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int i = cats->count;
BOOL fromBundle = NO;
while (i--) {
method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= cats->list[i].fromBundle;
}
}
attachMethodLists(cls, mlists, mcount, NO, fromBundle, inoutVtablesAffected);
_free_internal(mlists);
}
attachCategoryMethods做的工作相对比较简单,它只是把所有category的实例方法列表拼成了一个大的实例方法列表,然后转交给了attachMethodLists方法,我们只看其中一小段:
for (uint32_t m = 0;
(scanForCustomRR || scanForCustomAWZ) && m < mlist->count;
m++)
{
SEL sel = method_list_nth(mlist, m)->name;
if (scanForCustomRR && isRRSelector(sel)) {
cls->setHasCustomRR();
scanForCustomRR = false;
} else if (scanForCustomAWZ && isAWZSelector(sel)) {
cls->setHasCustomAWZ();
scanForCustomAWZ = false;
}
}
// Fill method list array
newLists[newCount++] = mlist;
.
.
.
// Copy old methods to the method list array
for (i = 0; i < oldCount; i++) {
newLists[newCount++] = oldLists[i];
}
通过上述实现原理可以看出,category的方法没有“完全替换掉”原来类已经有的方法,而是将category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法。也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA,只是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休。当有多个category覆盖相同方法时,其调用顺序由编译顺序决定。
扩展extension
扩展的作用
- 声明私有属性,是可以不对子类暴露的
- 声明私有方法,方便阅读
- 声明私有成员变量
扩展的特点
- 编译时决议
- 只以声明的形式存在,没有具体实现,多数情况寄生于宿主类的.m中,也就是说,它不是独立存在实现的一个文件
- 可以把扩展理解为类的一个内部的私有声明
- 不能为系统类添加扩展
分类和扩展的区别
- 分类是运行时决议,扩展是编译时决议
- 分类可以有声明有实现,而扩展只有声明,它的实现是直接写在宿主类当中
- 可以为系统类添加分类,但不能为系统类添加扩展
+load vs +initialize
调用时机
在 NSObject 类中有两个非常特殊的类方法 +load 和 +initialize ,用于类的初始化。
- +load 方法是当类或分类被添加到 Objective-C runtime 时被调用的,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。
- +initialize 方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说 +initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize 方法是永远不会被调用的。
调用顺序
- 当父类、子类、分类同时有+load方法时,会先调用父类,再调用子类,最后调用分类,多个分类时,根据编译顺序确定调用顺序。
- 当父类、子类、分类同时有+initialize方法时,会优先调用父类,再调用子类,如果此时父类已经调用过+initialize方法,则不会再调用。由于调用+initialize方法本质是走消息转发,所以分类会覆盖子类+initialize方法。
是否沿用父类实现
- 子类未实现+load方法时,子类不会沿用父类的实现,因此父类的+load方法仅在父类被添加到runtime时被调用一次
- 子类未实现+initialize方法时,子类会沿用父类的实现,因此父类的+initialize方法会因为子类未实现+initialize方法而多次被调用 总结如下:
| +load | +initialize | |
|---|---|---|
| 调用时机 | 被添加到 runtime 时 | 收到第一条消息前,可能永远不调用 |
| 调用顺序 | 父类->子类->分类 | 父类->子类 |
| 是否需要显式调用父类实现 | 否 | 否 |
| 沿用父类的实现 | 否 | 是 |
| 调用次数 | 1次 | 多次 |
category和关联对象
通过上述分析,我们知道在category里面是无法为category添加实例变量的。但是我们很多时候需要在category中添加和对象关联的值,值得庆幸的是,我们可以通过 Associated Objects 来弥补这一不足。与 Associated Objects 相关的函数主要有三个,我们可以在 runtime 源码的 runtime.h 文件中找到它们的声明:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);
这三个函数的命名对程序员非常友好,可以让我们一眼就看出函数的作用:
- objc_setAssociatedObject 用于给对象添加关联对象,传入 nil 则可以移除已有的关联对象;
- objc_getAssociatedObject 用于获取关联对象;
- objc_removeAssociatedObjects 用于移除一个对象的所有关联对象。
但是关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?我们去翻一下runtime的源码,在objc-references.mm文件中有个方法_object_set_associative_reference:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
_class_setInstancesHaveAssociatedObjects(_object_getClass(object));
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
我们可以看到所有的关联对象都由AssociationsManager管理,AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。
而在对象的销毁逻辑里面,见objc-runtime-new.mm:
void *objc_destructInstance(id obj)
{
if (obj) {
Class isa_gen = _object_getClass(obj);
class_t *isa = newcls(isa_gen);
// Read all of the flags at once for performance.
bool cxx = hasCxxStructors(isa);
bool assoc = !UseGC && _class_instancesHaveAssociatedObjects(isa_gen);
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (!UseGC) objc_clear_deallocating(obj);
}
return obj;
}
runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。