08-OC底层原理之Category实现原理

简介: 08-OC底层原理之Category实现原理

Category原理探索

Category的作用主要是在不改变原有类的前提下,动态地给这个类添加一些方法,同时可以将类的实现分散到多个不同文件或多个不同框架中,方便代码管理;接下来分析一下它的底层是如何实现的;

测试代码(为Person类添加分类)

Person+Cate.h:

@interface Person (Cate)<NSCoding>
@property(strong, nonatomic) NSString *name;
@property(assign, nonatomic) int age;
- (void)cate_method;
+ (void)cate_class_method;
@end
Person+Cate.m:
@implementation Person (Cate)
- (void)cate_method { }
+ (void)cate_class_method {}
- (void)encodeWithCoder:(NSCoder *)coder { }
- (instancetype)initWithCoder:(NSCoder *)coder {
    if (self = [super init]) { }
    return self;
}
@end

通过以下命令将Person+Cate.m文件转换为c++文件,查看一下编译过程:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Cate.m

通过Person+Cate.cpp文件我们可以看出_category_t结构体中,存放着类名,对象方法列表,类方法列表,协议列表,以及属性列表,并不包含成员变量信息(分类里面可以添加属性,但是默认并没有getter的和setter的实现)

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

1. 通过 _OBJC_$_CATEGORY_Person_$_Cate可以看出,是在_category_t各个成员赋值

