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

目录
x
  1. 神秘的错误图片
  2. 产生的原因&解决方案
    1. 幽灵图片从哪里来
    2. 解决方案
    3. 追根溯源
  • 总结
    1. 参考
  • 本篇文章来自笔者工作中遇到一个难解的BUG - 在App中用UIImageimageNamed:方法读取的图片始终是不正确的。

    暴走示意

    场景条件回放:

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

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

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

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

    笔者在这个问题上纠结了很久, 在Stackoverflow苹果开发者论坛都根据这个场景进行了提问, 最终在开发者论坛中经过昵称为Bob133的高人指点, 将问题的突破口定位在了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的图片使用也存在非常不规范的地方, 同一名字的图片被打入了这么多张。设想一下, 假如在这里写下述代码, 取到的究竟是上图中四张的哪张呢? =。=

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

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

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

    幽灵图片从哪里来

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

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

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

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

    1
    2
    3
    4
    5
    6
    7
    # 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着。笔者先回归正题, 为什么笔者的代码在线上跑了几个月后会突然出问题了呢? 关键代码在这里:

    1
    2
    3
    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条有改变过的可能, 追朔代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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中):

    1
    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的问题。

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

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

    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
    如果您觉得文章有用, 打赏一个呗~~ ( ⊙ o ⊙ )/
    本站总访问量 本站访客数人次