iOS runtime 之 Category

96
hi_xgb
0.1 2016.04.01 19:59* 字数 788

我们知道 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;
}

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

我们接着下载 objc 的源码,打开 objc-runtime-new.h,有如下定义:

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.

通过上面的代码我们发现,实例方法被加入到当前的类对象中,类方法被加入到当前 Class 的 MetaClass 中,(Class 和 MetaClass 的概念可以查看我之前写的这篇文章)。方法的添加逻辑主要是在 remethodizeClassattachCategoryMethods 里执行。

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 进行了介绍

  1. iOS runtime之消息转发
  2. iOS runtime 之 Class 和 MetaClass
  3. 深入理解 Objective-C 的方法调用流程
  4. Objective-C 深入理解 +load 和 +initialize

参考资料:

  1. http://blog.leichunfeng.com/blog/2015/05/18/objective-c-category-implementation-principle/
  2. http://chun.tips/blog/2014/11/06/bao-gen-wen-di-objective%5Bnil%5Dc-runtime(3)%5Bnil%5D-xiao-xi-he-category/

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

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


欢迎大家关注我们团队的公众号,不定期分享各类技术干货


image
日记本
Web note ad 1