美文网首页
—— Category

—— Category

作者: 大成小栈 | 来源:发表于2021-09-30 21:09 被阅读0次

    1. 分类的结构

    分类 — 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;
    }
    
    1. 分类可以扩展的方面包括:实例方法、类方法、协议、属性(但不支持扩展成员变量);
    2. 一般使用的场景有扩展现有类方法、代码分区、添加私有方法(不对外暴露category.h)、模拟多继承(使用关联对象的方式添加属性实现)。

    2. 分类的加载过程

    查看OC方法加载过程源码,每个Class对象都对应一个objc_class的结构。每个objc_class都包含有class_data_bits_t数据位,其中储存了class_rw_t的指针地址和一些其他标记。class_rw_t中包含有属性方法协议列表,以及class_ro_t指针地址。在class_ro_t结构中,储存的是编译器决定的属性方法协议

    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_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 方法中,加载了原类在编译期所有的方法、属性和协议,然后再获取未连接的分类表,并将列表中的扩展方法添加到运行时类中。

    3. 分类中同名方法的覆盖

    同名方法的情况分为两种:分类与原类中方法同名;或不同的分类之间实现了同名方法。那么,调用时具体会使用哪个方法的实现呢?

    • 加载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); 方法时,把新增分类中的方法列表添加到实际运行时查询的方法列表头部。在进行方法调用时会从头部查询,一旦查到后就返回方法实现的指针。

    因此,同名方法的调用顺序也就有了结论!
    同时,原类中在这之前的同名方法也被保存,可通过获取同名方法的方式查找原类的实现。

    4. 在分类中添加属性

    属性(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 变量。

    • 关联对象的原理

    iOS 通过 runtime 的 API 可以给分类添加属性,关联对象总共有下边3个相关的 API:

    // 获取某个对象的关联属性
    id objc_getAssociatedObject(id object, const void *key) {
        return _object_get_associative_reference(object, (void *)key);
    }
    // 给某个对象添加关联属性
    void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
        _object_set_associative_reference(object, (void *)key, value, policy);
    }
    // 移除对象所有的关联属性
    void objc_removeAssociatedObjects(id object)
    

    通过 runtime 的源码可以看出,关联属性并没有添加到 category_t 里边,运行时也不会合并属性到元类对象里边,而是存储在一个全局的 AssociationsManager 里。

    AssociationsManager层级关系

    AssociationsManager通过传递进来的对象作为地址,取出这个对象所对应的关联列表,然后再通过 key 取出这个关联列表的关联属性 ObjcAssociation,ObjcAssociation 包含了关联策略 和 关联值。

    5. 区别于Extension

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

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

    类扩展支持写在多个.h文件,但都必须在.m文件中引用,且不能有自己的实现。
    1.分类多用于扩充原类的方法;类扩展多用于声明私有变量和方法。
    2.分类作用在运行时,加载类的时候动态添加到原类中;类扩展作用在编译期,直接和原类在一起。
    3.分类中定义的属性只会申明setter/getter,并没有相关实现和成员变量;类扩展可以定义属性。

    参考文章:
    https://www.cnblogs.com/vanch/p/9662424.html
    https://tech.meituan.com/DiveIntoCategory.html
    https://www.jianshu.com/p/0dc2513e117b
    https://www.cnblogs.com/vanch/p/9646966.html
    https://www.cnblogs.com/vanch/p/9682554.html

    相关文章

      网友评论

          本文标题:—— Category

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