静态库中的类别符号(-ObjC\-all_load\-force_load)

1. 项目结构:

image.png

其中,SimpleStatic是一个静态库项目,我们将头文件Person.hPerson+MyPerson.h暴露出来供外部使用.

Symbol工程是主项目.

main.m中的代码为:

#import 
#import 
#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
      
        [p todoSomething];
        
        //extern void test(void);
        
        //test();
    }
    return 0;
}

Person.m中代码为:

#import "Person.h"
@implementation Person
- (void)aabbccdd {
    
}

Person+MyPerson.m中代码为:

#import "Person+MyPerson.h"

void test() {
    NSLog(@"这是test");
}

@implementation Person (MyPerson)
- (void)todoSomething {
    NSLog(@"这是person分类");
}
@end

Config.xcconfig中内容为:

LD_MAP_FILE_PATH = ${SRCROOT}/myfile.m
LD_GENERATE_MAP_FILE = YES

这主要是为了生成link map文件.

2. 运行

此时,build项目Symbol会发现没有任何问题.
然而,运行时发现项目崩溃:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person todoSomething]: unrecognized selector sent to instance 0x1004814f0'

根据网上资料,可以很容易的查到,这是因为main.o编译成可执行文件的时候,没有链接Person+MyPerson.o导致的.解决方法是在Other linker flags中添加-ObjC.它的含义是:链接静态库中所有包含OC类和类别的目标文件

3. 如何验证

可以通过两次生成的link map文件进行查看.
打开myfile.m:

# Object files:
    [  0] linker synthesized
    [  1] /Users/LQ/Library/Developer/Xcode/DerivedData/Symbol-akbspkizvxjwrmamodsanattllyd/Build/Intermediates.noindex/Symbol.build/Debug/Symbol.build/Objects-normal/x86_64/main.o
    [  2] /Users/LQ/Library/Developer/Xcode/DerivedData/Symbol-akbspkizvxjwrmamodsanattllyd/Build/Products/Debug/SimpleStatic(Person.o)
    ...

由此可以判断出链接了哪些目标文件.

现在,把main.m中调用test()方法的地方打开注释,重新运行.

注意:此时没有添加-ObjC.

结果发现link map文件中已经可以链接到Person+MyPerson.o

...
[  2] /Users/LQ/Library/Developer/Xcode/DerivedData/Symbol-akbspkizvxjwrmamodsanattllyd/Build/Products/Debug/SimpleStatic(Person.o)
[  3] /Users/LQ/Library/Developer/Xcode/DerivedData/Symbol-akbspkizvxjwrmamodsanattllyd/Build/Products/Debug/SimpleStatic(Person+MyPerson.o)

4. 为什么明明调用了分类的方法todoSomething,但是没有链接到分类?而假如在分类.m中定义一个c函数void test(void) {},在项目中调用test()却能链接到SimpleStatic(Person+MyPerson.o)?

这是因为C和OC有所区别:

当源文件使用在另一个文件中定义的东西(比如函数)时,一个未定义的符号被写入到目标文件中,以“替代”丢失的东西。链接器通过在构建最终可执行文件时拉入包含未定义符号定义的对象文件来解析这些符号。

例如,如果main.c使用函数foo(),其中foo在另一个文件B.c中定义,那么对象文件main.o将具有foo()的未解析符号,而B.o将包含foo()的实现。在链接时,B.o将被引入到最终的可执行文件中,因此main.o中的代码现在引用B.o中定义的foo()的实现。

UNIX静态库只是对象文件的集合。通常,如果这样做会解析一些未定义的符号,那么链接器仅从静态库中提取对象文件。不拉入所有对象文件会减小最终可执行文件的大小。

Objective-C的动态特性使事情稍微复杂一些。因为实现方法的代码直到方法被实际调用才确定,所以Objective-C不为方法定义链接器符号。链接器符号仅为类定义。

例如,如果main.m包含代码[[FooClass alloc]initWithBar:nil];那么main.o将包含FooClass的未定义符号,但是-initWithBar:方法的链接器符号将不在main.o中。

