目录
1. Category的使用场景
2. Category的底层结构
3. Category的加载处理过程
4. Category和Extension区别
1. Category的使用场景
- Category可以在不改变或不继承原类的情况下,动态地给类添加方法。除此之外还有一些其他的应用场景:
- 把类的的实现分开在几个不同的文件里面。这样做有几个显而易见的好处:
可以减少单个文件的体积;
可以把不同的功能组织到不同的 category 里面,降低耦合;
可以由多个开发者共同完成一个类;
可以按需加载想要的 category;
声明私有方法。
-
为系统类添加方法(开发中应该是最常用的吧)。
-
模拟多继承(另外可以模拟多继承的还有 protocol)。
-
把framework 的私有方法公开。
2. Category的底层结构
- 我们知道OC中所有的对象在runtime层都是用struct表示的,当然category也是,这里我们给NSObject加一个category,并且新增play和eat方法,看看内部到底是啥构造
//NSObject+Like.h
#import <Foundation/Foundation.h>
@interface NSObject (Like)
-(void)paly;
-(void)eat;
@end
//NSObject+Like.m
#import "NSObject+Like.h"
@implementation NSObject (Like)
-(void)paly{
NSLog(@"like play");
}
-(void)eat{
NSLog(@"like eat");
}
@end
使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc xxx.m -o xxx.cpp 转为.cpp看看里面的内部结构,主要函数如下:
///分类的结构体 这个就是category结构体
struct _category_t {
const char *name; //category的名字
struct _class_t *cls;//类结构体指针,包含类的一些基本信息(下面类结构体)
const struct _method_list_t *instance_methods;// category中所有给类添加的实例方法的列表
const struct _method_list_t *class_methods;// category中所有添加的类方法的列表
const struct _protocol_list_t *protocols;//category实现的所有协议的列表
const struct _prop_list_t *properties;//category中添加的所有属性
};
///_category_t的初始化
static struct _category_t _OBJC_$_CATEGORY_NSObject_$_Like __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"NSObject", //category的名字
0, // &OBJC_CLASS_$_NSObject,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Like,//有对象方法即paly和eat
0,//category没有类方法为0
0,//category没有协议方法为0
0,//category没有属性则为0
};
///类结构体
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
///方法列表
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2]; ///方法列表,2表示分类有两个方法即play和eat
} _OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Like __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"paly", "v16@0:8", (void *)_I_NSObject_Like_paly},
{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_NSObject_Like_eat}}
};
///用于运行期category的加载
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_NSObject_$_Like,
};
从上面可以看到:
-
编译器生成了实例方法列表
OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Like命名遵循了公共前缀+类名+category名字的命名方式,如果我们在category添加了属性或者协议同样也会生成属性/协议列表. -
其次编译器生成了category本身
_category_t _OBJC_$_CATEGORY_NSObject_$_Like,并用前面生成的列表来初始化category本身。 -
最后,编译器在DATA段下的objc_catlist section里保存了一个大小为1的category_t的数组L_OBJC_LABELCATEGORY$,用于运行时category的加载。
通过上面C++代码的实现我们知道Category底层就是 _category_t结构体,主要包含以下信息:
- name : category的名字
- cls : 类的信息(包含isa、superclass等信息)
- instance_methods : category中所有给类添加的实例方法的列表
- class_methods: category中所有添加的类方法的列表
- protocols : category实现的所有协议的列表
- properties: category中添加的所有属性
3. Category的加载处理过程
- Category的加载处理过程主要分为以下几步:
- 在运行时,通过runtime加载某个类的所有Category数据
- 所有Category的方法、属性、协议数据,都会被分别合并到一个大数组的中,并且后面编译的会在数组前面;
- 将合并后的分类数据(方法、属性、协议),分类插入到类原来的数据前面。
方法执行顺序:
- 当分类和类都有相同的方法,调用此方法会调用分类的还是类的?
通过上面3.的分析,类的方法在方法列表中是排在最后的,所以当对象调用方法去方法列表里找的时候,会先找到分类的方法执行返回。
- 当多个分类都有相同的方法,会优先调用哪个分类的?
这个就是通过编译顺序决定的,后编译的文件中的同名方法会排在前面。
源码分析:Objc源码下载地址
这里取的是目前最新的objc4-818.2版本来证实一下上面所说的加载过程
因为源码太多,这里分析主要的实现函数:
objc-os.mm 文件 (主要是初始化)
_objc_init
map_images
map_images_nolock
objc-runtime-new.mm文件
_read_images //读取信息
attachCategories //将方法列表、属性和协议从类别附加到类中
objc-runtime-new.h文件
attachLists //合并所有方法列表、属性和协议到新的数组中
//为了方便学习,保留关键代码
static void attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,int flags)
{
constexpr uint32_t ATTACH_BUFSIZ = 64;
//方法列表
method_list_t *mlists[ATTACH_BUFSIZ];
//属性列表
property_list_t *proplists[ATTACH_BUFSIZ];
//协议列表
protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
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];
//取出方法数组
//isMeta == Yes 取出类方法
//isMeta == No 取出对象方法
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) {
// 将mlist中的方法添加到添加到rwe(原类中的类方法列表)中去
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) {
// 将proplist中的属性添加到添加到rwe(原类中的属性表)中去
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) {
// 将protolist中的协议添加到添加到rwe(原类中的属性表)中去
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);
}
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
//主要是这部分:
//这里如果有分类对象的方法/协议/属性列表的话
//会重新malloc一个数组,大小为oldCount + addedCount
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;
//这里遍历是i--,即i是从大到小,原来的旧数据会放在newArray的最后面
//这里也就从源码的角度解释了,原类的方法在方法列表中是排在最后的
for (int i = oldCount - 1; i >= 0; i--)
newArray->lists[i + addedCount] = array()->lists[i];
//这里遍历是i++,即新增的数据会依次加到newArray的前面
//最终newArray刚好填满,组成一个新的方法/协议/属性列表
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;
if (oldList) array()->lists[addedCount] = oldList;
for (unsigned i = 0; i < addedCount; i++)
array()->lists[i] = addedLists[i];
validate();
}
}
通过上面关键部分代码和注释,应该对Category的加载处理过程有了一个更清晰的认识吧。
4. Category和Extension区别
-
Extension在编译的时候,它的数据就已经包含在类信息中,Category是在运行时,才会将数据合并到类信息中。
-
因为1.的原因,extension 可以添加成员变量,category 不能添加成员变量。运行时加载类到内存以后,才会加载分类,这时类的内存布局已经确定(编译器还会对成员变量顺序做出优化,保证遵循内存对齐原则下类占用内存容量最少),如果再去添加成员变量就会破坏类的内存布局。各个成员变量的访问地址是在编译时确定的,每个成员变量的地址偏移都是固定的(相对于类的起始地址的内存偏移(硬编码))
-
extension 和 category 都可以添加属性,但是 category 中的属性不能生成对应的成员变量以及getter 和 setter方法的实现(可以通过关联对象添加属性生成 getter和setter方法)
-
extension 不能像 category 那样拥有独立的实现部分(@implementation 部分),extension 所声明的方法必须依托对应类的实现部分来实现。
-
category 可以给系统提供的类添加分类,而extension一般用来隐藏类的私有信息,无法直接为系统的类扩展。