论项目中静态库符号冲突的几种解决方式

在实际项目过程中,我们经常会碰到引入三方的静态库后出现符号冲突的现象,也就是出现 duplicate symbols 错误,那么如何解决这类冲突呢?

这里我们区分几种不同的冲突情况

最复杂的情况: 项目中使用的 libSDKA.a 和 libSDKB.a中有符号冲突,这里假定两者都包含了很多同名的代码等

这里,两者重复的符号,并一定是在同一个文件中,或者即使在同一个.o中,但是,鬼知道它们有没有对这些重复符号的类啥的方法添加了另外的内容,也就是说即使它们两包含了同一份代码,也可能是并不兼容的两个版本.

这里的解决办法
最先能想到的肯定是让这两个静态库的开发者中的任意一个修改下他们的实现,重新打个包过来,简单,且解决的彻底!
不过,现实中往往只能呵呵,这两个库的开发者,不一定配合.

那么这个时候怎么解决?
剩下的唯一办法,也就是 二进制的符号重命名了!!
目前,对于二进制的符号重命名,并没有什么特别好的办法,本来,感觉这个并不难,按说,应该有一些工具可以用来重命名二进制的符号,很可惜,找了一圈,没有这样的工具,如果有,烦请各位大神也告知我一声,谢谢
于是乎,在受到欧阳大哥的静态拦截iOS对象方法调用的简易实现的启发,外加最近刚好重新阅读了pod package的实现mangle的源码,于是乎想
我们如果先获取了所有需要重命名的符号
是不是可以直接进行二进制的字符串替换来实现符号重命名呢?
当然了,因为并没有二进制符号的直接修改工具,所以这里的符号重命名后的名字的长度一定要同原来的名字是同样的长度!!!! 否则就破坏了mach-o格式了,也就没法被识别和加载了,切记!
理论还是要实践来检验,于是
我们用一个例子来检验,为了更加结合实践,这里,我选择一个稍微复杂点的库来测试 FLEX
我们生成个FLEX的静态库 libFLEX.a
创建一个demo工程,拖入两个 libFLEX.a(当然,一个重新命名为libRenameLib.a )
编译运行
重复符号
不出意外,报错了,提示有1028个dumlicate symbols

我们首先是要提取libFLEX.a中的所有的需要重命名的符号,这个脚本,我们可以直接提取pod package源码中的提取部分来修改
这里我附上处理的代码 getAllname.rb

#!/usr/bin/ruby

# 提取某个库的满足条件的符号
def symbols_from_library(library)
	if !library
		puts "文件不存在!"
		return
	end
    # --defined-only :Display only defined symbols
    # -g, --extern-only      Display only external symbols
    syms = `nm -defined-only -extern-only #{library}`.split("\n")  # 获取一个满足符号的数组
    result = classes_from_symbols(syms)
    result += constants_from_symbols(syms)

    result.select do |e|
      case e
      when 'llvm.cmdline', 'llvm.embedded.module', '__clang_at_available_requires_core_foundation_framework'
        false
      else
        true
      end
    end
  end

 #获取所有的class符号
  def classes_from_symbols(syms)
    classes = syms.select { |klass| klass[/OBJC_CLASS_\$_/] }  #字符串的正则查找,满足前缀是OBJC_CLASS_$_
    classes = classes.uniq
    classes.map! { |klass| klass.gsub(/^.*\$_/, '') }
  end

  #获取所有的常量,全局变量符号
  def constants_from_symbols(syms)
    consts = syms.select { |const| const[/ S /] }
    consts = consts.select { |const| const !~ /OBJC|\.eh/ }
    consts = consts.uniq
    consts = consts.map! { |const| const.gsub(/^.* _/, '') }

    other_consts = syms.select { |const| const[/ T /] }
    other_consts = other_consts.uniq
    other_consts = other_consts.map! { |const| const.gsub(/^.* _/, '') }

    consts + other_consts
  end


  # 输出所有的符号
  def create_symbols_file(library)
  	if !library
		puts "文件不存在!"
		return
	end
  	syms = symbols_from_library(library)
  	syms = syms.uniq  #去重
  	#puts "syms = #{syms}"
    create_output_symbols_file(syms)
  end



  def create_output_symbols_file(syms)
    all_new_str = ""
    syms = syms.sort_by {|x| x.length} #按长度来排序

    syms.each do |sym|
      symDest = sym.clone;
      symDest[symDest.length - 1] = '2'  #这个规则可以自己定,这里我是把符号的最后一位换为2
      all_new_str += "#{sym}==#{symDest}\n"

    end
    aFile = File.new("./input.txt", "w+")
    if aFile
      aFile.syswrite(all_new_str)
      aFile.rewind
    else
      puts "Unable to open file!"
    end
  end


