iOS runtime 之 Category

作者: hi_xgb | 来源:发表于2016-04-01 19:59 被阅读2488次

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

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

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

    相关文章

      网友评论

      • GeniusWong:你好, 想请教一个问题 , Category 声明一个类方法,实现的时候是用的实例方法, 我在用的时候,也可以正常调用 ,请问这是什么原理?
      • 起个名字想破头:"由于 Category 的方法会插入到原始类之前,我们要注意不要用 Category 来覆盖原始类的方法"
        这句话在别的文章中也看过相同的表述,但还有点疑问。
        创建一个BaseViewController(继承UIViewController);再创建一个UIViewController的分类Example
        1.Example中增加一个ViewDidLoad方法。运行发现,Example和BaseViewController中的ViewDidLoad都能得到调用。
        2.Example和BaseViewController中增加一个自定义方法(如:print)。运行,发现只会调用Example的print

        是不是可以说明,并不是所有分类方法都会覆盖原类方法,对于系统已经定义的这些方法,如果使用分类进行覆盖,则都会得到调用。像MJRefresh的demo中,就写了一个UIViewController的分类并重写了dealloc来记录ViewController的销毁。如果这样的话,就可以将一些需要统一处理的东西做到分类里。
        起个名字想破头:@木子夕 好久没登简书了,刚试了一下,确实像你说的,我在BaseViewController的ViewDidLoad方法中调用了super 的ViewDidLoad,就会调用分类中的方法。如果不调用super,就不会有分类方法的调用。
        这确实也是对的,系统调用BaseViewController的ViewDidLoad方法,而BaseViewController调用了super的ViewDidLoad方法。而super的ViewDidLoad方法被分类方法覆盖。所以就形成了先执行super分类的方法,再执行BaseViewController的ViewDidLoad方法。
        MoussyL:层主~ 你这个问题弄明白了吗? 求分享
        之前好像看到过有文章说,viewDidLoad 这个问题是因为,在原来的类里调用了 [super viewDidLoad] 的原因,还是不太清楚~ :joy:
      • Romit_lee:这篇文章收藏了,写的很深入,值得去阅读源码做深入研究

      本文标题:iOS runtime 之 Category

      本文链接:https://www.haomeiwen.com/subject/qhuylttx.html