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

在实际项目过程中,我们经常会碰到引入三方的静态库后出现符号冲突的现象,也就是出现 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中冲突的解决方案
这个文章中作者有提供一个简单脚本来生成.

当然,除了 符号重定义+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

两种方式添加都可以

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345