由于类别是方法的集合,因此使用类别的方法不会生成未定义的符号。这意味着链接器不知道加载定义类别的对象文件(如果类本身已经定义)。这会导致和未实现方法时一样的运行时错误"selector not recognized" 。

参考链接:oc静态库和类别

根据以上说法实现方法的代码直到方法被实际调用才确定,猜测是因为方法调用本质上是objc_msgSend,因此不会生成方法名的符号.

而调用C函数test()是因为需要链接Person+MyPerson.o,此时会一并将此目标文件中的其他符号导入,因此不会发生崩溃.

5.如何验证猜想?

首先将main.m编译成目标文件:

clang -fmodules -c main.m -o main.o -I/Users/LQ/Desktop/Test/OC/Symbol/SimpleStatic/SimpleStatic

注意:此时注释掉main中调用test函数的地方,并且没有添加-ObjC

使用nm命令查看该文件的符号表如下:

LQ-Pro:Symbol LQ$ nm ./main.o
                 U _NSLog
0000000000000078 s _OBJC_CLASSLIST_REFERENCES_$_
                 U _OBJC_CLASS_$_Person
00000000000000b8 s _OBJC_SELECTOR_REFERENCES_
                 U ___CFConstantStringClassReference
0000000000000000 T _main
                 U _objc_alloc_init
                 U _objc_autoreleasePoolPop
                 U _objc_autoreleasePoolPush
                 U _objc_msgSend

可以看到,此时main.o中根本没有OC方法todoSomething的链接符号,取而代之的则是_objc_msgSend.

现在,打开test()函数的注释,重新查看main.o中的符号:

0000000000000060 s _OBJC_CLASSLIST_REFERENCES_$_
                 U _OBJC_CLASS_$_Person
0000000000000078 s _OBJC_SELECTOR_REFERENCES_
0000000000000000 T _main
                 U _objc_alloc_init
                 U _objc_autoreleasePoolPop
                 U _objc_autoreleasePoolPush
                 U _objc_msgSend
                 U _test

_test符号是未定义的,因此在链接阶段,就会导入Person+MyPerson.o中的符号.

使用nm查看最终的可执行文件:

0000000100003e90 t -[Person aabbccdd]
0000000100003ec0 t -[Person(MyPerson) todoSomething]
                 U _NSLog
                 U _OBJC_CLASS_$_NSObject
0000000100008128 S _OBJC_CLASS_$_Person
                 U _OBJC_METACLASS_$_NSObject
0000000100008100 S _OBJC_METACLASS_$_Person
0000000100008028 s __OBJC_$_INSTANCE_METHODS_Person(MyPerson)
00000001000080a8 s __OBJC_CLASS_RO_$_Person
0000000100008060 s __OBJC_METACLASS_RO_$_Person
                 U ___CFConstantStringClassReference
0000000100008150 d __dyld_private
0000000100000000 T __mh_execute_header
                 U __objc_empty_cache
0000000100003e00 T _main
                 U _objc_alloc_init
                 U _objc_autoreleasePoolPop
                 U _objc_autoreleasePoolPush
                 U _objc_msgSend
                 U _objc_storeStrong
0000000100003ea0 T _test
                 U dyld_stub_binder

可以看到分类和类的符号都导入了.

6. 其他

  1. 对于使用OC方法的文件(即main.m引用Person的方法),不会生成该方法的链接符号;但是对于定义OC方法的文件(即Person.m),会生成链接符号.

前半句意思是在main.o中,是不能看到OC的方法符号的.

后半句的意思是,假设我们此时去查看Person.oPerson+MyPerson.o,是可以分别看到-[Person aabbccdd]-[Person(MyPerson) todoSomething]符号的.

  1. Other linker flags一些选项
  • -ObjC:强制静态库中所有实现了OC类和类别的.o文件被链接
  • -all_load:强制加载所有静态库中的目标文件.这是因为当静态库中只有OC类别时,-ObjC还是无法链接类别
  • -fore_load:后面必须跟一个静态库路径.强制加载单个静态库所有目标文件.

你可能感兴趣的:(静态库中的类别符号(-ObjC\-all_load\-force_load))