在Pod庫中使用xcasset的拷貝陷阱

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

暴走示意

場景條件回放:

  1. 有多張同名圖片存在工程下, 比如都叫pic_same_test
  2. 同名圖片有存在被工程引用的子Bundle中, 主Bundle中和xcasset中
  3. 同名圖片未被工程引用進來, 但是放置在工程物理目錄下的某個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í)行了下述操作:

  1. 刪除Project對應的Derived Data.
  2. 對項目執(zhí)行clean操作
  3. 刪除目標設備的項目App
  4. 重新打包編譯整個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文件提取出來看看。

ThemeEngine Demo

圖片示例提取的是國內知名女性購物平臺某某街的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語句, 這個條件語句有三個條件:

  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文件, 則會觸發(fā)這個全資源拷貝的腳本語句。這也是為啥之前工程沒事, 好端端突然就出問題的原因。

防止大家誤解, 這里條件3的添加xcasset需要通過引用庫的podspec指定添加, 添加后通過主工程pod_installpod_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.

Kyle Fuller #1549 #3384 #3358

該解決BUG對應的Merge issue是#3405

通過該關鍵節(jié)點引申出了一個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命名不規(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: 本人技術水平有限, 如果有錯誤的地方, 請各位大大及時指出哈~~

參考

  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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容