在Pod库中使用xcasset的拷贝陷阱

本篇文章来自笔者工作中遇到一个难解的BUG - 在App中用UIImageimageNamed:方法读取的图片始终是不正确的。

暴走示意

场景条件回放:

  1. 有多张同名图片存在工程下, 比如都叫pic_same_test
  2. 同名图片有存在被工程引用的子Bundle中, 主Bundle中和xcasset中
  3. 同名图片未被工程引用进来, 但是放置在工程物理目录下的某个xcasset中

试想一下, 这个时候你如果使用下述代码去读取该图片, 会发生取到哪种图片呢?

UIImage *image = [UIImage imageNamed:@"pic_same_test"];

在上述的条件场景中, 当我在应用中用UIImage去读取A图片的时候, 总是会读取到了错误的B图片。因为笔者最初的排查方向只在条件1条件2两个方向去查找, 没有去深究未被工程引用的部分, 导致了整个思路方向被引向了错误的方向, 极大的加深了BUG的排查难度。

笔者在这个问题上纠结了很久, 在Stackoverflow苹果开发者论坛都根据这个场景进行了提问, 最终在开发者论坛中经过昵称为Bob133的高人指点, 将问题的<font color="orange">突破口</font>定位在了xcasset上。

神秘的错误图片

事情的起因是因为笔者在开发的某App的时候突然爆出了一个图片锯齿的BUG, 可是笔者的代码在线上已经稳定运行了几个月了, 怎么可能会突然抽风呢?

处于笔者对UIImage的了解, 第一反应想到的就是缓存。这里的缓存不是UIImage加载图片的加速缓存, 而是在打包时候的资源不重复copy的缓存。因此, 笔者对这个BUG的存在性持有怀疑的态度, 二话不说自己做起了实验, 执行了下述操作:

  1. 删除Project对应的Derived Data.
  2. 对项目执行clean操作
  3. 删除目标设备的项目App
  4. 重新打包编译整个App

经过上述四部操作和漫长的打包等待, 结果当然是呵呵哒了~ 如果结果正常就不会出现本篇博文了! 没错, 经过上述四部操作, 图片依旧还是错误的!

呵呵, 删除缓存不行, 那就不是缓存问题, 笔者怀疑打出来的包里面有图片串位的可能, 心想根目录下的图是不是就是错误的。不多说, 提取ipa, 显示包内容, 包内容根目录下的图竟然是正确的!!!

在包内容目录下, 我想要取的图片名字一样的图片总共就两张, 一张在根目录下, 另外一张在子Bundle下面。既然总共就2张图片, 那我就尝试在工程里删除掉子Bundle下的另外一张图片, 然后执行上述四部操作重新来过。结果大家想必还是知道的, 图片照样是错误的, 但是打包出来的文件包根目录下就只有一张正确的图片!

这个尼玛不是一张幽灵图片么? 笔者当时脑洞大开, 甚至怀疑到是否iCloud同步下来的, 可是笔者的测试机压根就没有绑定iCloud。

PS: 当时忽略了Assets.car是因为工程里引用的Image.assets里并没有这张同名的文件, 源文件没有, 那自然就不会怀疑打包后的内容。另外, 笔者比较懒, 懒得去提取car文件。

产生的原因&解决方案

针对这个幽灵图片, 笔者在XCode全局搜索, 也就搜索到前文提到的两者图片。那么这个图片究竟是从哪里来的呢?

笔者在这个问题上纠结了超过十个小时, 并分别在苹果开发论坛Stackoverflow提出的疑问, 但是疑问有误导回答者往Bundle排查的嫌疑。

但是世界上开发牛人这么多, 稍微误导下问题也不大, 在苹果开发者论坛中的用户bob133说他曾经遇到过类似的场景, 也排查了好久, 让我仔细检查下是不是xcasset捣的鬼。

笔者基于bob133的提示, 想到是否真的xcasset有问题。笔者通过XCode全局搜索了项目里的xcasset, 并没有找到错误的那张显示图片。直到这个时候, 笔者才想到要把加密的Assets.car文件提取出来看看。

ThemeEngine Demo

图片示例提取的是国内知名女性购物平台某某街的App, 可以从上图看出该App的图片使用也存在非常不规范的地方, 同一名字的图片被打入了这么多张。设想一下, 假如在这里写下述代码, 取到的究竟是上图中四张的哪张呢? =。=

UIImage *image = [UIImage imageNamed:@"address_icon_location"];

Assets.car的提取工具很多, 笔者使用的是ThemeEngine。通过ThemeEngine提取的Assets.car文件中<font color="orange">果然找到错误的图片</font>! 原来UIImage读取错误图片的根源是在这里啊!

总之, 打包后读取的问题图片已经找到了, 藏在二进制文件Assets.car中。

幽灵图片从哪里来

在打包生产的Assets.car竟然会出现错误的图片, 那一定还是工程目录下打包进去的。那究竟是什么地方打包进去的呢?

笔者首先想到的突破点是打包编译过程的Copy Pods Resources过程, 通过编译选项笔者发现有一个物理目录下的Example里的XXX.assets被打包进入了最终的Asset.car

笔者尝试删除该目录下的Example工程, 果然编译出来的App可以读取到了正确的图片。

