• 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/)
  • 相关阅读:
    从小白到全栈的前端学习路径
    Vue初学者可能不知道的坑
    vue的基础使用
    15-浮动
    02-css的选择器
    深入理解JavaScript的闭包
    深入理解JavaScript的闭包,前戏— 作用域和词法作用域
    JavaScript简介
    02-HTML5新的input属性
    springboot+spring session+redis+nginx实现session共享和负载均衡
  • 原文地址:https://www.cnblogs.com/dins/p/category.html
Copyright © 2020-2023  润新知