TinySolution:CocoaPods类重复实现警告处理

Galvin Li
11 min readFeb 10, 2019

--

English version also available: TinySolution: Fix CocoaPods duplicate implement warning

CocoaPods相信是很多iOS开发者都有使用的一个依赖管理工具,一般情况下CocoaPods是很方便的,因为它自动做了很多项目配置相关的处理,甚至连xcworkspace文件也自动帮我们进行了处理。

但在项目结构逐渐复杂的时候,CocoaPods的自动配置就会造成一点问题,这也是为什么Carthage会出现并成为部分开发者的选择。

不过Carthage的灵活性也是伴随着配置比较繁琐的问题,所以我的选择是依然使用CocoaPods,然后利用CocoaPods强大的脚本支持来解决出现的少量问题。

一些”无伤大雅”的运行时警告

本文的问题和代码片段我直接使用TinySolution工程内的示例作为说明,让问题更加清晰明了。

事情源于一个公司的项目,项目重构到一个阶段,我把项目的数据层抽取成了内部的Framework,为了让解除耦合实现得更加彻底,就如我经常强调的,约定虽然灵活,但总会在一些特定的场景被打破,而限制是长期稳定的。但抽取了Framework后,Xcode console下出现了一堆“无伤大雅”的运行时警告,就像Target CocoapodsDuplicateWarning运行时候的console输出。

说是无伤大雅,因为程序的运行并没有受到影响,加上console的输出信息一般都很多并且刷新迅速,所以一些开发者可能并没有留意到。

但我们详细看一下具体的警告信息是一些Class被重复实现了,因此虽然运行没有问题,但一个类被多次实现,而我们并不清楚多次实现之间的差异,还有运行时最终选取的具体实现,因此这里存在一定的风险。

问题的根本与处理方法

要解决问题,首先得定位到问题所在。那让我们整理一下警告的具体信息:

Class AFXXX is implemented in both .../CocoapodsDuplicateWarningFramework and .../CocoapodsDuplicateWarning. One of the two will be used. Which one is undefined.

简化内容后我们能发现其实是同一个Class在Framework和App同时实现了。而AFXXX我们知道是AFNetworking的类,看看Podfile我们可以看到其实只有CocoapodsDuplicateWarningFramework有引入AFNetworking,而CocoapodsDuplicateWarning只在项目配置内引入了CocoapodsDuplicateWarningFramework,并无引入AFNetworking。按道理不应该有重复实现的问题,应该是CocoaPods的bug。

经过一些搜索,网上也有人反映同样的问题:

https://github.com/CocoaPods/CocoaPods/issues/7155

https://stackoverflow.com/questions/46932341/class-is-implemented-in-both-one-of-the-two-will-be-used-which-one-is-undefine/52773670

有些人建议加上use_frameworks!解决这个问题,这确实可以去掉这个warning,但这只是绕过了问题,而非解决问题,静态库和动态库有不同的使用场景,所以这并非一个通用解决方案。

也有人找到了问题的根本,是在于CocoaPods生成的项目xcconfig文件的OTHER_LDFLAGS重复引入了同一个依赖库:

问题清楚了,解决方法也很简单,我们把Pods-CocoapodsDuplicateWarning.xxx.xcconfigOTHER_LDFLAGS-l"AFNetworking"去掉,就不会有警告了,因为下层的Framework已经引入了所需的依赖库。

实用的自动化处理

但我们不可能每次Pod install或者Pod update都手动删除重复的依赖,对自动生成的内容进行手动修改永远都是愚蠢的选择,这样容易出错并且工作量不少,因为依赖库会有多个,而且下层还会有依赖,因此我们需要一个自动化的解决方案。

CocoaPods是由Ruby编写,而Podfile文件其实就是一套特定的Ruby代码,除了常用的配置其实我们还可以添加逻辑代码,我在项目中添加了一个Target CocoapodsDuplicateWarningFix用于对比,所有配置都跟CocoapodsDuplicateWarning一样,直接运行也是会出现同样的运行时警告。

