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

xcode 选择target -> buildSetting -> Other Linker Flags

先说结论,Other linker flags一些选项

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

1. 项目结构:

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

Symbol工程是主项目.

main.m中的代码为:

#import <Foundation/Foundation.h>
#import <Person.h>
#import <Person+MyPerson.h>

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)
    ...

由此可以判断出链接了哪些目标文件。此时只连接主类的Person.o, 没有类别文件Person+MyPerson.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的符号,_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的方法),不会生成该方法的链接符号;
    1.  在main.o中,是不能看到OC的方法符号的。因为OC是消息转发,只会引用到_objc_msgSend,不会生成具体的方法。
  2. 对于定义OC类的文件(比如主类 Person.m),会生成链接符号.
    1. 假设我们此时去查看Person.oPerson+MyPerson.o,是可以分别看到-[Person aabbccdd]-[Person(MyPerson) todoSomething]符号的.
    2. Objective-C没有为每个函数(或者方法)定义链接符号,它只为每个主类(定义类)创建链接符号,不包含类别。

Objective-C的一个重要特性:类别(category)。Unix的标准静态库实现和Objective-C的动态特性之间有一些冲突:Objective-C没有为每个函数(或者方法)定义链接符号,它只为每个类创建链接符号。这样当在一个静态库中使用类别来扩展已有类的时候,链接器不知道如何把类原有的方法和类别中的方法整合起来,就会导致你调用类别中的方法时,出现"selector not recognized",也就是找不到方法定义的错误。为了解决这个问题,引入了-ObjC标志,它的作用就是将静态库中所有的和对象相关的文件都加载进来。

      本来这样就可以解决问题了,凡事无绝对。链接器有一个bug,会导致只包含有类别的静态库无法使用-ObjC标志来加载文件,或者添加了也还是加载不进来。(也可以添加一个空的主类避免这种情况)

如果无法修改源码,变通方法是使用-all_load或者-force_load标志,它们的作用都是加载静态库中所有文件,不过all_load作用于所有的库,而-force_load后面必须要指定具体的文件。

最终结论,Other linker flags一些选项

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



链接:https://www.jianshu.com/p/cc821a0bab9b

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值