本篇文章來自筆者工作中遇到一個難解的BUG - 在App中用UIImage的imageNamed:方法讀取的圖片始終是不正確的。

場景條件回放:
- 有多張同名圖片存在工程下, 比如都叫
pic_same_test - 同名圖片有存在被工程引用的子Bundle中, 主Bundle中和xcasset中
- 同名圖片未被工程引用進來, 但是放置在工程物理目錄下的某個xcasset中
試想一下, 這個時候你如果使用下述代碼去讀取該圖片, 會發(fā)生取到哪種圖片呢?
UIImage *image = [UIImage imageNamed:@"pic_same_test"];
在上述的條件場景中, 當我在應用中用UIImage去讀取A圖片的時候, 總是會讀取到了錯誤的B圖片。因為筆者最初的排查方向只在條件1和條件2兩個方向去查找, 沒有去深究未被工程引用的部分, 導致了整個思路方向被引向了錯誤的方向, 極大的加深了BUG的排查難度。
筆者在這個問題上糾結了很久, 在Stackoverflow和蘋果開發(fā)者論壇都根據這個場景進行了提問, 最終在開發(fā)者論壇中經過昵稱為Bob133的高人指點, 將問題的<font color="orange">突破口</font>定位在了xcasset上。
神秘的錯誤圖片
事情的起因是因為筆者在開發(fā)的某App的時候突然爆出了一個圖片鋸齒的BUG, 可是筆者的代碼在線上已經穩(wěn)定運行了幾個月了, 怎么可能會突然抽風呢?
處于筆者對UIImage的了解, 第一反應想到的就是緩存。這里的緩存不是UIImage加載圖片的加速緩存, 而是在打包時候的資源不重復copy的緩存。因此, 筆者對這個BUG的存在性持有懷疑的態(tài)度, 二話不說自己做起了實驗, 執(zhí)行了下述操作:
- 刪除Project對應的Derived Data.
- 對項目執(zhí)行clean操作
- 刪除目標設備的項目App
- 重新打包編譯整個App
經過上述四部操作和漫長的打包等待, 結果當然是呵呵噠了~ 如果結果正常就不會出現(xiàn)本篇博文了! 沒錯, 經過上述四部操作, 圖片依舊還是錯誤的!
呵呵, 刪除緩存不行, 那就不是緩存問題, 筆者懷疑打出來的包里面有圖片串位的可能, 心想根目錄下的圖是不是就是錯誤的。不多說, 提取ipa, 顯示包內容, 包內容根目錄下的圖竟然是正確的!!!
在包內容目錄下, 我想要取的圖片名字一樣的圖片總共就兩張, 一張在根目錄下, 另外一張在子Bundle下面。既然總共就2張圖片, 那我就嘗試在工程里刪除掉子Bundle下的另外一張圖片, 然后執(zhí)行上述四部操作重新來過。結果大家想必還是知道的, 圖片照樣是錯誤的, 但是打包出來的文件包根目錄下就只有一張正確的圖片!
這個尼瑪不是一張幽靈圖片么? 筆者當時腦洞大開, 甚至懷疑到是否iCloud同步下來的, 可是筆者的測試機壓根就沒有綁定iCloud。
PS: 當時忽略了Assets.car是因為工程里引用的Image.assets里并沒有這張同名的文件, 源文件沒有, 那自然就不會懷疑打包后的內容。另外, 筆者比較懶, 懶得去提取car文件。
產生的原因&解決方案
針對這個幽靈圖片, 筆者在XCode全局搜索, 也就搜索到前文提到的兩者圖片。那么這個圖片究竟是從哪里來的呢?
筆者在這個問題上糾結了超過十個小時, 并分別在蘋果開發(fā)論壇和Stackoverflow提出的疑問, 但是疑問有誤導回答者往Bundle排查的嫌疑。
但是世界上開發(fā)牛人這么多, 稍微誤導下問題也不大, 在蘋果開發(fā)者論壇中的用戶bob133說他曾經遇到過類似的場景, 也排查了好久, 讓我仔細檢查下是不是xcasset搗的鬼。
筆者基于bob133的提示, 想到是否真的xcasset有問題。筆者通過XCode全局搜索了項目里的xcasset, 并沒有找到錯誤的那張顯示圖片。直到這個時候, 筆者才想到要把加密的Assets.car文件提取出來看看。

