Category

Objective-C 中的 Category 是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。

分类可以拓展类的属性、方法、协议等信息

一、使用场景

根据苹果官方文档对 Category 的描述,它的使用场景主要有三个:

  1. 给现有的类添加方法;
  2. 将一个类的实现拆分成多个独立的源文件;
  3. 声明私有的方法。

其中,第 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 做了如下处理:

  1. 将 Category 和它的主类(或元类)注册到哈希表中;
  2. 如果主类(或元类)已实现,那么重建它的方法列表。

在这里分了两种情况进行处理: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)、propertiesprotocols 的值。进一步,我们通过 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  

但是可以大概猜出它的实现思路:

  1. 由于 initialize 是第一次接受到消息调用,所以 initialize 的调用是在 objc_msgSend 方法里,所以它的调用顺序应该是在最前面,而且是只调用一次的判断;
  2. 通过 isa 寻找类/元类对象,寻找方法调用;
  3. 如果 isa 没有寻找到对应的方法,则通过 superClass 寻找父类是否有这个方法,调用。

五、内容来源

宁夏灼雪__ & iOS底层day4 - 探索Category的实现

[雷纯锋的技术博客](http://blog.leichunfeng.com/) - [Objective-C Category 的实现原理](http://blog.leichunfeng.com/blog/2015/05/18/objective-c-category-implementation-principle/)
posted @ 2020-02-26 11:16  和风细羽  阅读(1335)  评论(0编辑  收藏  举报