静态库冲突问题思路全解

iOS开发中,经常会遇到静态库冲突的问题, xcode报错关键词是duplicate symbol xxx;造成冲突的常见原因一般有两种

  1. 项目中引入了命名不同的同一个静态库

以openssl为例 同一份源码打包生成libopenssl.a 和openssl.a 两份静态库,引入到项目中时, 会报错

上文提到造成冲突的原因是引入不同名字的同一个静态库, 为什么同名的不会冲突呢?

这里其实是编译器对同名静态库的链接做了优化防止冲突,具体优化思路是编译器进行符号链接时如果已经绑定了一个静态库, 则后续同名静态库不会再链接, 这里针对的默认场景下的, 如果修改了xcode build setting下的other linker flags则会对这一问题造成影响

  1. 引入了不同的SDK 例如moduleA和moduleB, 其中moduleA & moduleB 中引用了同一个静态库

根据Xcode报错信息我们了解到静态库冲突的本质其实是符号(Symbol)冲突, 而且是全局符号导致的冲突;通过llvm nm -gUAj xx.o可以查看MachO文件的全局符号

解决静态库冲突的途径也就是解决符号冲突, 可以从以下几个方面入手:

  • 删除冲突的符号: 

    我们都知道静态库的本质是编译好的.o目标文件的集合, 所以可以通过删除冲突符号所在的.o文件解决冲突

    1. 首先查看.a静态库支持的架构, 可以使用lipo或者file命令

lipo -info libssl.a
file libssl.a
复制代码

    2. 针对冲突符号所在架构, 拆分出单一的架构, 以arm64为例

lipo -thin arm64 libssl.a -output  libssl_arm64.a
复制代码

    3. 针对单一架构输出.o到文件, 静态库操作都可以通过ar命令来实现, 如果报错 Inappropriate file type or format, 请校验静态库是否是单一架构


ar -x libssl_arm64.a 

# 如果仅删除少量目标文件, 可以通过`ar -d xx.o`实现单个删除,无需重新打包
复制代码

    4. 删除冲突的符号后, 重新打包生成单架构静态库 使用libtool 或者ar

libtool -static -o libssl_arm64_new.a *.o
ar -rcs libssl_arm64_new.a *.o
复制代码

    5. 将多个架构静态库合并为一个, 首个参数是合成的静态库命名, 后面的是需要合并的库

lipo -creata -output libssl_new.a libssl_arm64_new.a libssl_armv7_new.a
复制代码
  • 冲突符号改名
    • 如果有源码,那么可以直接修改源码符号名解决冲突, 没有难度, 只是工作量的问题
    • 如果没有源码, 针对.a静态库修改符号表实现改名,  使用llvm objcopy --redefine-syms 可以修改符号名, rename_symbols中存储修改的符号和新的符号名, 格式要求可以通过 man llvm objcopy查看

 /usr/local/opt/llvm/bin/llvm-objcopy --redefine-syms rename_symbols xxx.o

复制代码

我在测试过程中发现llvm-objcopy 在对objective-c .m生成的.o目标文件修改时会报错,内容为:**error: unsupported load command (cmd=0x2d) ** 表示找不到Load Command: LC_LINKER_OPTION;但是改为.mm后则可以正常修改, 

如果有了解的同学, 请联系我, 欢迎指教.

  • 修改冲突符号的类型

需要解决的问题有两个, 如何将冲突符号类型由Global修改为Local以及符号修改后,静态库中其他目标文件如何链接该符号

解决思路是通过ld命令将整个静态库链接成一个目标文件, 这样不会导致隐藏全局符号后静态库内部链接不到符号的问题, 

符号类型可以用ld的参数设置, 默认非导出符号都是local, 导出符号都是global

unexported_symbols_list 参数设置不导出的符号列表

exported_symbols_list 设置导出符号列表, 

具体步骤可以shell脚本处理


ARCH_LIST=("armv7" "arm64" "x86_64" "i386")

ARCH_COUNT=${#ARCH_LIST[@]}



do_fix()

{

ARCH=$1;

echo "fix for ${ARCH}..."



rm -rf ${ARCH}

mkdir ${ARCH}



#  架构抽取

lipo -thin ${ARCH} libcrypto.a -output "./${ARCH}/${ARCH}.a"



# 提取静态库.o目标文件

cd "./${ARCH}"

mkdir "objs"

ar -x "${ARCH}.a"

`mv *.o objs`

mv "__.SYMDEF SORTED" "objs"



# 目标文件提前链接, 这里可以解决符号隐藏后,内部无法链接的问题, 隐藏之前已经链接结束

cd "objs"

`ld -r *.o -o combine.o`

cp "combine.o" ../

# 输出全局符号作为隐藏符号的来源
cd ..
nm -g -j combine.o > hidden_symbols

# 剔除指定文件中的全局符号(需要对外开放的符号) 
`cat ../global_symbols > tmp_symbols`
`cat "\n" >> tmp_symbols`
`cat hidden_symbols >> tmp_symbols`
`sort tmp_symbols|uniq -u > real_hidden_symbols`

# 重新连接隐藏符号
ld -x -r -unexported_symbols_list real_hidden_symbols combine.o -o hidden.o
nm -g hidden.o > exits_symbols

# 生成新的包
ar -rv "fix.a" hidden.o
cd ..
}

for ((i=0; i < ${ARCH_COUNT}; i++))
    do
    do_fix ${ARCH_LIST[i]} 
done

LIB_PATHS=( ${ARCH_LIST[@]/#/} )
LIB_PATHS=( ${LIB_PATHS[@]/%//fix.a} )
lipo ${LIB_PATHS[@]} -create -output fix_libcrypto.a
复制代码
  • 动态库包装: 思路很简单,就是创建一个新的动态库, 动态库引用冲突的静态库, 需要注意的是,新创建的动态库要设置other linker flag 以使静态库会被链接(默认未使用的静态库不会链接); 解决的依据是被动态库包装后, 冲突符号变为local本地符号,不会与静态库中的global全局符号冲突, 不会在静态链接期间编译报错, 但是再运行时会在控制台提示 Class xxx is implemented in both的信息

**优缺点分析: **

  • 删除静态库冲突文件, 这种方式优点是无需源码就可以实现, 缺点也很明显, 包含删除了目标文件的库无法单独使用, 而且如果冲突版本不一致也可能造成undefine symbols的问题

  • 修改符号名称: 统一的缺点是调用时需要修改成新符号

    • 有源码情况下修改冲突符号的名称, 一般工作量比较大, 需要修改符号定义和符号调用,冲突的静态库后续版本升级也会造成一定工作量
    • 如果是直接修改的静态库中的符号表, 后续版本升级也需要每次都做重命名操作(可以通过脚本实现), 冲突的库可以同时使用,适用于不同版本库的符号兼容, 
  • 隐藏全局符号方式, 优点是冲突的库可以同时存在, 升级不涉及符号名修改, 缺点是静态库集成为一个目标文件, 编译器对静态库的按需链接无法优化,造成体积增大, 

  • 动态库包装的方式, 会涉及库引入方式的修改, 但是对不同版本冲突库比较友好, 

根据静态库冲突出现的场景分析, 大多数是由于不同的库中引入了同一个静态库导致的, 所以要求我们在做SDK时, 如果需要依赖第三方静态库, 不要直接集成到SDK中, 而是通过集成文档的方式提示业务方去主动集成依赖的静态库

目前大部分知名三方SDK也是这么做的

参考

How to create static library for iOS without making all symbols public

An objcopy equivalent for Mac / iPhone?

llvm objcopy

猜你喜欢

转载自juejin.im/post/7052506797654933541