美文网首页iOS底层原理
小码哥底层原理笔记:Catgory的本质

小码哥底层原理笔记:Catgory的本质

作者: chilim | 来源:发表于2020-05-12 11:04 被阅读0次

首先我们要明白什么是类扩展class extension?
像下面这样就是类扩展

//==============类扩展====
@interface Person()

@property (nonatomic, assign)int height;

@end
//==============类扩展====

@implementation Person

- (void)run{
    NSLog(@"run");
}

+ (void)run2{
    NSLog(@"+ run2");
}

@end

类扩展和分类Catgory一样都是可以给类增加定义属性和方法。
我们新建一个Person类和它的两个分类

@interface Person : NSObject

- (void)run;

+ (void)run2;

@end
@implementation Person

- (void)run{
    NSLog(@"Person  -run");
}

+ (void)run2{
    NSLog(@"Person + run2");
}

@end
@interface Person (Test)

- (void)run;

- (void)test;

+ (void)test2;


@end
@implementation Person (Test)

- (void)run{
    NSLog(@"Person (Test) -run");
}

- (void)test{
    NSLog(@"Person (Test) -test");
}

+ (void)test2{
    NSLog(@"Person (Test)+ test2");
}

@end
@interface Person (Eat)<NSCoding,NSCopying>

- (void)run;

- (void)eat;

+ (void)eat2;

@property (nonatomic, assign)double height;

@property (nonatomic, assign)int weight;

@end
@implementation Person (Eat)

- (void)run{
    NSLog(@"Person (Eat) -run");
}

- (void)eat{
    NSLog(@"Person (Eat) -eat");
}

+ (void)eat2{
    NSLog(@"Person (Eat)+ eat2");
}

@end

我们将其中一个分类转成.cpp文件,其实分类底层结构就是一个_category_t结构体

struct _category_t {
    const char *name;//类名
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;//对象方法列表
    const struct _method_list_t *class_methods;//类方法列表
    const struct _protocol_list_t *protocols;//协议列表
    const struct _prop_list_t *properties;//属性列表
};

也就是说当我们每创建一个分类,系统编译时就会把这个分类转化成这样一个结构体保存起来

当我们在程序运行时runtime,所有分类的这些属性方法等将会合并到Person里面,分类的对象方法合并到Person类对象里面,分类的类方法合并到Person元类对象里面。runtime合并过程源码如下:

//cls传进去的有可能是类对象也可能是元类对象,cls = [Person class]
//cats传进去的是分类数组,有几个分类数组里面就有几个 cats = [category_t(Test), category_t(Eat)];//每个分类编译时实际上是通过category_t结构体保存的
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_t],
     ]
     */
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    //属性数组
    /*
    [
       [property_t,property_t],//其中一个分类的属性列表
       [property_t,property_t],
    ]
    */
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    //协议数组
    /*
    [
       [protocol_t,protocol_t],//其中一个分类的协议列表
       [protocol_t,protocol_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;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        //取出某个分类category_t
        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);
}

具体的合并细节我们拿Methods来看,点开

  //将所有分类的对象方法,附加到类对象的方法列表中
    rw->methods.attachLists(mlists, mcount);

我们可以看到attachLists的实现细节

/*
     addedLists
     [
        [method_t, method_t],
        [method_t, method_t],
     ]
     addedCount
     2
     */
    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;
            
            //array()->lists:原来的方法列表往后移,新的分类的方法列表放在原来的方法列表前面
            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]));
        }
    }

从以上我们可以看到后面合并进来的分类方法列表是放在数组的前面的,最后把所有分类的方法列表合并到原有类的方法列表前面。也就是说后编译的分类数据会在数组的前面,当我们调用时会优先调用数组前面的方法。
我们可以实践一下,我们调用[person run]实例方法,首先我们的编译顺序是这样的,先编译Test分类,然后时Eat分类,最后是Person类

245B0321-5339-4DDA-B478-DB4B02476D19.png
我们打打印结果是:
2020-05-12 10:38:30.945775+0800 XMGTestProject[8012:7203832] Person(Eat)-run

因为Eat分类是最后编译的,所以调用的是Eat分类的run方法。虽然Person类是最后编译,但是从上述源码中看到所有分类的方法合并时是放在Person类方法列表前面的。不管Person类何时编译,只要它有分类就会把分类的方法列表合并到Person类的方法列表前面。所以我们调用时分类会覆盖Person类的方法

面试题

1、category的实现原理
答:category编译之后的底层结构是struct category_t,里面存储着分类的对象方法,类方法,属性,协议信息,在程序运行的时候,runtime会将category的数据,合并到原有类信息中(类对象,元类对象中)

2、category的加载处理过程
答:1、通过Runtime加载某个类的所有category数据
2、把所有category的方法,属性,协议数据,合并到一个大数组中,后面参与编译的category数据,会在数组的前面
3、将合并后的分类数据(方法,属性,协议),插入到类原来数据的前面

3、category和类扩展class extension的区别?
答:category是在运行时将方法属性合并到类对象里面,而类扩展是在编译的时候就将方法属性等合并到类对象里面

相关文章

网友评论

    本文标题:小码哥底层原理笔记:Catgory的本质

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