lib = ARGV[0]
#puts("ARGV= #{ARGV},lib = #{lib}")
create_symbols_file(lib)

使用的时候 传入需要获取修改的符号的文件名

ruby ./getAllname.rb ../libFLEX.a

在当前目录会生成一个input.txt,所有需要重命名的符号都在这个文件中.

接下来,使用如下的脚本 rename.rb 来对二进制的符号进行重命名

#!/usr/bin/ruby

require 'fileutils'
require 'pathname'
 
$symbol_file = ''
def disposeBin(oringin,dest)
	binnaryFile = $symbol_file
	command = "LC_CTYPE=C sed -i '' 's/#{oringin}/#{dest}/g' #{binnaryFile}"
	puts("command = #{command}")
	result = `LC_CTYPE=C sed -i '' 's/#{oringin}/#{dest}/g' #{binnaryFile}`
	output = result
	#puts("output = #{output}")
	return output
end
def disposeLine(line)
	arr = line.split("==")
	origin = arr[0].chomp
	dest = arr[1].chomp
	puts ("origin = #{origin},dest = #{dest}")
	return disposeBin(origin,dest)

end


def main
	$symbol_file = ARGV[0]
	fileName = ARGV[1]
	if !$symbol_file || $symbol_file == ''
		puts "文件不存在!"
		return
	end
	if !fileName
		puts "符号文件不存在!"
		return
	end

	#获得当前执行文件的完整路径
	path =File.dirname(__FILE__)
	#path = Pathname.new(__FILE__).realpath
	puts(path)
	puts("cp -f #{$symbol_file} #{path}/libDispose.a")
	`cp -f #{$symbol_file} #{path}/libDispose.a`
	$symbol_file = "#{path}/libDispose.a"

	File.open(fileName, "r") do |file|
    	file.each_line do |line|
        	res = disposeLine(line)
        	if !res
        		return
        	end

    	end
	end

end


main


使用的时候

ruby ./rename.rb ../libFLEX.a ./input.txt

传入的是 二进制文件名和上面生成的要重名的符号文件
将生成的新的libDispose.a拷贝到demo工程中,可以看到,可以编译运行了,并且两个都可以使用!!
image.png

当然,其实还有一种方式:静态库动态化
将某个静态库用动态库来包含,也就是静态库动态化,这样也可以解决问题, 我们继续可以尝试下:
建立一个Dynamic framework的工程(记得添加工程的 other link flags中的-ObjC,不然静态库中的很多内容不会添加到动态库中)
将 libFLEX.a 添加进去,生成动态库
image.png
将这个生成的DymamicContainer.framework添加到测试工程中,运行,也是可以的,只是会出现运行时警告
image.png
虽然编译是通过了,不过这种方式带来的问题,还是比较麻烦的
如果两个重复的符号的实现,完全一致,那非常好,皆大欢喜
如果两个重复的符号的实现是不一致的,那由于不确定加载的会是哪个,导致最后的行为是不可预知的
这种实现,其实并不好,通常只是在两个库的重复符号的实现完全一致的情况下才比较好
相对来说,第一种办法,是一种非常好的方式:
并不需要两个静态库中的重复符号的实现是一致的!!

三方库包含了某个开源代码

例如,某静态库libSDK.a中融入了开源代码AFnetworking,对于这种比较简单的情况来说,我们有两种处理方式

第一种处理方式:核心就是将.a分离成.o文件,然后把重复的.o文件去掉再重新打包成.o文件.
查看libSDK.a的架构信息
lipo -info libSDK.a 或者 file libSDK.a

file libWeChatSDK.a
libWeChatSDK.a: Mach-O universal binary with 4 architectures: [i386:current ar archive] [arm_v7] [x86_64] [arm64]
libWeChatSDK.a (for architecture i386):	current ar archive
libWeChatSDK.a (for architecture armv7):	current ar archive
libWeChatSDK.a (for architecture x86_64):	current ar archive
libWeChatSDK.a (for architecture arm64):	current ar archive

