OC基础之分类Category

·  阅读 347

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做关联对象的清理工作。

参考文章:
深入理解Objective-C:Category
分类Category & 扩展 Extension

分类:
iOS