分类(Category)

作者: 眷卿三世 | 来源:发表于2019-08-07 00:36 被阅读0次

    作用:

    当为类添加新方法时,OC一共提供了4中方式:

    1. 直接在类中再写一个方法,缺点:破坏了类的封装性。
    2. 通过继承再子类里扩展一个方法,缺点:开销大、增加代码复杂度
    3. 通过协议实现扩展一个类的方法,优点:该方式能够大大降低代码的耦合度,但是实现上会更复杂。
    4. 最后一种就是使用Category。

    优点和缺点:

    Category优点:

    • 在不改变一个类的情况下,对一个已存在的类添加新的方法
    • 可以在没有源代码的情况下对框架内的类进行扩展,例如NSString。
    • 减小个文件的体积
    • 可以按需加载不同的Category

    Category缺点:

    • 方法的扩展是硬编码,不能动态添加
    • Category中方法的优先级高于原类中的方法,所以Category中的方法可能会覆盖原有类中相同名字的方法,从而造成未知问题。
    • 不能添加成员变量(并非不能,只不过比较复杂)
    • 可以添加属性,但是不会自动生成getter和setter方法,需要通过关联对象实现。
    • 同一个类的Category中不能有重复名字的方法。

    Category实现的原理:

    Category源码在:objc-runtime-new.h和objc-runtime-new.mm这两个文件中

    Category runtime中的定义:

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

    通过:

     struct method_list_t *instanceMethods;
     struct method_list_t *classMethods;
     struct protocol_list_t *protocols;
     struct property_list_t *instanceProperties;
    

    结构体中这四个变量可以看出,分类可以添加实例方法、类方法、协议和属性列表,但是唯独不能添加实例变量,因为没有存储实例变量对应的指针变量。

    Category加载的详细步骤和调用到的函数:

    _objc_init(void)->map_images(...)->map_images_nolock(...)->_read_images(...)->remethodizeClass(Class cls)->attachCategories(Class cls, category_list *cats, bool flush_caches)->void attachLists(List* const * addedLists, uint32_t addedCount
    

    函数介绍:

    • _objc_init:是runtime的入口函数,进行一些初始化操作
    • map_images:加锁
    • map_images_nolock:完成所有类的注册和fixup等工作,还包括一些初始化工作以及调用load类方法。
    • _read_images:完成类的加载、协议的加载、类别的加载等工作
    • remethodizeClass:将类别绑定到目标类上
    • attachCategories:将类别中的方法和属性绑定到目标类上。
    • attachLists:将目标类中的方法和分类中的方法放到一个列表中。

    主要涉及Category原理的三个方法:_read_images、attachCategories、attachLists
    来看看_read_images函数源码重要的部分:

    // Discover categories. 
        for (EACH_HEADER) {
            category_t **catlist = 
                _getObjc2CategoryList(hi, &count);
            bool hasClassProperties = hi->info()->hasCategoryClassProperties();
    
            for (i = 0; i < count; i++) {
                category_t *cat = catlist[i];
                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);
                    }
                }
            }
        }
    
        ts.log("IMAGE TIMES: discover categories");
    
        // Category discovery MUST BE LAST to avoid potential races 
        // when other threads call the new category code before 
        // this thread finishes its fixups.
    
        // +load handled by prepare_load_methods()
    
        if (DebugNonFragileIvars) {
            realizeAllClasses();
        }
    
    
        // Print preoptimization statistics
        if (PrintPreopt) {
            static unsigned int PreoptTotalMethodLists;
            static unsigned int PreoptOptimizedMethodLists;
            static unsigned int PreoptTotalClasses;
            static unsigned int PreoptOptimizedClasses;
    
            for (EACH_HEADER) {
                if (hi->isPreoptimized()) {
                    _objc_inform("PREOPTIMIZATION: honoring preoptimized selectors "
                                 "in %s", hi->fname());
                }
                else if (hi->info()->optimizedByDyld()) {
                    _objc_inform("PREOPTIMIZATION: IGNORING preoptimized selectors "
                                 "in %s", hi->fname());
                }
    
                classref_t *classlist = _getObjc2ClassList(hi, &count);
                for (i = 0; i < count; i++) {
                    Class cls = remapClass(classlist[i]);
                    if (!cls) continue;
    
                    PreoptTotalClasses++;
                    if (hi->isPreoptimized()) {
                        PreoptOptimizedClasses++;
                    }
                    
                    const method_list_t *mlist;
                    if ((mlist = ((class_ro_t *)cls->data())->baseMethods())) {
                        PreoptTotalMethodLists++;
                        if (mlist->isFixedUp()) {
                            PreoptOptimizedMethodLists++;
                        }
                    }
                    if ((mlist=((class_ro_t *)cls->ISA()->data())->baseMethods())) {
                        PreoptTotalMethodLists++;
                        if (mlist->isFixedUp()) {
                            PreoptOptimizedMethodLists++;
                        }
                    }
                }
            }
    
            _objc_inform("PREOPTIMIZATION: %zu selector references not "
                         "pre-optimized", UnfixedSelectors);
            _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) method lists pre-sorted",
                         PreoptOptimizedMethodLists, PreoptTotalMethodLists, 
                         PreoptTotalMethodLists
                         ? 100.0*PreoptOptimizedMethodLists/PreoptTotalMethodLists 
                         : 0.0);
            _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) classes pre-registered",
                         PreoptOptimizedClasses, PreoptTotalClasses, 
                         PreoptTotalClasses 
                         ? 100.0*PreoptOptimizedClasses/PreoptTotalClasses
                         : 0.0);
            _objc_inform("PREOPTIMIZATION: %zu protocol references not "
                         "pre-optimized", UnfixedProtocolReferences);
        }
    

    其中在代码中:

     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" : "");
         }
     }
    

    在有注释的地方,就是重要关键点:1、去绑定分类和目标类。2、重新构建方法列表。
    接下来我们来看attachCategories函数:

    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_list_t **mlists = (method_list_t **)
            malloc(cats->count * sizeof(*mlists));
        property_list_t **proplists = (property_list_t **)
            malloc(cats->count * sizeof(*proplists));
        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;
        int i = cats->count;
        bool fromBundle = NO;
        while (i--) {
            auto& entry = cats->list[i];
    
            method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
            if (mlist) {
                mlists[mcount++] = mlist;
                fromBundle |= entry.hi->isBundle();
            }
    
            property_list_t *proplist = 
                entry.cat->propertiesForMeta(isMeta, entry.hi);
            if (proplist) {
                proplists[propcount++] = proplist;
            }
    
            protocol_list_t *protolist = entry.cat->protocols;
            if (protolist) {
                protolists[protocount++] = protolist;
            }
        }
    
        auto rw = cls->data();
    
        prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
        rw->methods.attachLists(mlists, mcount);
        free(mlists);
        if (flush_caches  &&  mcount > 0) flushCaches(cls);
    
        rw->properties.attachLists(proplists, propcount);
        free(proplists);
    
        rw->protocols.attachLists(protolists, protocount);
        free(protolists);
    }
    

    从源码可以看出,这里是将目标类中所有扩展出来的类别中的属性、方法、协议列表头取出放到一个列表里面,然后统一交给attachLists这个函数去处理。
    接下来在看看attachLists源码:

    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;
    
        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }
    

    这里强调一下,oldCount和newCount分别代表的是目标类中方法、属性、协议列表的长度还有类别中的方法、属性、协议列表的长度。通过realloc函数申请内存,这里使用realloc申请内存有几种情况,大家可以自行查阅,这里不再描述,当realloc重新申请好内存后,使用memmove和memcpy这两个函数,将列表内容放到我们重新申请的内存中,从这里可以得到,Category中的方法并没有覆盖目标类中的方法,只不过将Category中的方法放到了目标类方法的前面而已,在调用方法的时候,若Category中和目标类中有同名的方法,系统会先找到放到前面的类别中的方法,这就是为什么Category中方法的优先级高于目标类中的方法。

    Category添加属性:

    尽管在源码中我们看到了指向属性列表的指针变量,但是系统并没有给这里的属性生成getter和setter函数,需要手段生成实例变量,并为其生成getter和setter方法。

    相关文章

      网友评论

        本文标题:分类(Category)

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