或者

lipo -info libWeChatSDK.a
Architectures in the fat file: libWeChatSDK.a are: i386 armv7 x86_64 arm64

一般的静态库都会包含真机arm64,armv7和模拟器x86_64三种架构.
对于每种架构要分别处理,然后再合并(为什么要分离? 因为每种架构里面都有同样的.o文件啊,如果你不分离,不是乱套了....)

对于每种架构,例如arm64:

  1. 创建一个临时文件夹 mkdir arm64,分离出arm64架构
lipo -thin arm64 libWeChatSDK.a -output arm64/libWeChatSDK_arm64.a

2)解压出object file(即.o后缀文件)

cd arm64 && ar -xv libWeChatSDK_arm64.a

输出的结果是
.
├── .DS_Store
├── AppCommunicateData.o
├── WXApi+ExtraUrl.o
├── WXApi+HandleOpenUrl.o
├── WXApi.o
├── WXApiObject.o
├── WXLogUtil.o
├── WapAuthHandler.o
├── WeChatApiUtil.o
├── WeChatIdentityHandler.o
├── WechatAuthSDK.o
├── .SYMDEF
└── base64.o
这里的
.SYMDEF 文件是符号定义,其内容是要被要被加载的符号.

  1. 找到冲突的.o,例如AppCommunicate.o,删除它
rm AppCommunicate.o

其实如果我们知道要移除的是哪个.o,那么可以直接使用命令

ar -d libWeChatSDK_arm64.a AppCommunicate.o

从.a中直接删除.o,省却了先分离,删除,再合并!

  1. 重新打包object file
cd .. && ar rcs libWeChatSDK_arm64 arm64/*.o

-s表示无论ar 命令是否修改了库内容都强制重新生成库符号表
当然,还可以用

libtool -static -o ../libWeChatSDK_arm64.a *.o

这两个生成的.a效果是一样的

在当前目录生成了去除了AppCommunicate.o后的libWeChatSDK_arm64
这个时候,可以确认下这个新的libWeChatSDK_arm64还有没有那个AppCommunicate.o了,用命令

ar -t libWeChatSDK_arm64

可以看到,列表里已经没有AppCommunicate.o了.说明ok了

将其他几个架构(armv7s, x86_64,i386)等重复上面的1)-4)步骤

最后将去掉AppCommunicate.o的各种架构,合并成新的.a文件

lipo -create libWeChatSDK_arm64 libWeChatSDK_armv7.a  -output  libWeChatSDK-new.a

覆盖掉项目中原来的文件,即可!!

当然,这种方式也是有缺点的 ,你不知道这个静态库中重复的.o文件是不是同你项目中的版本兼容,如果不兼容呢,这样去掉了,会导致行为的错误.或者说,假如这个静态库中包含的重复符号所在的文件进行了他们自定义的修改呢?这样去掉,也是有问题的.

第二种办法:
由于,libSDK.a中包含了AFnetworking的符号,那简单啊,直接把项目中用到了AFnetworking整个的重命名,也就是重命名包括类名、分类名、全局常量名、协议名等会导致冲突的符号.这也是目前很多项目中的做法,这种做法,修改的地方太多了,且会造成另外一种问题,就是每次如果要升级AFnetworking,那么要整个的再重新修改一次,太麻烦了,属于典型的吃力不讨好!
我们不去除libSDK.a中用到的AFnetworking,我们可以建立个预编译的头文件,将AFnetworking的所有符号都重新宏定义成另外的名字,这个在[# iOS静态库开发中引入的第三方库可能与宿主APP中冲突的解决方案
](https://my.oschina.net/u/4312161/blog/3449819)这个文章中作者有提供一个简单脚本来生成.

当然,除了 符号重定义+pch

还可以使用 GCC_PREPROCESSOR_DEFINITIONS 的形式

xcodebuild GCC_PREPROCESSOR_DEFINITIONS='$(inherited) PodsDummy_FLEX=PodFLEX_PodsDummy_FLEX FHSRangeSlider=PodFLEX_FHSRangeSlider FHSSnapshotNodes=PodFLEX_FHSSnapshotNodes' CONFIGURATION_BUILD_DIR=build clean build -configuration Debug -sdk iphonesimulator -arch x86_64 -target RenameLib -project RenameLib.xcodeproj

两种方式添加都可以

comments powered by Disqus