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方法进行调试,可以分为以下几个,因篇幅较长,这里不再一一贴出运行结果;
- 主类有load + 分类有load
- 主类有load + 分类没有load
- 主类没有load + 1个分类1个load
- 主类没有load + 1个分类没有load
- 主类没有load + 2个分类(1个load、1个没有load)
- 主类没有load + 2个分类(2个load)
- 主类没有load + 3个分类(当有2个或者2个以上的load的时候)
调试源码执行流程:
通过源码调试结论,读取分类信息的地方有两处,调用顺序分别为:
- load_images -> prepare_load_methods -> realizeClassWithoutSwift -> methodizeClass -> attachCategories
- _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是在运行时才会将数据合并到类信息中;本文部分内容来可能来源于网络,发布的内容如果侵犯了您的权益,请联系我们尽快删除!