category是Objective-C里面最常用到的功能之一。category可以为已有的类增添实例方法或类方法,而不需要修改原有类。
Category 结构体
在苹果开源的objc项目的objc-runtime-new.h头文件中可以找到Category的结构体
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);
};
name: Category所有附加的类的类名
cls: Category索要附加的类对象
instanceMethods: Category中定义的实例方法
classMethods: Category中定义的类方法
protocols: Category中声明实现的协议列表
instanceProperties: Category中声明的实例属性
_classProperties: Category中声明的类属性,但不会出现在内存中
当然,这些字段所代表的函数都只是我们的猜测而已,现在就需要一步一步的去验证是不是真的是这样子。我们可以先简单的创建一个Person的分类Test1来分析category_t
结构中是不是真的存储这些值
// Person+Test1.h
@interface Person (Test1) <NSCopying, NSMutableCopying>
@property (nonatomic, strong) NSString *smallName;
- (void)run;
- (void)eat;
+ (void)battle;
@end
// Person+Test1.m
@implementation Person (Test1)
- (void)run {
NSLog(@"categroy1 - run");
}
- (void)eat {
NSLog(@"categroy1 - eat");
}
+ (void)battle {
NSLog(@"categroy1 - battle");
}
@end
通过clang编译器反编译之后能够得到C/C++代码,下面截取Person(Test1)结构体的相关代码分析一下
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test1.m
static struct _category_t _OBJC_$_CATEGORY_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test1,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test1,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test1,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Test1,
};
- 第一个字段
name
是Person - 第二个字段
cls
是0,但是注释为Person的类对象地址,这个待会再看 - 第三个字段
instanceMethods
是一个_method_list_t的结构体指针 - 第四个字段
classMethods
也是一个_method_list_t的结构体指针 - 第五个字段
protocols
则是一个_protocol_list_t的结构体指针 - 第六个字段
instanceProperties
是一个_prop_list_t的结构体指针 - 第七个字段
_classProperties
,没有这个字段了,在category_t结构体的定义中也说明了这个字段不会出现在存储中
我们一个一个的查看这些结构体指针里面是否真的存储的是我们的Person(Test1)分类中的定义的内容
1. instanceMethods
static void _I_Person_Test1_run(Person * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_pw_9kxn77w15h15s3l_24f9vtv80000gn_T_Person_Test1_28c269_mi_0);
}
static void _I_Person_Test1_eat(Person * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_pw_9kxn77w15h15s3l_24f9vtv80000gn_T_Person_Test1_28c269_mi_1);
}
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_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
4,
{{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Person_Test1_run},
{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Person_Test1_eat}},
{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", (void *)_I_Person_Test1_copyWithZone_},
{(struct objc_selector *)"mutableCopyWithZone:", "@24@0:8^{_NSZone=}16", (void *)_I_Person_Test1_mutableCopyWithZone_}}
};
OBJC$CATEGORY_INSTANCE_METHODS_Person$_Test1里面存储了四个_objc_method结构体,通过objc_selector可以看出这四个函数刚好是我们实现的run,eat,copyWithZone:,mutableCopyWithZone:四个实例方法,而函数调用地址也是我们在.m文件中实现的方法。
2. classMethods
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"battle", "v16@0:8", (void *)_C_Person_Test1_battle}}
};
反观OBJC$CATEGORY_CLASS_METHODS_Person$_Test1里面只有一个方法,并且是我们定义并实现的battle类方法。
3. protocols
static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
2,
&_OBJC_PROTOCOL_NSCopying,
&_OBJC_PROTOCOL_NSMutableCopying
};
我们在分类中实现的分类有2个,分别为NSCopy和NSMutableCopying;而在OBJC_CATEGORY_PROTOCOLS$Person$_Test1中的2个协议就是我们所实现的2个协议。
4. instanceProperties
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Test1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"smallName","T@\"NSString\",&,N"}}
};
我们在分类的声明中声明定义了一个名为smallName的属性,在OBJC$PROP_LIST_Person$_Test1结构体里面就是我们所声明的实例属性。
可能有同学有疑问了,如果通过@property声明的属性,编译器应该是会自动为我们生成一个_smallName的成员变量,setter和getter方法才对的,为什么这个分类里面既没有_smallName成员变量,也没有setter和getter方法呢?
那是因为成员变量也是在编译的时候确定的,类结构体也是在编译的时候就确定的。而Category分类是在运行加载进内存的时候才附加到类中的,所以Category分类中声明的属性编译器是不会生成对应的成员变量的,既然没有成员变量,setter和getter方法也就失去了存在的意义,因此编译器也不会自动生成这两个实例方法。
Category 加载
在objc-runtime-new.mm文件中我们找到一段代码,通过方法名猜测是用来加载Category
分类的
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
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--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
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->protocols.attachLists(protolists, protocount);
free(protolists);
}
这段代码理解起来不是很难,3个函数参数分类为类对象cls,category_list
结构体指针cats和是否清除缓存flush_caches。
首先定义一个method_list_t结构体指针数组mlists,通过malloc
方式分配一块内存,内存大小由category_list
里面的category_t
结构体个数和结构体指针所占内存共同决定;property_list_t结构体指针数组proplists和protocol_list_t结构体指针protolists也同理。
从后往前遍历cats中的list数组,并将每个元素中的cat成员(category_t分类结构体)中的方法数组指针保存到指针数组mlists中,属性数组指针则保存到指针数组proplists中,协议数组指针保存到指针数组protolists中。
取出cls对象中的class_rw_t结构体赋值为rw,调用attachLists(List* const * addedLists, uint32_t addedCount)
将指针数组mlists,proplists和protolists分别附加到rw的methods,properties和protocols上。
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
...
};
class_rw_t主要存放OC类对象(元类对象)的方法数组,属性数组,协议数组,因此将mlists,proplists和protolists这三个指针数组都附加上去。给原来的类对象(元类对象)附加额外的方法,属性,协议。method_array_t,property_array_t和protocol_array_t都实现了一套模板template <typename Element, typename List>
,这三个成员主体都是指针数组,每个指针所指向的内存又是一个数组,大概的内存如下图所示,如果我理解错了,请联系我
接下来进去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;
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]));
}
}
在这个函数内分成了3种情况去处理:
- 原指针数组有多个值
- 计算添加新的指针数组后总的个数,并根据总个数调用realloc函数重新分配内存,*如果新分配的内存大小大于原来的大小,原数据不会丢失
- 将原有的指针数组移动到新内存的最后,前面空出addedCount个指针内存
- 将addedLists指针数组复制到新内存的前面
- 原指针数组为空,且新添加的指针数组中只有一个指针
- 直接将** addedLists的唯一指针赋值给list**
- 其他情况
- 重新分配内存
- 判断原指针数组是否有值,有值的话,先把值放到数组的最后
- 将addedLists指针数组复制到新内存的前面
为什么在将原来的指针数组放到数组的最后这一步要用memmove而不是memcpy呢?请看下图,
在调用realloc函数的时候,如果原内存段后有足够的内存空间,则会扩展这段内存段,否则就重新分配一段新的内存空间,并将原内存段中的数据复制到新的内存段中,释放原内存段内存。
假如原数据有3个指针,新扩展了2个指针大小的空间,这时如果调用memcpy函数将原来的3个指针复制到这块内存段的最后,因为memcpy函数是一个一个元素的复制,就会出现中间那个指针在还没复制前就被覆盖丢失了。
如果调用的是memmove函数,函数内部会先判断源内存段和目标内存段是否存在重叠部分,如果存在,会根据重叠地方判断从头到尾移动数据还是从尾到头来移动数据,上图中源内存段的尾部和目标内存段的头部重叠了,所以memmove会先移动源内存段的尾部数据,这样可以保证数据在迁移前不被覆盖丢失。
为什么赋值新的指针数据的时候要用memcpy而不继续用memmove呢?猜测是memcpy效率更高吧。
分类的方法是在原方法的前面,当调用方法的时候,从前往后遍历,先找到分类中的方法就直接调用,但是原方法还在方法列表中的。
还可以通过运行时来验证原方法是否还存在
unsigned int count;
Method *methods = class_copyMethodList([Person class], &count);
for (int i=0; i<count; i++) {
Method method = methods[i];
NSLog(@"%@", NSStringFromSelector(method_getName(method)));
}
free(methods);
打印结果为:
我定义了2个Person的分类,都实现了
- (void)run;
实例方法,因此打印出来得有3个- (void)run;
方法。
最后附上本文的代码:GitHub传送门
网友评论