今天看了一篇文章【iOS】category 重写方法的调用,介绍了category在重写主类方法,和多个category中方法重名的时候的调用方式。其中提到category加入函数列表中的顺序是反向于文件编译的顺序,即编译是根据buildPhases->Compile Sources里面的顺序从上至下编译的,那么category的执行顺序就是反向于这个顺序的。那么我们来验证一下。
@interface Father : NSObject
- (void)name;
@end
@implementation Father
- (void)name{
NSLog(@"my name is father");
}
@end
@interface Father (aaa)
@end
@implementation Father (aaa)
- (void)name{
NSLog(@"my name is father aaa");
}
@end
@interface Father (bbb)
@end
@implementation Father (bbb)
- (void)name{
NSLog(@"my name is father bbb");
}
@end
int count;
Method* list;
list = class_copyMethodList( [Father class], &count );
for (int i = 0; i < count; i++) {
NSLog(@"%@",NSStringFromSelector(method_getName(list[i])));
}
运行结果:
2018-01-04 23:59:59.904820+0800 test[48010:12449543] name:
2018-01-04 23:59:59.904937+0800 test[48010:12449543] name:
2018-01-04 23:59:59.905057+0800 test[48010:12449543] name:
打印出3个name:,可以看到category和主类的方法都在函数列表中,但是selector的名字都一样啊,那么怎么确定他的顺序呢?哈哈,我们来想想办法,看看method都包括哪些内容
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
selector是查找函数的key,而selector本身就是一个字符串,那么我们可以定义同样的selector,通过定义不同的method_types加以区分。
@interface Father : NSObject
- (void)name:(int)a;
@end
@implementation Father
- (void)name:(int)a{
NSLog(@"my name is father");
}
@end
@interface Father (aaa)
@end
@implementation Father (aaa)
- (void)name:(char)a{
NSLog(@"my name is father aaa");
}
@end
@interface Father (bbb)
@end
@implementation Father (bbb)
- (void)name:(short)b{
NSLog(@"my name is father bbb");
}
@end
int a;
Father* father = [[Father alloc]init];
[father name:a];
int count;
Method* list;
list = class_copyMethodList( [Father class], &count );
des = method_getDescription(list);
for (int i = 0; i < count; i++) {
NSLog(@"%@ %s",NSStringFromSelector(method_getName(list[i])), method_getTypeEncoding(list[i]));
}
运行结果:
2018-01-05 00:27:44.160124+0800 test[48427:12478941] my name is father bbb
2018-01-05 00:21:44.170938+0800 test[48427:12478941] name: v20@0:8s16
2018-01-05 00:21:44.171095+0800 test[48427:12478941] name: v20@0:8c16
2018-01-05 00:21:44.200842+0800 test[48427:12478941] name: v20@0:8i16
其中8s中的s代表short,8c中的c代表char,8i中的i是int。
到Compile Sources中看一下,发现编译的顺序是
Father+aaa.m
Father+bbb.m
Father.m
说明是先编译的Father.m,然后编译的Father+aaa.m和Father+bbb.m,所以Father+bbb.m中的函数
-(void)name:(short)b 最后一个载入,也得到了最终执行。
接下来我们在Compile Sources中拖动Father.m交换文件顺序。
Father+bbb.m
Father.m
Father+aaa.m
运行结果:
2018-01-05 00:37:28.714927+0800 test[48698:12497028] my name is father aaa
2018-01-05 00:37:28.715230+0800 test[48698:12497028] name: v20@0:8c16
2018-01-05 00:37:28.727622+0800 test[48698:12497028] name: v20@0:8s16
2018-01-05 00:37:28.727735+0800 test[48698:12497028] name: v20@0:8i16
可以看到,Father+aaa.m和Father+bbb.m中的函数在methodlist中的顺序交换了,最终执行的也成了Father+aaa.m中的函数。但是主类的函数(name: v20@0:8i16)一直是最先放入methodlist中的。这也说明了category的加载是在主类之后,这与Compile Sources中的顺序无关。
再思考2个问题:
- 子类和父类都实现了相同的category函数,会执行谁呢?
- 子类继承后重写了父类的category函数,会执行谁呢?
其实答案很明显,按照selector的查找顺序,会先在子类中找,再到父类中找。只要子类找到了selector,不管是不是category,都会先执行。
看下这两个问题的代码:
@implementation Father (bbb)
- (void)name:(double)b{
NSLog(@"my name is father bbb");
}
- (void)age{
NSLog(@"I am 30 years old");
}
@end
@interface Son : Father
- (void)age;
@end
@implementation Son
- (void)name:(int)a{
NSLog(@"my name is son");
}
- (void)age{
NSLog(@"I am 6 years old");
}
@end
@interface Son (bbb)
@end
@implementation Son (bbb)
- (void)name:(double)a{
NSLog(@"my name is son bbb");
}
@end
Son* son = [[Son alloc]init];
[son name:a];
执行结果:
2018-01-05 10:29:15.625968+0800 test[50457:12615463] my name is son bbb
2018-01-05 10:29:15.626104+0800 test[50457:12615463] I am 6 years old
明显,子类son的category得到了执行,复写的age函数也是先执行的子类的,与分析的结果一致。
category导致的问题
从上面的结果中我们可以看到,category虽然给我们提供了便利,但是最大的问题就是不确定性,当出现重名的函数时,执行的结果很可能和预想的不一样。尤其在一些大的工程中,代码多,很可能出现重名的category。特别是很多工程采用sdk组件化集成,内部的代码都不知道实现了哪些category,当出现一些crash和执行错误的时候,很可能是执行了不同的category导致的。那么怎么控制呢?
- 首先是命名,category中要有自己的命名规范,根据category的名字给函数加前缀。由于objective-c没有namespace,只能通过前缀来区分。
@implementation Father (aaa)
- (void)aaa_Name:(char)a{
NSLog(@"my name is father aaa");
}
@end
@implementation Father (bbb)
- (void)bbb_Name:(char)b{
NSLog(@"my name is father bbb");
}
@end
- 对于一些公用的比较基础的category,放到基础库中,大家引用同一份,不要各自定义。
@interface NSString (Addition)
- (NSString *)urlEncode;
- (NSString *)md5Digest;
@end
- category的使用时机。个人认为,有2点:
- 如果要实现的功能,是对这个类普遍生效的,则使用category。如果是对单独场景的一种扩展,还是使用继承比较好。其实这种讨论类似于一个方法是要加到基类中,还是继承后实现到子类中。比如UIView,添加动画,frame设置这些基础方法,需要放到category中。像UIButton这中针对单独场景的特殊设置,就用的子类。
- 在大工程多个sdk组件化的工程中,对于其他模块封装的类,还是尽量使用子类化,category还是尽量实现在声明这个类的sdk中。