然后我们开始在Podfile下添加我们的逻辑代码:

post_install do |installer|

auto_process_target(['CocoapodsDuplicateWarningFix'], 'CocoapodsDuplicateWarningFramework', installer)

end

先加入的这段代码是一个入口,post_install是CocoaPods定义的一个事件hook,里面调用的auto_process_target(...)方法是实际的处理方法,三个参数分别是:

  1. 应用的target组,这里设定成数组方便配置同时修复多个target,例如我们在数组中直接加上CocoapodsDuplicateWarning就能同时修复掉target CocoapodsDuplicateWarning下的警告。
  2. framework名称,这里只支持单个framework,因为通常都是多对一,如果有多个framework都有这个问题也简单,只要多次调用auto_process_target(...)即可。
  3. 把installer传到方法内,无需定制配置。

然后我们需要加上auto_process_target(...)的实现方法:

def auto_process_target(app_target_names, embedded_target_name, installer)
words = find_words_at_embedded_target('Pods-' + embedded_target_name,
installer)
handle_app_targets(app_target_names.map{ |str| 'Pods-' + str },
words,
installer)
end

这里可以看到里面两个方法也相当简单,首先是words = find_words_at_embedded_target(...),我们从framework里面把依赖都取出来,然后通过handle_app_targets(...)方法在应用target下删除掉重复的依赖。具体实现如下:

def find_line_with_start(str, start)
str.each_line do |line|
if line.start_with?(start)
return line
end
end
return nil
end

def remove_words(str, words)
new_str = str
words.each do |word|
new_str = new_str.sub(word, '')
end
return new_str
end

def find_words_at_embedded_target(target_name, installer)
target = installer.pods_project.targets.find { |target| target.name == target_name }
target.build_configurations.each do |config|
xcconfig_path = config.base_configuration_reference.real_path
xcconfig = File.read(xcconfig_path)
old_line = find_line_with_start(xcconfig, "OTHER_LDFLAGS")

if old_line == nil
next
end
words = old_line.split(' ').select{ |str| str.start_with?("-l") }.map{ |str| ' ' + str }
return words
end
end

def handle_app_targets(names, words, installer)
installer.pods_project.targets.each do |target|
if names.index(target.name) == nil
next
end
puts "Updating #{target.name} OTHER_LDFLAGS"
target.build_configurations.each do |config|
xcconfig_path = config.base_configuration_reference.real_path
xcconfig = File.read(xcconfig_path)
old_line = find_line_with_start(xcconfig, "OTHER_LDFLAGS")

if old_line == nil
next
end
new_line = remove_words(old_line, words)

new_xcconfig = xcconfig.sub(old_line, new_line)
File.open(xcconfig_path, "w") { |file| file << new_xcconfig }
end
end
end

具体实现逻辑就不一一解释,直接使用的话直接配置并调用auto_process_target(...)则可,底层实现不需要调整,如果有兴趣了解慢慢细读一下也不难理解。

如果代码添加正确,在每次执行Pod install或者Pod update都会自动清除多余的依赖,并且在终端有相应的提示输出。

最后我们可以看看CocoapodsDuplicateWarning和CocoapodsDuplicateWarningFix的xcconfig内容,多余的-l"AFNetworking"确实被移除了:

然后我们运行CocoapodsDuplicateWarningFix就没有在看到运行时警告了,问题永远解决了,引入新的依赖也不需要手动做任何维护。

  • 本文用到的代码均可以在GitHub项目里面找到。
  • 如果你对文中的内容有疑问或者建议,欢迎留言讨论。
  • 如果你觉得文中内容有价值,请转发让更多人可以看到。
  • 如果你喜欢这类型内容,欢迎follow我的MediumTwitter,我会持续发布更多有用内容给大家。

--

--

Galvin Li

A Tiny iOS developer who love to solve problems and make things better.