static struct _category_t _OBJC_$_CATEGORY_Person_$_Cate __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
  "Person",
  0, // &OBJC_CLASS_$_Person,
  (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Cate,
  (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Cate,
  (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Cate,
  (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Cate,
};

首先看方法列表_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Cate和_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Cate,可以看到它们别赋值给了 instance_methods和class_methods类型的结构体中;

2. _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Cate方法实现:

static struct /*_method_list_t*/ {
  unsigned int entsize; // sizeof(struct _objc_method)
  unsigned int method_count;
  struct _objc_method method_list[3];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Cate __attribute__ ((used, section ("__DATA,__objc_const"))) = {
  sizeof(_objc_method),
  3,
  {{(struct objc_selector *)"cate_method", "v16@0:8", (void *)_I_Person_Cate_cate_method},
  {(struct objc_selector *)"encodeWithCoder:", "v24@0:8@16", (void *)_I_Person_Cate_encodeWithCoder_},
  {(struct objc_selector *)"initWithCoder:", "@24@0:8@16", (void *)_I_Person_Cate_initWithCoder_}}
};

从_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Cate中可以看到,里面为实例方法列表,也可以看到这个里面的方法与我们声明的方法一致,(_CATEGORY_CLASS_METHODS_Person_$_Cate类方法列表同理,这里不再列出);3. 接下来看协议列表3.1 可到遵守了一个协议NSCoding:

static struct /*_protocol_list_t*/ {
  long protocol_count; // Note, this is 32/64 bit
  struct _protocol_t *super_protocols[1];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Cate __attribute__ ((used, section ("__DATA,__objc_const"))) = {
  1,
  &_OBJC_PROTOCOL_NSCoding
};

3.2 接下来看参数_OBJC_PROTOCOL_NSCoding干了什么?

struct _protocol_t _OBJC_PROTOCOL_NSCoding __attribute__ ((used)) = {
  0,
  "NSCoding",
  0,
  (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding,
  0,
  0,
  0,
  0,
  sizeof(_protocol_t),
  0,
  (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCoding
};

继续向下看调用的_OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding函数;

3.3 _OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding函数:

static struct /*_method_list_t*/ {
  unsigned int entsize; // sizeof(struct _objc_method)
  unsigned int method_count;
  struct _objc_method method_list[2];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCoding __attribute__ ((used, section ("__DATA,__objc_const"))) = {
  sizeof(_objc_method),
  2,
  {{(struct objc_selector *)"encodeWithCoder:", "v24@0:8@16", 0},
  {(struct objc_selector *)"initWithCoder:", "@24@0:8@16", 0}}
};

可以看到里面为NSCoding需要实现的方法;

4. 接下来看属性列表_OBJC_$_PROP_LIST_Person_$_Cate

static struct /*_prop_list_t*/ {
  unsigned int entsize; // sizeof(struct _prop_t)
  unsigned int count_of_properties;
  struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Person_$_Cate __attribute__ ((used, section ("__DATA,__objc_const"))) = {
  sizeof(_prop_t),
  2,
  {{"name","T@\"NSString\",&,N"},
  {"age","Ti,N"}}
};

里面有name和age两个不同类型的属性;通过上述我们可以发现:分类源码中确实是将我们定义的对象方法,类方法,属性等都存放在catagory_t结构体中,接下来通过源码探究一下分类是如何和类对象以及元类对象进行合并的;

Category源码探索

通过上文分析我们已经知道,类是如何进行加载的,在这里我们在一些关键方法的地方加上一些调试方法,直接查看“glt新增方法”

找到readClass函数增加以下测试方法:

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized) {
    const char *mangledName = cls->nonlazyMangledName();
    auto ro = (const class_ro_t *)cls->data();
    // glt新增方法
    if (strcmp(mangledName, "Person") == 0) {
        method_list_t *list = ro->baseMethods();
        if (list != NULL) {
            for (uint32_t i = 0; i < list->count; i++) {
                printf("Category - Person - ro中的方法 - readClass:%s\n", (char *)list->get(i).big().name);
            }
        }
    }
    ...
}

找到realizeClassWithoutSwift函数增加以下测试代码:

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    // 我加的调试代码
    const char *mangledName = cls->mangledName();
    const char *myPersonName = "Person";
    //从Mach- O里面读取到cls->data(),强制转换成class_ro_t结构
    auto ro = (const class_ro_t *)cls->data();
    ...
    //Attach分类
    methodizeClass(cls, previously);
    //glt新增方法
    if (strcmp(mangledName, myPersonName) == 0) {
        method_list_t *list = ro->baseMethods();
        if (list != NULL) {
            for (uint32_t i = 0; i < list->count; i++) {
                printf("Category - Person中的方法 realizeClassWithoutSwift:%s\n", (char *)list->get(i).big().name);
            }
        }
    }
    return cls;
}

找到attachCategories函数增加以下函数:

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags) {
    // glt新增方法
    if (strcmp(cls->mangledName(), "Person") == 0) {
        printf("Category - attachCategories\n");
    }
    bool fromBundle = NO;
    ...
}

调试方法:

通过不同的文件是否添加load方法进行调试,可以分为以下几个,因篇幅较长,这里不再一一贴出运行结果;

  1. 主类有load + 分类有load
  2. 主类有load + 分类没有load
  3. 主类没有load + 1个分类1个load
  4. 主类没有load + 1个分类没有load
  5. 主类没有load + 2个分类(1个load、1个没有load)
  6. 主类没有load + 2个分类(2个load)
  7. 主类没有load + 3个分类(当有2个或者2个以上的load的时候)

调试源码执行流程:
通过源码调试结论,读取分类信息的地方有两处,调用顺序分别为:

  1. load_images -> prepare_load_methods -> realizeClassWithoutSwift -> methodizeClass -> attachCategories
  2. _objc_init -> map_images -> map_images_nolock -> _read_images -> readClass

调试得出的结果:

  • 主类有load + 分类有load(不区分有几个):调用attachCategories
  • 主类有load + 分类没有load(不区分有几个):从ro读取,也就是readClass函数刚开始的ro里面已经包含了分类的信息了;
  • 主类没有load + 1个分类1个load:从ro读取
  • 主类没有load + 1个分类没有load:从ro读取
  • 主类没有load + 2个分类(1个load、1个没有load):从ro读取
  • 主类没有load + 2个分类(2个load):调用attachCategories
  • 主类没有load + 3个分类(当有2个或者2个以上的load的时候):调用attachCategories

调试结果总结:

  • 当主类有load的时候,无论分类是否有load,均会调用attachCategories
  • 当主类没有load的时候,至少有1个以上load才会调用attachCategories
  • 其他情况均从ro中读取

接下来具体看一下attachCategories函数里面都做了哪些事?

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags) {
    // glt新增方法
    if (strcmp(cls->mangledName(), "Person") == 0) {
        printf("Category - attachCategories\n");
    }
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rwe = cls->data()->extAllocIfNeeded();
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }
        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }
    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}


在该方法中有mlists[ATTACH_BUFSIZ - ++mcount] = mlist;,首先mlist是一个数组,而又将这个数组赋值给mlists最后一个元素。那么mlists相当于一个二维数组,mlists中存储着当前分类中所有的方法列表,接下来调用attachLists函数:

rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);

主要看一下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;
            //数组进行扩容
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;
            //旧数组元素从后往前插
            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i];
            //新数组元素从前往后插
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i];
            free(array());
            setArray(newArray);
            validate();
        }
        else if (!list  && addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        }
        else {
            // 1 list -> many lists
            Ptr<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;
            //把旧数组当做一个元素放到lists最后一位
            if (oldList) array()->lists[addedCount] = oldList;
            //把新数组从头依次放入
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

结合注释可以得知新的list总是放到旧的list的前面,也就是说将来调用方法的时候由于后编译的类的方法总会插入到list的最前面,所以后编译的方法会优先调用,执行的并不是方法覆盖,而是找到以后便不再查找了;

总结

Category的实现原理?

Category编译之后的底层结构是category_t,里面存储着分类的对象方法、类方法、属性、协议列表等信息,程序运行的时候,通过runtime加载某个类的所有Category数据(区分load方法是否实现),把所有Category的方法、属性、协议列表数据合并到一个大数组中,后面参与编译的Category数据,会在数组的前面,最后将合并后的分类数据,再插入到原类的前面;

类和分类中有同名方法的调用顺序?

1. 类和分类中同名方法会优先调用分类方法(合并后的分类数据,插入到了原来的类的前面)

2. 多个分类的同名方法(load除外)调用顺序后编译的会优先调用(后编译的会插入到方法列表的前面)

Category和Class Extension扩展的区别?

Class Extension在编译的时候,它的数据已经包含在类信息中;Category是在运行时才会将数据合并到类信息中;本文部分内容来可能来源于网络,发布的内容如果侵犯了您的权益,请联系我们尽快删除!

相关文章
|
7月前
|
消息中间件 设计模式 Java
Java Review - Java进程内部的消息中间件_Event Bus设计模式
Java Review - Java进程内部的消息中间件_Event Bus设计模式
128 0
|
2月前
|
Java
Optional源码分析(涉及Objects源码和Stream源码)
本文分析了Java中Optional类的源码,包括其内部的Objects.requireNonNull方法、EMPTY定义、构造方法、ofNullable方法、isEmpty方法以及如何与Stream类交互,展示了Optional类如何避免空指针异常并提供流式操作。
41 0
Optional源码分析(涉及Objects源码和Stream源码)
|
7月前
|
Java
【JAVA学习之路 | 进阶篇】Record(记录)与密封类(sealed)
【JAVA学习之路 | 进阶篇】Record(记录)与密封类(sealed)
|
7月前
|
存储 缓存 编译器
Go语言解析Tag:深入探究实现原理
【2月更文挑战第20天】
320 2
|
缓存 JavaScript 算法
v-if和v-show的区别及源码分析
v-if和v-show的区别及源码分析
145 1
|
安全 Go
大白话讲讲 Go 语言的 sync.Map(二)
上一篇文章《大白话讲讲 Go 语言的 sync.Map(一)》讲到 entry 数据结构,原因是 Go 语言标准库的 map 不是线程安全的,通过加一层抽象回避这个问题……
115 1
|
存储 程序员 Go
大白话讲讲 Go 语言的 sync.Map(一)
在讲 sync.Map 之前,我们先说说什么是 map(映射)。我们每个人都有身份证号码,如果我需要从身份证号码查到对应的姓名,用 map 存储是非常合适的……
126 1
|
安全 Java
Java Review - 并发组件ConcurrentHashMap使用时的注意事项及源码分析
Java Review - 并发组件ConcurrentHashMap使用时的注意事项及源码分析
117 0
Go面试题进阶知识点:select和channel
这篇文章将重点讲解Go面试进阶知识点:select和channel。
206 0
Go面试题进阶知识点:select和channel
|
存储 JavaScript 前端开发
深入解析 Category 的实现原理
无论一个类设计的多么完美,在未来的需求演进中,都有可能会碰到一些无法预测的情况。那怎么扩展已有的类呢?一般而言,继承和组合是不错的选择。但是在Objective-C 2.0中,又提供了category这个语言特性,可以动态地为已有类添加新行为。如今category已经遍布于Objective-C代码的各个角落,从Apple官方的framework到各个开源框架,从功能繁复的大型APP到简单的应用,catagory无处不在。本文对category做了比较全面的整理,希望对读者有所裨益。
160 0