CocoaPods 对 Xcode Assets 打包的诡异问题

好久没有写博客了,有一年多了吧,想想那些能够安心码字的日子还甚是怀念,于是今晚无论外界条件怎样的恶劣,这一篇是一定要更新的。

想必 CocoaPods 和 Carthage 对于 iOS 开发者而言都不会陌生,今天的这一篇我们就来看看混合使用这两者,以及多个 .xcassets 的情况下,一些莫名其妙的问题。

问题的出现

去年下半年,我们内部搭建了一个私有的 CI 平台,主要用于跑一些自动化和日常的测试打包。由于机器性能很强悍,很快成了大家构建新包的首选,原本应该是个愉快的事情,可是在系统这样跑了几个月后,恼人的问题突然就出现了:大家在自己的机器上编译没任何问题,而在 CI 里发起构建,每次都会失败在 Build Phases 中的 [CP] Copy Pods Resources 这一步,查看详细日志的话,主要是下面的这段错误:

error: None of the input catalogs contained a matching stickers icon set or app icon set named  "AppIcon".

我们的主工程比较大,代码和资源文件都很多,技术选型上是采用了 CocoaPods (1.5.x 版本) 来做模块化,模块拥有各自的 .xcassets 来存放资源,所以会有多个 .xcassets 文件。在此基础上,我们还依赖了一些 Swift 开源库,所以也使用了 Carthage 来管理依赖。

第一次解决问题

问题必须解决,但我们的 .xcassets 中,肯定是有一个含有 “AppIcon” 的,于是我找了一个时间,仔细分析了下错误的详细日志,发现里面有几处这样的警告:

warning: The app icon set name "AppIcon" is used by multiple app icon sets.

这个警告提示我们 “AppIcon” 冲突了,然后看了下冲突的路劲,尽然都是在 Carthage/Checkouts 目录下。由于我们依赖的 Carthage 库是以源码编译成 Framework 的,而这些源码中有示例和测试项目,其中包括了一些.xcassets,最主要的是 CocoaPods 把这些目录下的 .xcassets 都编译到了最终输出的目标中。

相关 issue: https://github.com/CocoaPods/CocoaPods/issues/6159#issuecomment-296698412

不查不知道,一查吓一跳啊,有种“我们 App 被偷偷植入了一些莫名其妙的资源”的感觉,也很庆幸以往的 AppIcon 能正常显示,甚至是很意外它尽然能正常显示。与此同时,错误日志中还有一些其他资源名称冲突的警告,我把这些资源名称和它对应的 .xcassets 名称一一对应的提取了出来,然后在模块间查找、对比,发现尽然存在名称一致但长相完全不同的图片,庆幸的是较新的图片得到了显示。捏了一把冷汗,在手动处理完所有名称冲突后,我把 CI 服务中的构建脚本修改了下,在编译前执行了下面命令:

1
rm -rf Carthage/Checkouts/**/*.xcassets || :

删除了这些不相干、也没任何作用的 .xcassets。做完这一切,我在 CI 上发起了一个构建,然后真的就成功了。可这没法解释为啥原先本地没问题,隐隐觉得还没有找到问题的主线,这次只是完成了一个支线任务。

大家又开始愉快地使用 CI 了。

第二次解决问题

时间飞快,好景不长,过了一个月左右,相同的问题、相同的错误信息再一次出现在我面前,而我再一次翻起那详细日志时,里面已没有了任何重复冲突的警告。这一次,我开始认真思考:本地发起构建和通过 CI 发起构建到执行 [CP] Copy Pods Resources 这一步,到底有什么区别?通过一系列测试,发现 Shell 的环境不同,但无法确定环境中哪些会影响到这一步执行,没办法,只能细看下 CocoaPods 这一步自动生成的脚本了:

1
2
# 其中 #{Target Name} 为主工程输出目标名称
Pods/Target Support Files/Pods-#{Target Name}/Pods-#{Target Name}-resources.sh

这个脚本主要就是拷贝和编译通过 CocoaPods 所依赖的资源文件,和 .xcassets 相关的主要是 XCASSET_FILES 这个变量,以及最后的这段脚本:

1
2
3
4
5
if [ -z ${ASSETCATALOG_COMPILER_APPICON_NAME+x} ]; then
printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
else
printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${TARGET_TEMP_DIR}/assetcatalog_generated_info_cocoapods.plist"
fi

