搜索
您的当前位置:首页正文

iOS runtime 之 Category

来源:二三娱乐

我们知道 Objective - C 中 Category 主要有以下作用:

  1. 不改变原有类的实现对类添加新的接口
  2. 将类的接口按功能模块分类,模块更清晰
  3. 声明私有方法

我们还知道,即使没有引入 Category 的头文件,Category 的方法也会被添加进主类的方法列表里,可以通过 performSelector 的方式使用,导入头文件只是为了通过编译器的静态检查。

那么 Category 是如何添加到主类里的呢?下面我们一起来学习记录下。

Category 的结构

首先了解下 Category 的结构,打开 runtime.h,我们看到了 Category 的定义

/// An opaque type that represents a category.
typedef struct objc_category *Category;

是指向 objc_category 的 C 结构体,定义如下

struct objc_category {
    char *category_name                                      OBJC2_UNAVAILABLE;
    char *class_name                                         OBJC2_UNAVAILABLE;
    struct objc_method_list *instance_methods                OBJC2_UNAVAILABLE;
    struct objc_method_list *class_methods                   OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
}

通过上面的结构体,我们可以很清楚的了解存储的内容。

typedef struct category_t {
    const char *name;
    struct class_t *cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct objc_property_list *instanceProperties;
} category_t;

其中,

  • name 是指 class_name 而不是 category_name
  • cla 是指要扩展的类
  • objc_property_list 是指 Category 中所有的 properties,也就是我们通过 objc_setAssociatedObject 动态添加的属性

Category 的加载过程

打开 objc 源码中,在 libobjc.order 中我们能看到如下的执行顺序

__objc_init
_map_images
_sel_registerName
___sel_registerName
__objc_search_builtins
_phash
_lookup
_exception_init
__getImageSlide
__getObjcImageInfo
_getsegbynamefromheader
__getObjcModules
__malloc_internal
__objc_internal_zone
_verify_gc_readiness
_gc_init
_rtp_init
__read_images
...

我们要关注的是 _read_images,在这个方法里 load 了所有的类、协议和 Category,其中加载 Category 的代码段如下:

// Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            // Do NOT use cat->cls! It may have been remapped.
            class_t *cls = remapClass(cat->cls);

            // 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);
                }
            }
        }
    }

    // Category discovery MUST BE LAST to avoid potential races 
    // when other threads call the new category code before 
    // this thread finishes its fixups.
static void 
attachCategoryMethods(class_t *cls, category_list *cats, 
                      BOOL *outVtablesAffected)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    BOOL isMeta = isMetaClass(cls);
    method_list_t **mlists = _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, fromBundle, outVtablesAffected);

    _free_internal(mlists);

}

这里面的主要操作就是取出 category_list 的所有方法列表,然后倒序添加到 mlists 中,最后再将 mlists 正序添加到被扩展的类中。因此,新生成的 Category 的方法会优先添加到方法列表里。

如果原来类的方法列表是 A、B,Category 的方法列表是 C、D,那么添加后的方法列表是 C、D、A、B。

至此,Category 的方法便被添加到了类中。

由于 Category 的方法会插入到原始类之前,我们要注意不要用 Category 来覆盖原始类的方法

这是我写的 runtime 系列文章中的一篇,还有以下几篇从其他方面对 runtime 进行了介绍

参考资料:

如果您觉得本文对您有所帮助,请点击「喜欢」来支持我。

转载请注明出处,有任何疑问都可联系我,欢迎探讨。

Top