iOS中的Category我们经常使用,主要是给一些类添加新的方法,或者拆分类。进行方法调用的时候,如果调用的是写在类里面的方法,调用顺序是:首先,实例对象根据它的isa找到类对象,然后去类对象里面的方法列表里面寻找方法的实现,如果找到,就会调用这个方法,完毕。但是如果调用的方法是写在分类里面,那么调用流程是什么呢?
其实无论写多少分类,最后运行的时候,Runtime会把分类所有的对象方法合并到类对象的方法列表里面,把类方法合并到元类对象的方法列表里面。
一. 编译的时候
首先看一下编译的时候分类发生了什么。
创建一个MJPerson类,给MJPerson类添加一个MJPerson+Eat分类,在分类中添加属性、协议、对象方法、类方法,代码如下:
#import "MJPerson.h"
@interface MJPerson (Eat) <NSCopying, NSCoding>
- (void)eat;
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) double height;
@end
#import "MJPerson+Eat.h"
@implementation MJPerson (Eat)
- (void)run
{
NSLog(@"MJPerson (Eat) - run");
}
- (void)eat
{
NSLog(@"eat");
}
- (void)eat1
{
NSLog(@"eat1");
}
+ (void)eat2
{
}
+ (void)eat3
{
}
@end
按照NSObject的本质文章中的指令,把MJPerson+Eat.m文件转换成C++文件,在C++文件中搜索_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; //属性
};
这就是分类的底层结构,相当于,我们写的分类编译完成之后就会变成上面那种结构,有多少分类就有多少上面的结构体。当程序运行的时候就会把上面结构体的东西合并到类对象或者元类对象里面去。
所以分类的功能我们也知道了,分类可以添加属性、协议、对象方法、类方法。
再次进入C++源码,会发现如下代码:
//对象方法列表
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_MJPerson_Eat_eat},
{(struct objc_selector *)"eat1", "v16@0:8", (void *)_I_MJPerson_Eat_eat1}}
};
//类方法列表
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"eat2", "v16@0:8", (void *)_C_MJPerson_Eat_eat2},
{(struct objc_selector *)"eat3", "v16@0:8", (void *)_C_MJPerson_Eat_eat3}}
};
//下面是关于NSCopy协议的一些描述
static const char *_OBJC_PROTOCOL_METHOD_TYPES_NSCopying [] __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"@24@0:8^{_NSZone=}16"
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}}
};
struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
0,
"NSCopying",
0,
(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
0,
0,
0,
0,
sizeof(_protocol_t),
0,
(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCopying = &_OBJC_PROTOCOL_NSCopying;
//下面是关于NSCoding协议的一些描述
static const char *_OBJC_PROTOCOL_METHOD_TYPES_NSCoding [] __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"v24@0:8@\"NSCoder\"16",
"@24@0:8@\"NSCoder\"16"
};
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}}
};
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
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCoding = &_OBJC_PROTOCOL_NSCoding;
//协议列表
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
2,
&_OBJC_PROTOCOL_NSCopying,
&_OBJC_PROTOCOL_NSCoding
};
//属性和列表
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_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"weight","Ti,N"},
{"height","Td,N"}}
};
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_MJPerson;
//把上面的列表传入这个结构体中
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"MJPerson",
0, // &OBJC_CLASS_$_MJPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_MJPerson_$_Eat,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_MJPerson_$_Eat,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MJPerson_$_Eat,
};
观察代码的最后那个结构体,可以发现,上面的代码是给分类的_category_t结构体赋值,分别给名称赋值MJPerson、cls赋值0、对象方法列表赋值、类方法列表赋值、协议赋值、 属性赋值。
下面我们在objc4源码里面查看一下category_t结构体的定义,打开源码,搜索“category_t {”找到如下代码:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
可以发现和C++里面的定义大同小异。
总结:
编译完成之后,分类里的所有东西都在category_t结构体中,暂时和类是分开的。
二. 运行的时候
下面我们就通过分析objc4源码证明,最后运行的时候,Runtime就会把这个类所有的对象方法合并到类对象的方法列表里面,把类方法合并到元类对象的方法列表里面去。
源码解读顺序:
objc-os.mm文件
_objc_init (运行时的入口,运行时的初始化)
map_images
map_images_nolock
objc-runtime-new.mm文件
_read_images (加载一些模块、镜像,参数传入所有的类信息)
remethodizeClass (核心方法,将类对象和元类对象重新组织下)
attachCategories (核心方法,参数传入类对象(或者元类对象)和分类)
attachLists
realloc、memmove、 memcpy
由于源码阅读比较复杂,可按照上面的顺序来阅读,这里只贴上核心的代码。
attachCategories方法
//参数传入类对象或者元类对象 分类列表(里面有好几个分类)
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
//方法数组(二维数组,大数组里面装好多小数组,每个小数组都是一个分类的方法列表)
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
//属性数组 同上
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
//协议数组 同上
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--) { //i的值不断减小,先拿出的是最后面那个分类
//取出某个分类
auto& entry = cats->list[I];
//取出分类的方法列表(根据isMeta决定拿出的是对象方法还是类方法)
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
//并且将取出的方法列表扔到mlists数组里面
mlists[mcount++] = mlist; //mcount从0开始,所以最后面的分类放到最前面
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->或者元类对象(protolists, protocount);
free(protolists);
}
上面的代码主要做的是将所有的分类的数据拿出来放到数组中,并且是最后面的分类放到最前面,详细解释可一步步看注释,下面进入attachLists方法
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return; //addedCount分类的个数
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原来的方法列表
//addedCount分类的个数
//将原来的方法列表往后移addedCount位置 前面的就空出来了
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
//addedLists所有分类的方法列表
//将所有分类的方法列表放到前面
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]));
}
}
解释:
首先,扩容,然后将原来的方法列表挪到后面去,再将所有分类列表放到原来列表的位置。最后结果是分类在前面,原来的类在后面,所以,同样的方法会优先调用分类。如果不同分类中有相同的方法,由于后编译的分类放到了前面,所以后编译的分类会优先调用。
可在下图中查看和改变编译顺序:
总结:
① 运行的时候,通过Runtime加载某个类的所有Category数据
② 把所有Category的属性、协议、方法数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面
③ 将合并后的分类数据(属性、协议、方法),插入到类原来数据的前面
补充:
memmove内存挪动和memcpy内存拷贝的区别:
如图,如果内存中的方法列表顺序是 3 4 1 ,如果想把3和4放到最后面两位(3 3 4),如果使用memcpy,会先变成 3 3 1,再变成 3 3 3。
如果使用memmove就直接一步到位将最后两位变成3 4,结果为 3 3 4。
所以,对于原来的类,肯定要一字不差的挪动过来,所以使用了memmove,而对于分类里的数据,就一个一个拷贝过来就好了,没必要用move。
面试题:
Category的使用场合是什么?
主要是给一些类添加新的方法,或者拆分类。Category的实现原理是什么?
Category编译之后的底层结构是struct category_t,里面存储着分类的属性、协议、对象方法、类方法。
在程序运行的时候,Runtime会将Category的数据,合并到类信息中(类对象、元类对象中)。Category和类扩展有什么区别呢?
类扩展就是在类的.m里面添加一些属性,成员变量,方法声明,如下:
// class extension (类扩展)
@interface MJPerson()
{
int _abc;
}
@property (nonatomic, assign) int age;
- (void)abc;
@end
其实类扩展就是相当于将原来写在.h文件里面的东西剪掉放到.m里面,把原来公开的属性,成员变量,方法声明私有化。所以,类扩展里面的东西在编译的时候就已经存在类对象里面了,这点和分类不同。
Demo地址:分类的本质