圖片示例提取的是國內知名女性購物平臺某某街的App, 可以從上圖看出該App的圖片使用也存在非常不規(guī)范的地方, 同一名字的圖片被打入了這么多張。設想一下, 假如在這里寫下述代碼, 取到的究竟是上圖中四張的哪張呢? =。=
UIImage *image = [UIImage imageNamed:@"address_icon_location"];
Assets.car的提取工具很多, 筆者使用的是ThemeEngine。通過ThemeEngine提取的Assets.car文件中<font color="orange">果然找到錯誤的圖片</font>! 原來UIImage讀取錯誤圖片的根源是在這里啊!
總之, 打包后讀取的問題圖片已經找到了, 藏在二進制文件Assets.car中。
幽靈圖片從哪里來
在打包生產的Assets.car竟然會出現(xiàn)錯誤的圖片, 那一定還是工程目錄下打包進去的。那究竟是什么地方打包進去的呢?
筆者首先想到的突破點是打包編譯過程的Copy Pods Resources過程, 通過編譯選項筆者發(fā)現(xiàn)有一個物理目錄下的Example里的XXX.assets被打包進入了最終的Asset.car。
筆者嘗試刪除該目錄下的Example工程, 果然編譯出來的App可以讀取到了正確的圖片。
問題根源已經找到了, 筆者查看Copy Pods Resouces下的核心腳本Pods_resources.sh, 發(fā)現(xiàn)一段很牛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截止筆者發(fā)文之前依舊還open著。筆者先回歸正題, 為什么筆者的代碼在線上跑了幾個月后會突然出問題了呢? 關鍵代碼在這里:
if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ]
...
fi
上述的Copy腳本執(zhí)行條件是滿足這個if語句, 這個條件語句有三個條件:
- 有WRAPPER_EXTENSION, pod庫依賴的資源文件默認都是
bundle - xcode命令行支持actool, actool是用來合并xcasset的官方工具
- 有添加過任意一個
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文件, 則會觸發(fā)這個全資源拷貝的腳本語句。這也是為啥之前工程沒事, 好端端突然就出問題的原因。
防止大家誤解, 這里條件3的添加xcasset需要通過引用庫的podspec指定添加, 添加后通過主工程pod_install或pod_update生產的腳本引入產生。
示例語句(寫在podspec中):
s.resource = 'DemoLib/Pod/AnyName.xcassets'
總而言之, CocoaPods判斷如果任意的Pod庫里通過描述文件引入了xcasset文件, 就會觸發(fā)根目錄下所有xcasset文件掃描打包car的執(zhí)行操作。
解決方案
針對該問題的解決方案有很多, 熟悉了CocoaPods的特性后怎么樣都可以解決這個問題:
方法一: 刪除所有物理目錄下多余的xcasset, 本身在源代碼根目錄下放置沒有用到庫本身就是非常危險的行為。
方法二: 通過Podfile Hook去屏蔽Pod庫資源的Copy和合成, 替換核心腳本, 定向指定自己需要Copy的資源。
方法三: 逃避的方法, 不要在Pod庫中使用xcasset。本身CocoaPods的初衷并沒有打算支持資源文件的, 后續(xù)演變成目前的形態(tài)。(不適用xcasset默認png壓縮不會執(zhí)行, 可能需要手動執(zhí)行, 并且圖片容易被提取)
追根溯源
作為一個極具盛名的開源庫, 怎么可能會寫這么大的一個BUG呢? 有因必有果, 有一個關鍵問題還是沒有找出來, 為什么兩年來沒有人給這個問題提Pull Request呢?
筆者本著好奇之心去探索CocoaPods的相關issue和commit記錄, 找到了一個關鍵提交節(jié)點:
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.
該解決BUG對應的Merge issue是#3405
通過該關鍵節(jié)點引申出了一個BUG Fix的commit - Do not discard .xcassets from the main project和issue - Only include *.xcassets from Pods。
從提交記錄可以看出這兩次提交分別是為了解決支持:path屬性和打包xcasset時候遺漏了主工程的xcasset的問題。
原來這個暴力的拷貝腳本是用來<font color="orange">將主工程的xcasset和Pod的xcasset一起利用actool合成car用的</font>。因為主工程的xcasset命名不規(guī)律和文件存儲位置的不規(guī)律, 和actool的特性有限。CocoaPods的研發(fā)者暫時也沒有更好的辦法, 所以采用這種暴力的方式!
<font color="orange">廣大的網友如果有更好的方法, 可以幫助CocoaPods開發(fā)者解決該問題。筆者想了半天, 沒有想出什么靠譜的方法。</font>
PS: 如果估計針對主工程的xcasset做標志位的話, 和直接利用hook去屏蔽一些對應的資源文件本質上是沒有差距的, 因為都需要在主工程里做額外的操作。
總結
UIImage加載重名圖片本身就存在問題, 因為圖片不應該重名出現(xiàn)在工程里。但是, 在大型App開發(fā)中, 因為參與人員流動和數量的問題, 就不可避免的會出現(xiàn)各種各樣的復雜情況。本文將筆者遇到的資源圖片錯誤加載梳理了一下, 因為對CocoaPods和xcasset共同使用的不了解, 導致了排查的困難。
CocoaPods在Pod里引用了任意一個xcasset相關的文件后, 就會去根目錄搜索所有的xcasset組合成為最終的car。CocoaPods設定這樣腳本的原因是無法精確的將主工程下的xcasset尋找到, 只能采用暴力的方式去解決, 暫時也沒有更好的解決方案!
PS: 本人技術水平有限, 如果有錯誤的地方, 請各位大大及時指出哈~~