iOS分类Category探索

什么是Category?

Category是Objective-C 2.0之后添加的语言特性,Category的主要作用是为已经存在的类添加方法,一般称为分类,文件名格式是"NSObject+A.h"。

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;
    struct property_list_t *_classProperties;
}

从结构能看出分类可以扩展实例方法列表、类方法列表、协议列表,也支持扩展属性,但不支持扩展成员变量(之后会说)。

一般使用的场景有扩展现有类方法、代码分区、添加私有方法(不对外暴露category.h)、模拟多继承(使用关联对象的方式添加属性实现)


什么是Extension?

Extension一般被称为类扩展、匿名分类,用于定义私有属性和方法,不可被继承。只能依附自定义类写于.m中,定义一般为:

@interface ViewController ()

@property (nonatomic, strong) NSObject *obj;

@end

类扩展支持写在多个.h文件,但都必须在.m文件中引用,且不能有自己的实现。

类扩展很多时候会与分类搞混,我在文后问答环节详细整理了他们的区别。


Category如何加载的?

struct objc_class : objc_object {
    Class superclass;
    class_data_bits_t bits; 
    class_rw_t *data() {
        return bits.data();
    }
    ...
}

struct class_rw_t {
    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    ...
}

struct class_ro_t {
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; //只有ro才有实例变量表
    property_list_t *baseProperties;
    ...
};

先简单了解一下Class对象的结构,每个objc_class都包含有class_data_bits_t数据位,其中储存了class_rw_t的指针地址和一些其他标记。class_rw_t中包含有属性方法协议列表,以及class_ro_t指针地址。而在class_ro_t结构中,储存的是编译器决定的属性方法协议。

那么是怎么运行的呢?

在编译期类的结构中的class_data_bits_t指向的是一个 class_ro_t指针。

在运行时调用realizeClass方法,初始化一个class_rw_t结构体,设置ro值为原数据中的class_ro_t后设为数据位中的指向,最后调用methodizeClass方法加载。

static void methodizeClass(Class cls)
{
    auto rw = cls->data();
    auto ro = rw->ro;

    //从ro中加载方法表
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }
    //加载属性
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
    //加载协议
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }
    //基类添加初始化方法
    if (cls->isRootMetaclass()) {
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }
    //加载分类
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
    
    if (cats) free(cats);
}

可以看到,在methodizeClass中加载了原先类在编译期决定的方法属性和协议,然后获取了未连接的分类表,将列表中的扩展方法添加到运行期类中。


Category方法覆盖

如果不同的分类实现了相同名字的方法,那么调用时会使用最后加入的实现,这是为什么呢?

加载Category

dyld链接并初始化二进制文件后,交由ImageLoader读取,接着通知runtime处理,runtime调用map_images解析,然后执行_read_images分析文件中包含的类和分类。

//加载分类
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) {
        //分类指定的类还没加载,可能是链接库顺序的问题
        catlist[i] = nil;
        continue;
    }
    //添加分类到类的分类表中,伺机重载入
    bool classExists = NO;
    if (cat->instanceMethods ||  cat->protocols  
        ||  cat->instanceProperties) 
    {
        addUnattachedCategoryForClass(cat, cls, hi);
        if (cls->isRealized()) {
            remethodizeClass(cls);
            classExists = YES;
        }
    }
    //添加分类到元类中
    if (cat->classMethods  ||  cat->protocols  
        ||  (hasClassProperties && cat->_classProperties)) 
    {
        addUnattachedCategoryForClass(cat, cls->ISA(), hi);
        if (cls->ISA()->isRealized()) {
            remethodizeClass(cls->ISA());
        }
    }
}

添加方法属性和协议

如果有新增的分类,就分别添加到原类和meta类,并通过remethodizeClass更新,具体就是调用attachCategories方法把分类中所有的方法都添加到指定类中。

static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;

    bool isMeta = cls->isMetaClass();

    //新建数组指针
    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));

    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();
    //加载列表到rw中
    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);
}
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            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]));
            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]));
        }
    }

可以看到最后调用了rw->methods.attachLists(mlists, mcount); 把新增分类中的方法列表添加到实际运行时查询的方法列表头部。

在进行方法调用时会从头部查询,一旦查到后就返回结果,因此后编译的文件中的方法会被优先调用。

同时之前添加的方法实现也保存了,可以通过获取同名方法的方式查找原类的实现。


Category实现属性

分类不能添加成员变量

属性(Property)包含了成员变量(Ivar)和Setter&Getter。

可以在分类中定义属性,但由于分类是在运行时添加分类属性到类的属性列表中,所以并没有创建对应的成员变量和方法实现。

关联对象

如果我们想让分类实现添加新的属性,一般都通过关联对象的方式。

// 声明文件
@interface TestObject (Category)
@property (nonatomic, strong) NSObject *object;
@end

// 实现文件
static void *const kAssociatedObjectKey = (void *)&kAssociatedObjectKey;

@implementation TestObject (Category)

- (NSObject *)object {
    return objc_getAssociatedObject(self, kAssociatedObjectKey);
}

- (void)setObject:(NSObject *)object {
    objc_setAssociatedObject(self, kAssociatedObjectKey, object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

这种方式可以实现存取对象,但是不能获取_object变量。


问答

分类和扩展有什么区别?

1.分类多用于扩展方法实现,类扩展多用于申明私有变量和方法。

2.类扩展作用在编译期,直接和原类在一起,而分类作用在运行时,加载类的时候动态添加到原类中。

3.类扩展可以定义属性,分类中定义的属性只会申明setter/getter,并没有相关实现和变量。

分类有哪些局限性?

1.分类只能给现有的类加方法或协议,不能添加实例变量(ivar)。

2.分类添加的方法如果与现有的重名,会覆盖原有方法的实现。如果多个分类方法都重名,则根据编译顺序执行最后一个。

分类的结构体里面有哪些成员?

分类结构体包含了分类名,绑定的类,实例与类方法列表,实例与类方法属性以及协议表。


参考

深入理解Objective-C:Category

神经病院 Objective-C Runtime 入院第一天—— isa 和 Class

探秘Runtime - 深入剖析Category

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    萌萌的小伟哥阅读 539评论 0 1
  • 前言 在 iOS 开发中,使用的编程语言主要是 Objective-C。这一种编程语言虽然是 C/C++ 的扩展,...
    Anyeler阅读 737评论 0 11
  • pdf下载地址:Java面试宝典 第一章内容介绍 20 第二章JavaSE基础 21 一、Java面向对象 21 ...
    王震阳阅读 83,057评论 26 522
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 79,404评论 12 120
  • 现在回忆一下,你第一次听到同学升级当父母是什么时候? 我在上大一的时候就看到初中的同学群因为某位同学升级当爸爸而沸...
    绛哥儿阅读 318评论 13 0