zoukankan      html  css  js  c++  java
  • iOS分类Category探索

    什么是Category?

    Category是Objective-C 2.0之后添加的语言特性,Category的主要作用是为已经存在的类添加方法,一般称为分类,文件名格式是"NSObject+A.h"。

    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;
        struct property_list_t *_classProperties;
    }
    

    从结构能看出分类可以扩展实例方法列表、类方法列表、协议列表,也支持扩展属性,但不支持扩展成员变量(之后会说)。

    一般使用的场景有扩展现有类方法、代码分区、添加私有方法(不对外暴露category.h)、模拟多继承(使用关联对象的方式添加属性实现)


    什么是Extension?

    Extension一般被称为类扩展、匿名分类,用于定义私有属性和方法,不可被继承。只能依附自定义类写于.m中,定义一般为:

    @interface ViewController ()
    
    @property (nonatomic, strong) NSObject *obj;
    
    @end
    

    类扩展支持写在多个.h文件,但都必须在.m文件中引用,且不能有自己的实现。

    类扩展很多时候会与分类搞混,我在文后问答环节详细整理了他们的区别。


    Category如何加载的?

    struct objc_class : objc_object {
        Class superclass;
        class_data_bits_t bits; 
        class_rw_t *data() {
            return bits.data();
        }
        ...
    }
    
    struct class_rw_t {
        const class_ro_t *ro;
    
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
        ...
    }
    
    struct class_ro_t {
        const char * name;
        method_list_t * baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars; //只有ro才有实例变量表
        property_list_t *baseProperties;
        ...
    };
    

    先简单了解一下Class对象的结构,每个objc_class都包含有class_data_bits_t数据位,其中储存了class_rw_t的指针地址和一些其他标记。class_rw_t中包含有属性方法协议列表,以及class_ro_t指针地址。而在class_ro_t结构中,储存的是编译器决定的属性方法协议。

    那么是怎么运行的呢?

    在编译期类的结构中的class_data_bits_t指向的是一个 class_ro_t指针。

    在运行时调用realizeClass方法,初始化一个class_rw_t结构体,设置ro值为原数据中的class_ro_t后设为数据位中的指向,最后调用methodizeClass方法加载。

    static void methodizeClass(Class cls)
    {
        auto rw = cls->data();
        auto ro = rw->ro;
    
        //从ro中加载方法表
        method_list_t *list = ro->baseMethods();
        if (list) {
            prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
            rw->methods.attachLists(&list, 1);
        }
        //加载属性
        property_list_t *proplist = ro->baseProperties;
        if (proplist) {
            rw->properties.attachLists(&proplist, 1);
        }
        //加载协议
        protocol_list_t *protolist = ro->baseProtocols;
        if (protolist) {
            rw->protocols.attachLists(&protolist, 1);
        }
        //基类添加初始化方法
        if (cls->isRootMetaclass()) {
            addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
        }
        //加载分类
        category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
        attachCategories(cls, cats, false /*don't flush caches*/);
        
        if (cats) free(cats);
    }
    

    可以看到,在methodizeClass中加载了原先类在编译期决定的方法属性和协议,然后获取了未连接的分类表,将列表中的扩展方法添加到运行期类中。


    Category方法覆盖

    如果不同的分类实现了相同名字的方法,那么调用时会使用最后加入的实现,这是为什么呢?

    加载Category

    dyld链接并初始化二进制文件后,交由ImageLoader读取,接着通知runtime处理,runtime调用map_images解析,然后执行_read_images分析文件中包含的类和分类。

    //加载分类
    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) {
            //分类指定的类还没加载,可能是链接库顺序的问题
            catlist[i] = nil;
            continue;
        }
        //添加分类到类的分类表中,伺机重载入
        bool classExists = NO;
        if (cat->instanceMethods ||  cat->protocols  
            ||  cat->instanceProperties) 
        {
            addUnattachedCategoryForClass(cat, cls, hi);
            if (cls->isRealized()) {
                remethodizeClass(cls);
                classExists = YES;
            }
        }
        //添加分类到元类中
        if (cat->classMethods  ||  cat->protocols  
            ||  (hasClassProperties && cat->_classProperties)) 
        {
            addUnattachedCategoryForClass(cat, cls->ISA(), hi);
            if (cls->ISA()->isRealized()) {
                remethodizeClass(cls->ISA());
            }
        }
    }
    

    添加方法属性和协议

    如果有新增的分类,就分别添加到原类和meta类,并通过remethodizeClass更新,具体就是调用attachCategories方法把分类中所有的方法都添加到指定类中。

    static void attachCategories(Class cls, category_list *cats, bool flush_caches)
    {
        if (!cats) return;
    
        bool isMeta = cls->isMetaClass();
    
        //新建数组指针
        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));
    
        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();
        //加载列表到rw中
        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);
    }
    
    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]));
            }
        }
    

    可以看到最后调用了rw->methods.attachLists(mlists, mcount); 把新增分类中的方法列表添加到实际运行时查询的方法列表头部。

    在进行方法调用时会从头部查询,一旦查到后就返回结果,因此后编译的文件中的方法会被优先调用。

    同时之前添加的方法实现也保存了,可以通过获取同名方法的方式查找原类的实现。


    Category实现属性

    分类不能添加成员变量

    属性(Property)包含了成员变量(Ivar)和Setter&Getter。

    可以在分类中定义属性,但由于分类是在运行时添加分类属性到类的属性列表中,所以并没有创建对应的成员变量和方法实现。

    关联对象

    如果我们想让分类实现添加新的属性,一般都通过关联对象的方式。

    // 声明文件
    @interface TestObject (Category)
    @property (nonatomic, strong) NSObject *object;
    @end
    
    // 实现文件
    static void *const kAssociatedObjectKey = (void *)&kAssociatedObjectKey;
    
    @implementation TestObject (Category)
    
    - (NSObject *)object {
        return objc_getAssociatedObject(self, kAssociatedObjectKey);
    }
    
    - (void)setObject:(NSObject *)object {
        objc_setAssociatedObject(self, kAssociatedObjectKey, object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    @end
    

    这种方式可以实现存取对象,但是不能获取_object变量。


    问答

    分类和扩展有什么区别?

    1.分类多用于扩展方法实现,类扩展多用于申明私有变量和方法。

    2.类扩展作用在编译期,直接和原类在一起,而分类作用在运行时,加载类的时候动态添加到原类中。

    3.类扩展可以定义属性,分类中定义的属性只会申明setter/getter,并没有相关实现和变量。

    分类有哪些局限性?

    1.分类只能给现有的类加方法或协议,不能添加实例变量(ivar)。

    2.分类添加的方法如果与现有的重名,会覆盖原有方法的实现。如果多个分类方法都重名,则根据编译顺序执行最后一个。

    分类的结构体里面有哪些成员?

    分类结构体包含了分类名,绑定的类,实例与类方法列表,实例与类方法属性以及协议表。


    参考

    深入理解Objective-C:Category

    神经病院 Objective-C Runtime 入院第一天—— isa 和 Class

    探秘Runtime - 深入剖析Category

  • 相关阅读:
    Leetcode Substring with Concatenation of All Words
    Leetcode Divide Two Integers
    Leetcode Edit Distance
    Leetcode Longest Palindromic Substring
    Leetcode Longest Substring Without Repeating Characters
    Leetcode 4Sum
    Leetcode 3Sum Closest
    Leetcode 3Sum
    Leetcode Candy
    Leetcode jump Game II
  • 原文地址:https://www.cnblogs.com/vanch/p/9662424.html
Copyright © 2011-2022 走看看