所有的 .xcassets 文件都存在了 XCASSET_FILES 这个数组里面了,然后这个数组传递给 actool 来进行编译成 Assets.car 文件。脚本看完后,通过在文件头部加上 set -x 开启了它的调试,在茫茫的输出中,我死盯着 XCASSET_FILES 这个变量,然后惊人的发现这个变量中所有依赖而来的 .xcassets 都变成了两份!真心不知道 actool 在面对这些重复的路劲是怎么的处理,又捏了一把冷汗,于是乎在 Podfile 中加上了这样一些代码:

1
2
3
4
5
6
7
8
9
10
11
post_install do |installer|
# 其中 #{Target Name} 为主工程输出目标名称
copy_pods_resources_path = "Pods/Target Support Files/Pods-#{Target Name}/Pods-#{Target Name}-resources.sh"

str1 = 'printf "%s\0" "${XCASSET_FILES[@]}"'
str2 = 'printf "%s\n" "${XCASSET_FILES[@]}" | sort -u | tr \'\n\' \'\\\\0\' '

text = File.read(copy_pods_resources_path)
new_contents = text.gsub(str1, str2)
File.open(copy_pods_resources_path, "w") {|file| file.puts new_contents }
end

通过对 CocoaPods 自动生成文件内容进行替换,我们插入了一段脚本,最终对 XCASSET_FILES 中的条目进行了去重。做完这一切,我又在 CI 上发起了一个构建,然后它又成功了,然后还是没法解释为啥本地没问题,所以,注定了这还是一个支线任务。

可是,大家再一次愉快地使用 CI 了。

第三次解决问题

过了很长一段时间,长到我都以为这个问题真的彻底解决了,但冷不丁的就在前几天,这个问题又出现了。都说事不过三,这问题一次又一次的反复,也实在是让我颜面尽失,大过年的,你这该死又淘气的 CocoaPods。按捺住心中的烦躁不安,我又一次仔细地把那自动生成的脚本撸了一遍,可能是内心足够安静了吧,这一次我尽然只凭理论分析,就找到了罪魁祸首 xargs

xargs 不仅能正确处理空格之类的转义,还会在超过一定的限制后,把传递给它的参数分批传递给后续的命令,XCASSET_FILES 中就是我们所存储的参数。xargs 分批传递的限制主要是两个参数:-n 的条目限制和 -s 的大小限制,其中 -s 的大小限制受环境变量 ARG_MAX 影响。

所以,一旦我们的 XCASSET_FILES 被分批传递给了 actool,其中只有某一批里面有“AppIcon”,其它的自然会报错。由于环境不同,ARG_MAX 值不一致,这也解释了一直没法解释的那个问题。前面的两次修复,都不经意间缩减了 XCASSET_FILES 中的内容,而我们的 .xcassets 文件在慢慢增多,一旦突破了限制,问题就又出现了。

感觉找打了主线,于是修改了下 Podfile 中的代码:

1
2
3
4
5
6
7
8
9
10
11
post_install do |installer|
# 其中 #{Target Name} 为主工程输出目标名称
copy_pods_resources_path = "Pods/Target Support Files/Pods-#{Target Name}/Pods-#{Target Name}-resources.sh"

str1 = 'printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0'
str2 = 'printf "%s\n" "${XCASSET_FILES[@]}" | sort -u | tr \'\n\' \'\\\\0\' | xargs -0 -s 20480 -n 100'

text = File.read(copy_pods_resources_path)
new_contents = text.gsub(str1, str2)
File.open(copy_pods_resources_path, "w") {|file| file.puts new_contents }
end

搞完后,我在 CI 上再一次发起了构建,如预期的一样,再一次的成功了,我相信这问题不会再出现了。

大家又开始愉快地使用 CI 了。

总结一下

这个问题让我纠结了大半年时间,终于在这新春佳节里给彻底解决了,大体来说就是这样:

  • 如果你混合使用了 CocoaPods 和 Carthage,确认下 Carthage 的所有目录里是否有 .xcassets,如果有的话,确认下是否被打包到你最后的 App 里了
  • 注意 CocoaPods 生成的脚本中,XCASSET_FILES 里的条目有重复
  • CocoaPods 生成的脚本中,最终传递给 actool 编译的参数,一定不能被 xargs 分批传递