寫這篇博客起因是由於上周四在知識小集發了一個 Tip :「 應用 icon 被 Cocoapods 「吃掉」的解決方式 」,講 Pod 裡面使用了 .xcassets 會導致 Xcode 9 打出來的包沒有 icon 的解決方案。然後和 @Damonwong 展開了一下討論。當然這條 Tip 講得不全對,而且表達得不是很清晰,所以這裡專門開一篇文章來講一下那個 Tip 想要表達的意思,最後也會附上對應的 Demo。也當做之後要寫的模塊化系列的文章開篇吧。
首先我們來給出結論:
下面我們先來了解一下這兩種方式的具體情況,以及各自的優劣勢,最後通過一個 Demo 來驗證我們上面給出的結論。
註:本文內容涉及的環境配置為:Xcode 9.2 、Cocoapods 1.4.0;文中描述的資源主要是指 png 格式的圖片。
resource 和 resource_bundleresource 和 resource_bundle 是 Pod 中引入資源的兩種方式,下面我們圍繞官方文檔做進一步解釋和對比,看看兩者之間的區別。
resource官方文檔對 resource 的描述如下:
A list of resources that should be copied into the target bundle.
這種方式會將引用的文件夾下的所有資源拷貝到 target 的 bundle 中去,可以簡單的理解為 .app 目錄下或者 .app 的 Assets.car 文件中(如果是 .xcassets 的資源)。
Tips: 注意一下上面加粗的部分。
我們來看一下官方給出的示例:
spec.resource = 'Resources/HockeySDK.bundle'
spec.resources = ['Images/\*.png', 'Sounds/\*']
官方文檔對 resource_bundle 的描述如下:
This attribute allows to define the name and the file of the resource bundles which should be built for the Pod. They are specified as a hash where the keys represent the name of the bundles and the values the file patterns that they should include.
這種方式可以將指定路徑下的資源打包到以 => 之前的 key 命名的 bundle 中,這個 bundle 最終會被拷貝到 target 也就是 .app 根目錄下。如果有指定 .xcassets 資源,會被打包到以 key 命名的 bundle 裡的 Assets.car 文件中。
Tips: 注意一下上面加粗的部分。
我們來看一下官方給出的示例:
spec.ios.resource_bundle = { 'MapBox' => 'MapView/Map/Resources/*.png' }
spec.resource_bundles = {
'MapBox' => ['MapView/Map/Resources/\*.png'],
'OtherResources' => ['MapView/Map/OtherResources/\*.png']
}
其實上面官網的描述我故意漏附了後面的幾段話,文檔上在兩種方式下都強烈(strongly)推薦使用 resource_bundle 的方式。
We strongly recommend library developers to adopt resource bundles as there can be name collisions using the resources attribute.
另外對 resource 還有如下描述:
Moreover, resources specified with this attribute are copied directly to the client target and therefore they are not optimised by Xcode.
而對 resource_bundle 還有如下描述:
The names of the bundles should at least include the name of the Pod to minimise the chance of name collisions.
綜合上述所說,就是使用 resource_bundle 主要有以下兩點好處:
我們來對比下兩種方式下最終打包出來的應用目錄結構:
可以看出 resource_bundle 的形式會生成對應的 bundle(上圖中的 Pod1.bundle),並且 .xcassets 最終會被打包到對應 bundle 下的 Assets.car 文件下(該文件可用這個工具打開:iOS-Images-Extractorhttps://github.com/devcxm/iOS-Images-Extractor )。而 resource 的形式,會把 .xcassets 打包到應用根目錄下的 Assets.car 中。
而要讀取對應的圖片時,resource 對應的代碼長下面這樣:
UIImage *image = [UIImage imageNamed:@"your-image-name"
inBundle:[NSBundle bundleForClass:[self class]]
compatibleWithTraitCollection:nil];
而 resource_bundle 對應的讀取代碼如下面所示:
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSURL *url = [bundle URLForResource:@"your-bundle-name" withExtension:@"bundle"];
NSBundle *targetBundle = [NSBundle bundleWithURL:url];
UIImage *image = [UIImage imageNamed:@"your-image-name"
inBundle:targetBundle
compatibleWithTraitCollection:nil];
通過以上兩個代碼片段,相信各位已經知道代碼片段 1、2 都可能存在圖片命名衝突的問題,但是一般情況下片段 2 的衝突概率遠小於片段 1 的。因為一般情況下,模塊內的圖片命名肯定是不會衝突的,而模塊間的圖片的命名就不好說了。當然如果團隊裡有明確的命名規範,片段 1 和片段 2 都不會有問題。
雖然片段 2 代碼較片段 1 稍複雜一些,但是如果我們將其封裝成一個 NSBundle 的分類,就免去了冗長的寫法,對於 Storyboard/Xib 同樣適用。
@interface NSBundle (Pod1Bundle)
+ (NSBundle *)pod1_bundle;
@end
@interface Pod1FakeClass : NSObject
@end
@implementation Pod1FakeClass
@end
@implementation NSBundle (Pod1Bundle)
+ (NSBundle *)pod1_bundle {
NSBundle *bundle = [self bundleForClass:[Pod1FakeClass class]];
NSURL *url = [bundle URLForResource:@"Pod1" withExtension:@"bundle"];
return [self bundleWithURL:url];
}
@end
UIImage *image = [UIImage imageNamed:@"your-image-name"
inBundle:[NSBundle pod1_bundle]
compatibleWithTraitCollection:nil];
最後我們通過一個 Demo 來驗證一下,具體代碼可以查看:https://github.com/wang9262/PodResourceDemo
第一個 commit 寫好了兩個 podspec,一個用 resource_bundle(Pod1),一個用 resource(Pod2),然後都引用 .xassets 資源,裡面都有一個名為 Pod 的圖片,主工程也有。區別在於 Pod 中的圖片頂部會有該 Pod 名稱的水印。然後頁面上有3個 ImageView,目前只設置了中間那個 ImageView 的圖片為主工程的圖片。運行起來,一切表現正常,展示出來的圖片也確實是主工程的圖片。
第二個 commit 在主工程內分別讀取 Pod1 和 Pod2 的名為 Pod 的圖片,然後分別塞到上下兩個 ImageView 中,運行起來,最下面那個 ImageView 的圖片變成主工程的圖片了,而最上面的 ImageView 的圖片是正常的!!!這就是我們上面說到的那個問題,由於 Pod2 使用 resource的方式,.xcassets 中圖片是直接和主工程的 .xcassets 中圖片一樣是打包到 .app 根目錄下的 Assets.car 中,命名一致,導致被主工程的圖片給覆蓋掉了(可以使用上面說到的解壓工具解壓,查看根目錄 .car 下的文件內容,只有主工程的圖片)。
第三個 commit 及 ruby-shell 分支主要解決應用 icon 為空的問題。當我們 Home 鍵回到桌面時,會發現 Demo 的 icon 是空的,但是我們是有設置 icon 的。所以我們有兩種解決方案來解決這個問題。
方案一:不使用 .xcassets把圖片放到 Pod2 目錄下,不再放到 .xcassets 裡面,然後 podspec 裡的寫法改成:
s.resources = ['Pod2/**/*.png']
pod install 之後再跑一下,發現 icon 回來了,但是圖片還是被主工程覆蓋了,查看 .app 文件,我們發現根目錄下多了兩張圖片,他們來自於 Pod2 目錄下。
相應的我們的讀取代碼就需要改成下面這種形式,還需要區分 @2x 和 @3x,非常麻煩,但是這個時候確實能讀到對應的圖片。
_podImage = [UIImage imageNamed:@"Pod@2x"
inBundle:[NSBundle bundleForClass:[self class]]
compatibleWithTraitCollection:nil];
_podImage = [UIImage imageNamed:@"Pod@3x"
inBundle:[NSBundle bundleForClass:[self class]]
compatibleWithTraitCollection:nil];
在 podfile 中加入下面這一段腳本
post_install do |installer|
copy_pods_resources_path = "Pods/Target Support Files/Pods-ResourceDemo_Example/Pods-ResourceDemo_Example-resources.sh"
string_to_replace = '--compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"'
assets_compile_with_app_icon_arguments = '--compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${BUILD_DIR}/assetcatalog_generated_info.plist"'
text = File.read(copy_pods_resources_path)
new_contents = text.gsub(string_to_replace, assets_compile_with_app_icon_arguments)
File.open(copy_pods_resources_path, "w") {|file| file.puts new_contents }
end
然後 pod install 之後,運行即可。
以上兩種方式執行之前最好 clean(cmd+option+shift+k) 一下,防止 Xcode 緩存,導致以上方法執行後也會出現 icon 消失的情況。
總結綜合以上,我們再總結一下 resource_bundle 的優勢:
至於劣勢,我覺得根本就沒有,上面那個硬編碼問題,完全可以通過我說的分類或者你自定義宏的方式把這個硬編碼問題 Cover 掉。
如果認為文章中的觀點或結論有問題,歡迎指出,一起溝通探討。
參考連結官方文檔 https://guides.cocoapods.org/syntax/podspec.html#resources
App Icons not included in build from Xcode 9 https://github.com/CocoaPods/CocoaPods/issues/7003
關於 Pod 庫的資源引用 resource_bundles or resources http://zhoulingyu.com/2018/02/02/pod-resource-reference/
覺得好,就打個賞吧
再關注一下嘍