问题根源已经找到了, 笔者查看Copy Pods Resouces下的核心脚本Pods_resources.sh, 发现一段很牛B的代码段:

# Find all other xcassets (this unfortunately includes those of path pods and other targets).
OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d)
while read line; do
if [[ $line != "`realpath $PODS_ROOT`*" ]]; then
    XCASSET_FILES+=("$line")
fi
done <<<"$OTHER_XCASSETS"

我去啊。。。怎么会有这样的代码段, 而且从0.35的CocoaPods版本开始就早已存在。笔者当时使用的0.39.stable的CocoaPods版本。关于这个问题, 笔者顺藤摸瓜, 找到了一个相关的CocoaPods issue - Pods copy resource script overrides default xcasset bahaviour

这个资源覆盖的issue截止笔者发文之前依旧还open着。笔者先回归正题, 为什么笔者的代码在线上跑了几个月后会突然出问题了呢? 关键代码在这里:

if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ]
...
fi

上述的Copy脚本执行条件是满足这个if语句, 这个条件语句有三个条件:

  1. 有WRAPPER_EXTENSION, pod库依赖的资源文件默认都是bundle
  2. xcode命令行支持actool, actool是用来合并xcasset的官方工具
  3. 有添加过任意一个xcasset相关的文件

条件1和条件2一直都没有改变过, 那么客观条件只有第3条有改变过的可能, 追朔代码:

install_resource()
{
  case $1 in
  ...
  *.xcassets)
      ABSOLUTE_XCASSET_FILE=$(realpath "${PODS_ROOT}/$1")
      XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE")
      ;;
  /*)
  ...
}

原来如此, 笔者所用的工程里依赖的Pod库里只要有任意一个Pod库被添加过一次xcasset文件, 则会触发这个全资源拷贝的脚本语句。这也是为啥之前工程没事, 好端端突然就出问题的原因。

防止大家误解, 这里条件3的添加xcasset需要通过引用库的podspec指定添加, 添加后通过主工程pod_installpod_update生产的脚本引入产生。

示例语句(写在podspec中):

s.resource = 'DemoLib/Pod/AnyName.xcassets'

总而言之, CocoaPods判断如果任意的Pod库里通过描述文件引入了xcasset文件, 就会触发根目录下所有xcasset文件扫描打包car的执行操作。

解决方案

针对该问题的解决方案有很多, 熟悉了CocoaPods的特性后怎么样都可以解决这个问题:

方法一: 删除所有物理目录下多余的xcasset, 本身在源代码根目录下放置没有用到库本身就是非常危险的行为。

方法二: 通过Podfile Hook去屏蔽Pod库资源的Copy和合成, 替换核心脚本, 定向指定自己需要Copy的资源。

方法三: 逃避的方法, 不要在Pod库中使用xcasset。本身CocoaPods的初衷并没有打算支持资源文件的, 后续演变成目前的形态。(不适用xcasset默认png压缩不会执行, 可能需要手动执行, 并且图片容易被提取)

追根溯源

作为一个极具盛名的开源库, 怎么可能会写这么大的一个BUG呢? 有因必有果, 有一个关键问题还是没有找出来, 为什么两年来没有人给这个问题提Pull Request呢?

笔者本着好奇之心去探索CocoaPods的相关issue和commit记录, 找到了一个关键提交节点:

0.36.4 (2015-04-16)

Bug Fixes

Fixes various problems with Pods that use xcasset bundles. Pods that use xcassets can now be used with the pod :path option.

Kyle Fuller #1549 #3384 #3358

该解决BUG对应的Merge issue是#3405

通过该关键节点引申出了一个BUG Fix的commit - Do not discard .xcassets from the main projectissue - Only include *.xcassets from Pods

从提交记录可以看出这两次提交分别是为了解决支持:path属性和打包xcasset时候遗漏了主工程的xcasset的问题。

原来这个暴力的拷贝脚本是用来<font color="orange">将主工程的xcasset和Pod的xcasset一起利用actool合成car用的</font>。因为主工程的xcasset命名不规律和文件存储位置的不规律, 和actool的特性有限。CocoaPods的研发者暂时也没有更好的办法, 所以采用这种暴力的方式!

<font color="orange">广大的网友如果有更好的方法, 可以帮助CocoaPods开发者解决该问题。笔者想了半天, 没有想出什么靠谱的方法。</font>

PS: 如果估计针对主工程的xcasset做标志位的话, 和直接利用hook去屏蔽一些对应的资源文件本质上是没有差距的, 因为都需要在主工程里做额外的操作。

总结

UIImage加载重名图片本身就存在问题, 因为图片不应该重名出现在工程里。但是, 在大型App开发中, 因为参与人员流动和数量的问题, 就不可避免的会出现各种各样的复杂情况。本文将笔者遇到的资源图片错误加载梳理了一下, 因为对CocoaPods和xcasset共同使用的不了解, 导致了排查的困难。

CocoaPods在Pod里引用了任意一个xcasset相关的文件后, 就会去根目录搜索所有的xcasset组合成为最终的car。CocoaPods设定这样脚本的原因是无法精确的将主工程下的xcasset寻找到, 只能采用暴力的方式去解决, 暂时也没有更好的解决方案!

PS: 本人技术水平有限, 如果有错误的地方, 请各位大大及时指出哈~~

参考

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

推荐阅读更多精彩内容