category似乎是OC提供的一种比较独特的功能,第三方程序员可以使用OC中的category为已有的类添加新的方法,不管这个类的源代码是不是可见的。这种扩展类的方式不需要创建子类,因此可以应用在很多场景下。
但是在实际使用category的时候,也有一些需要注意的地方。所以这里记一下因为我的疏忽而被坑过的地方>.<
写这篇的时候,森森感觉自己知道的东西太少了(´・_・`)对编译、链接还有运行时的动作都好模糊-.-虽然觉得可能漏洞百出,但还是先发上来吧,待我搞明白了更多事情,再来修改TAT
问题1:方法名冲突
官方文档中这样描述了category可能引起的方法名冲突:
If the name of a method declared in a category is the same as a method in the original class, or a method in another category on the same class (or even a superclass), the behavior is undefined as to which method implementation is used at runtime.
如果定义在category中的方法与原始类中的某个方法重名,或者与这个类(及其父类)的另一个category中的方法重名,那么在运行时,哪一个方法实现会被执行,则是不一定的。
因此,苹果建议为category中的方法名加上前缀,就像这样:
@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end
然而,在实际编程中,很多人都会忘了这一点(´・_・`)也包括我。
在使用category中的方法的时候,想当然的以为,当我import了这个category时,我发送的消息就会发送到import的category上。(因为用XCode点击方法名跳转到了我期望的那个方法的定义上-.-,莫名其妙的误导了我)
实际上,这和import了哪个头文件没什么关系。我的理解是,import的信息只在编译期间起作用。在运行时,运行时系统会将category中的方法加入到对应类的method_list中,变成和这个类的原有方法完全平等的方法。这篇文章解释了category的原理。
官方文档也表示:
The added methods are inherited by subclasses and are indistinguishable at runtime from the original methods of the class.
被加入的方法可以被子类继承,并且在运行时,无法区分category中的方法和这个类原有的方法。
问题2:静态库链接
这篇Q&A解释了在静态库中使用category的一个可能产生的问题:如果使用静态库中定义的category发送,则有可能在运行时报出selector not recognized错误。
为了了解发生这个问题的原因,首先要解释一下静态库的概念。《深入理解计算机系统》中这样描述静态库:
相关的函数可以被编译为独立的目标模块,然后封装成一个单独的静态库文件。然后,应用程序可以通过在命令行上指定单独的文件名字来使用这些在库中定义的函数。
在链接时,链接器将只拷贝被程序引用的目标模块,这就减少了可执行文件在磁盘和存储器中的大小。另一方面,应用程序员只需要包含较少的库文件的名字。
所以这个问题的产生,很可能是因为链接器在链接静态库时,没有链接到这个category所在的目标模块。
文档中的解释是,Objective-C不会为方法定义linker symbol,只会为类定义linker symbol。
比如我写了这样一个程序:
#import <Foundation/Foundation.h>
@interface XSQArray : NSArray
- (void)xsq_hello;
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
XSQArray *xsq_array = [XSQArray new];
[xsq_array xsq_hello];
return 0;
}
}
在build这个程序的时候,编译可以顺利执行,但链接时会报错:
Undefined symbols for architecture x86_64:
"_OBJC_CLASS_$_XSQArray", referenced from:
objc-class-ref in main.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
猜测_OBJC_CLASS_$_XSQArray
就是OC为XSQArray
生成的linker symbol,而由于这个类没有实现,所以链接器找不到linker symbol的定义,所以链接失败。
而这样的程序是可以通过链接的:
#import <Foundation/Foundation.h>
@interface XSQArray : NSArray
- (void)xsq_hello;
@end
@implementation XSQArray
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
XSQArray *xsq_array = [XSQArray new];
[xsq_array xsq_hello];
return 0;
}
}
即便xsq_hello
方法没有它对应的实现。
(猜测)这就说明了Objective-C不会为方法定义linker symbol。
所以在链接静态库的时候,使用category中的方法是不会生成linker symbol的,也就是说链接器可能不知道应该链接这个category对应的.o文件。
加上-ObjC
选项会让链接器载入静态库中所有实现了Objective C中的类或者category的成员。它可以解决这个问题,同时也可能会导致最终获得的可执行文件变大。所以这一选项不是默认的链接选项。
参考:
Category
Customizing Existing Classes
Building Objective-C static libraries with categories
刨根问底Objective-C Runtime(3)- 消息 和 Category
《深入理解计算机系统》
2015年9月19日更新
关于linker symbol的问题,后来我请教了我的老板。老板教我做了这样的实验:
首先,写了一些实验代码,分为两个文件:
//main.m
#import <Foundation/Foundation.h>
@interface NSObject (MyCategory)
- (void)sayHelloWorld;
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
[[NSObject new] sayHelloWorld];
return 0;
}
}
//MyCategory.m
#import <Foundation/Foundation.h>
@implementation NSObject (MyCategory)
- (void)sayHelloWorld {
NSLog(@"Hello World!");
}
@end
用clang
命令分别编译它们,输出.o文件。
然后用nm
命令查看.o文件的linker symbol,比如查看main.o文件的linker symbol:
nm main.o
得到:
0000000000000000 t -[NSObject(MyCategory) sayHelloWorld]
0000000000000027 s L_.str
0000000000000060 s L_OBJC_CLASS_NAME_
000000000000006b s L_OBJC_METH_VAR_NAME_
0000000000000079 s L_OBJC_METH_VAR_TYPE_
U _NSLog
U _OBJC_CLASS_$_NSObject
U ___CFConstantStringClassReference
0000000000000088 s l_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_MyCategory
00000000000000a8 s l_OBJC_$_CATEGORY_NSObject_$_MyCategory
这里,U
代表的是没有定义的符号,其中没有和sayHelloWorld
这个方法相关的。
用ar
命令可以创建静态库,将MyCategory.o打包进libCategory.a这个静态库中:
ar rcs libCategory.a MyCategory.o
此时出现了warning:
warning: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ranlib: warning for library: libCategory.a the table of contents is empty (no object file members in the library define global symbols)
表示libCategory.a中的符号表是空的(因为定义类中的方法不会产生linker symbol)。
链接main.o和libCategory.a,以及Foundation这个framework(加上-whyload
可以打印出静态库中每个.o文件被链接的原因):
clang main.o libCategory.a -framework Foundation -whyload
会输出一个a.out的可执行文件。
运行a.out,会报出预料中的运行时错误:
2015-09-19 21:57:51.097 a.out[63232:2132374] -[NSObject sayHelloWorld]: unrecognized selector sent to instance 0x7fca5a5138f0
2015-09-19 21:57:51.100 a.out[63232:2132374] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSObject sayHelloWorld]: unrecognized selector sent to instance 0x7fca5a5138f0'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff97eab03c __exceptionPreprocess + 172
1 libobjc.A.dylib 0x00007fff9015276e objc_exception_throw + 43
2 CoreFoundation 0x00007fff97eae0ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
3 CoreFoundation 0x00007fff97df3e24 ___forwarding___ + 1028
4 CoreFoundation 0x00007fff97df3998 _CF_forwarding_prep_0 + 120
5 a.out 0x0000000109999f27 main + 71
6 libdyld.dylib 0x00007fff8a04e5c9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Abort trap: 6
而使用了-ObjC
的linker flag之后:
clang main.o libCategory.a -framework Foundation -whyload -ObjC
输出了:
-ObjC forced load of libCategory.a(MyCategory.o)
意思是,由于-ObjC
这个linker flag,MyCategory.o被强制链接了进来。
此时运行新生成的a.out,可以成功输出“Hello World”。
网友评论