1、什么是分类
iOS分类使用的场景就是把功能模块化,把一组类似的功能扩展出一个分类,便于代码的维护以及功能的分块
2、分类的底层结构
分类在iOS底层的数据结构如下:
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;
};
name:被分类的类名称
cls :isa指针
instance_methods:对象方法列表
class_methods:类方法列表
protocols:协议列表
properties:属性列表
有上面的数据结构可以看出:
分类可以扩展一个类的(对象方法,类方法,协议和属性)。
name:被分类的类名称
cls :isa指针
instance_methods:对象方法列表
class_methods:类方法列表
protocols:协议列表
properties:属性列表
有上面的数据结构可以看出:
分类可以扩展一个类的(对象方法,类方法,协议和属性)但不能增加成员变量。同时,添加的属性不会生成get和set方法的实现,如果需要,则要用运行时动态添加
其实我们写的所有分类的数据结构都是跟上面一样的,不同的是里面存放的数据
3、实践证明分类的结构体
例如我创建一个Person+Eat的分类
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Eat)
-(void)eat;
@end
NS_ASSUME_NONNULL_END
#import "Person+Eat.h"
@implementation Person (Eat)
-(void)eat {
NSLog(@"eat-----");
}
@end
运行之后,用命令行(clang -rewrite-objc Person+Eat.m -o Person+Eat.cpp)生成c++代码。产生的C++相关代码如下:
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;
};
static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat,
0,
0,
0,
};
上面可以看出:这个分类的类型就是上面第二步说到的《 _category_t 》 类型,结构体的赋值如下:
name:"Person"
cls : &OBJC_CLASS_$_Person
instance_methods:Eat方法
class_methods:没有,所以为空
protocols:没有,所以为空
properties:没有,所以为空
4、分类的实现原理
上面几个步骤说到的只是分类的结构,是程序编译的时候就已经生成的。 但这个分类跟对应的类关联在一起是在运行时,这个就用到oc的runtime机制了,runtime在程序运行的过程中,会把所有的分类,合并到对应的类或者原类里面去,如果有同名方法,会优先调用分类里面的方法(利用这个功能,我们可以对系统的类做方法交换)
分类的实现的具体步骤:
所有分类的方法会存放在一个二维数组里面,二维数组的每一个数组就是其中一个分类的所有方法,在运行时,会遍历这个数组,然后把所有的方法添加到对应的类里面去,具体添加的步骤如下
1、根据要添加的方法数组大小加上原来数组的大小,重新分配数组空间,
2、先把原来类的数组的存储地址向后面移动n个单元,n取决于二维数组的大小,然后把新传进来的数组从大到小的顺序进行遍历,一个一个插入到新分配的数组空间里面去,因为从大到小的顺序进行遍历,也就是数组后面的会排在第一个。二维数组的顺序是编译的先后顺序决定的,所以同一个方法名,后编译的分类的方法比先编译的方法优先执行
把分类里面的方法合并到对应的类中的核心源码:
/**
* addedLists 所有分类的方法列表
* addedCount 有多少个分类
*/
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]));
}
}
右上图可以看出为什么分类的方法和原来类的方法同名的情况下,会优先调用分类方法,因为分类方法在数组的前面,最先被找到。
5、这里顺便提一下类扩展
类扩展是程序编译的时候就确定了的,类扩展一般是扩展私有属性和方法