iOS符号冲突(duplicate symbols)

前言

背景是我们项目升级某个SDK,结果发现项目和SDK出现符号冲突。
符号冲突是接入SDK有可能会出现的问题,本文便尝试从技术角度去解决。

正文

因为本身项目本身不便公开,所以新建两个工程来模拟这个场景。(工程代码地址)
LYTestFramework静态库工程,里面带有不公开的SSUser类,模拟SDK;
LearnSymbol普通工程,模拟项目的主工程,里面也有SSUser类;

将LYTestFramework手动导入LearnSymbol工程:

这样便出现了两个SSUser:
一个是LYTestFramework.framework内不公开的SSUser;

@implementation SSUser
// from framework's user
- (void)test {
    NSLog(@"framework test");
}
@end

另一个是LearnSymbol工程内自己带的SSUser;

@implementation SSUser
// from project's user
- (void)test {
    NSLog(@"main test");
}
@end

那么编译的时候,就会出现duplicate symbol _OBJC_CLASS_$_SSUser的错误。

可是,当我真正开始运行的时候,才发现竟然编译通过了:

对比了这个新建工程和原工程的Other Linker Flags,发现是因为新工程少了一个-ObjC的设置,另外原工程还有-l secXXX的flag。

回顾下-ObjC 、 -all_load 、-force_load这三个flag的区别:

  • -ObjC 链接器会加载静态库中所有的Objective-C类和Category;(导致可执行文件变大)
  • -all_load 链接器会加载静态库中所有的Objective-C类和Category(这里和上面一样);当静态库只有Category时-ObjC会失效,需要使用这个flag;
  • -force_load 加载特定静态库的全部类,与-all_load类似但是只限定于特定静态库,所以-force_load需要指定静态库;当两个静态库存在同样的符号时,使用-all_load会出现duplicate symbol的错误,此时可以选择将其中一个库-force_load;(需要注意两个库的版本是不是一致的)

所以这里的直接编译通过的原因:工程中已经有了SSUser类的符号,所以链接的时候会直接使用工程中的SSUser符号,所以编译运行完的结果是调用了工程中的SSUser类,静态库中的SSUser并没有被链接。

而原工程的-l secXXX的链接flag是什么意思?
gcc有三个很像的参数,分别是-l -l -L,第一个I是i的大写,中间的是L的小写l。

  • -I,用于指定头文件的地址;
  • -l,用于指定具体的静态库、动态库;
  • -L,用于指定库文件的地址;

回到我们的工程,我们往Other Linker Flags添加-ObjC的flag之后,再次尝试编译。

此时终于复现了之前的符号冲突:

duplicate symbol _OBJC_CLASS_$_SSUser in:
    /Users/loyinglin/Library/Developer/Xcode/DerivedData/LearnSymbol-dhlwaeprifzeedegywrvodujmcoj/Build/Intermediates.noindex/LearnSymbol.build/Debug-iphonesimulator/LearnSymbol.build/Objects-normal/x86_64/SSUser.o
    /Users/loyinglin/Documents/Learn/LearnDuplicateSymbol/LearnSymbol/LearnSymbol/LYTestFramework.framework/LYTestFramework(SSUser.o)
duplicate symbol _OBJC_METACLASS_$_SSUser in:
    /Users/loyinglin/Library/Developer/Xcode/DerivedData/LearnSymbol-dhlwaeprifzeedegywrvodujmcoj/Build/Intermediates.noindex/LearnSymbol.build/Debug-iphonesimulator/LearnSymbol.build/Objects-normal/x86_64/SSUser.o
    /Users/loyinglin/Documents/Learn/LearnDuplicateSymbol/LearnSymbol/LearnSymbol/LYTestFramework.framework/LYTestFramework(SSUser.o)
ld: 2 duplicate symbols for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

接下来从尝试技术的角度去解决这个问题:
解决方案1:去掉主工程的SSUser,用静态库里面的SSUser;

不可行,静态库的SSUser没有开放头文件,无法使用;

解决方案2:在主工程的compile source去掉SSUser.m文件,工程中仅用SSUser.h文件来调用;(假设两方用的是同个版本)
尝试编译,符号冲突可以解决;
运行的结果表示调用了LYTestFramework中的SSUser:

2019-07-14 14:13:21.767218+0800 LearnSymbol[28982:5102302] framework test

解决方案3:去掉LYTestFramework静态库中的SSUser符号,链接时全部使用主工程的SSUser;
我们知道静态库是多个.o文件组成的,那么我们可以找到SSUser.o然后剔除,静态库依赖的SSUser会在链接时找到主工程生成的SSUser.o;
我们先进入打包的出来的LYTestFramework.framework文件夹,目录如下:

我们在Headers的同级目录创建一个目录pack,将LYTestFramework这个文件移动到pack目录中。
ar -t LYTestFramework指令,可以看到这个库中的.o文件包括SSUser.o,下面尝试手动移除这个SSUser.o文件:

  • 1、先将LYTestFramework解压:ar xv LYTestFramework
  • 2、手动删除SSUser.o文件;
  • 3、回到上级目录,重新把.o文件打包:ar rcs LYTestFramework pack/*.o

再用ar -t LYTestFramework指令查看,发现SSUser.o已经不见,重新打包成功运行,结果表示调用了主工程的SSUser:

2019-07-17 16:20:33.576468+0800 LearnSymbol[86290:7683465] main test

附1:这为了简化逻辑,这里只有模拟器的cpu架构,没有包括armv7/arm64,用 lipo -info LYTestFramework指令可以看到:
LYTestFramework is architecture: x86_64;
如果有多种cpu架构,需要分别对每种架构进行处理,再合并。

附2:以上的解决方案均是假设两方用的是同个静态库版本。如果是不同版本,则需要修改命名,使得多个版本的静态库可以共存。

另一个Linking中的选项:

Dead Code Stripping 是对程序编译出的可执行二进制文件中没有被实际使用的代码进行Strip操作。
Dead code stripping removes code that the compiler determines is unreachable.
代码举例:

总结

符号冲突是引入第三方库的时候,有可能会遇到的问题。
当库A和库B的符号出现冲突时,如果库A和库B冲突的符号,是功能相同的符号,则可以选择去掉其中一个符号,选择只加载其中一个库的符号。
如果两个符号所表示的意义不同,比如说不来自同一个库(仅仅是命名一样,导致符号冲突),或者来自同一个库但是版本不同,这种只能通过重命名或者修改库的代码逻辑来实现共存。

附录

静态库与动态库的思考
编译与链接过程的思考
https://blog.csdn.net/djl4104804/article/details/43099061
https://garbageout.wordpress.com/2015/03/24/dead-code-stripping/

你可能感兴趣的:(iOS符号冲突(duplicate symbols))