主流的依賴管理有三大開源庫:最老牌的 CocoaPods, 新秀 Carthage, 官方的 Swift Package Manager(目前只支持 macOS允青,不予討論)玛迄。
讓我們的庫支持這兩種依賴管理方式需要特定的工程形式嗎?比如一個動態(tài)庫:
這不是必需的宁舰,CocoaPods 和 Carthage 有自己的規(guī)則去獲取源代碼文件和資源文件拼卵,并不依賴這種特殊的封裝,對 CocoaPoda 而言蛮艰,甚至不需要庫以工程的形式存在腋腮,而對 Carthage 來說,庫的代碼和資源文件必須通過工程的形式來獲取壤蚜。
CocoaPods
CocoaPods 是最早出現(xiàn)的即寡,也是目前影響力最大的依賴管理庫。CocoaPod 的官方教程:Making a CocoaPod袜刷。
面對一堆代碼和資源文件聪富,需要三個步驟讓別人通過 pod 安裝:
- 創(chuàng)建 .podspec 配置文件(spec 是 specification 的縮寫):庫名,版本水泉,托管地址善涨,源碼和資源在托管地址的相對位置;
- 托管到網(wǎng)絡(luò)上草则,比如 Git钢拧;
- 發(fā)布到 pod 的 trunk 服務(wù)器
第2步結(jié)束后,別人就可以通過 pod 來使用你的庫炕横,而實現(xiàn)了第3步后源内,可以實現(xiàn)最簡單的安裝方式:pod 'libraryName'
。
在托管到網(wǎng)絡(luò)之前份殿,我們需要測試下庫是否能夠正常接入和使用膜钓;發(fā)布后,更新版本前需要進行測試卿嘲,所以支持本地使用是很重要的颂斜,pod 支持。
本地開發(fā)階段
使用 pod 接入第三方庫是通過 Podfile 文件配置拾枣,而讓你的庫支持 pod 則是通過 libraryName.podspec 文件進行配置沃疮,這兩個配置文件不要用 TextEdit 編輯盒让,最好使用 Xcode 來編輯。
在開發(fā)階段的通常做法是:創(chuàng)建一個文件夾作為庫的根目錄司蔬,將庫源碼單獨放在一個子文件夾里邑茄,另建一個子文件夾作為 demo 的目錄;在根目錄下添加 libraryName.podspec 文件(可以通過這個命令創(chuàng)建:pod spec create libraryName
)俊啼,在 demo 目錄下添加一個 Podfile肺缕。pod 為此提供了一個模版:pod lib create libraryName
,它創(chuàng)建了一個完整的占位工程以及配置文件授帕,如果是從頭開始新的項目同木,可以用這個命令省去很多工作,如下所示跛十,我通過這個命令創(chuàng)建了一個SDEDownloader
測試庫泉手,這個命令會有一些交互式的配置選項,右側(cè)為最終的目錄結(jié)構(gòu):
需要接入這個本地的庫時偶器,在 demo 目錄下通過pod install
安裝 Podfile 中指定的本地庫和其它庫,Podfile 里的配置如下:
pod 'libraryName', :path => 'podspecFolderPath'
podspecFolderPath
以 Podfile 所在目錄為基礎(chǔ)路徑缝裤,以上面的測試庫為例的話屏轰,這個值為../
(也就是這個庫的根目錄),而且這個指定的路徑下必須有 libraryName.podspec 文件憋飞,以及一個LICENSE霎苗。
建議使用上面提到的兩個命令來創(chuàng)建 .podspce 文件,很多必須的配置項都有了榛做,詳細的配置項可參考:Podspec Syntax Reference唁盏。其中的關(guān)鍵必需配置項如下:
// .podspec 文件本身的文件名也必須是 libraryName
s.name = 'libraryName'
// 當(dāng)前庫的版本,使用本地庫時該值被忽略
s.version = '0.1.0'
// 庫的主頁检眯,使用本地庫時厘擂,這個值被忽略
s.homepage = 'https://github.com/seedante/SDEDownloader'
// 庫的托管地址,使用本地庫時锰瘸,這個值被忽略刽严。
// 這里使用了 git,還支持 svn, http, hg避凝。
s.source = { :git => 'https://github.com/seedante/libraryName.git', :tag => s.version.to_s }
// 源代碼文件舞萄,多個值之間使用,分割
s.source_files = 'libraryName/Classes/**/*'
// 排除文件
s.exclude_files = 'Tests'
// 資源文件,還有一個可替代配置 resource_bundles管削,該如何選擇呢倒脓?后面來討論
s.resources = ['Assets/*.xcassets', 'LocalizableFiles/**/*.strings']
podspec 的語法比較寬松,比如上面的值含思,你會看到有的庫里使用''
崎弃,有的使用""
甘晤,無所謂,選個你喜歡的就行吊履;有多個值時可以放在[]
內(nèi)安皱,也可以不用。
pod 根據(jù) .podspec 文件里source
提供的庫地址結(jié)合source_files
, exclude_files
, resources
這幾個值(除了source_files
艇炎,剩下兩個是可選的)去獲取文件酌伊,這幾個值都是使用相對位置,以 libraryName.podspec 所在目錄為基礎(chǔ)路徑缀踪。在使用本地庫時居砖,source 值被忽略,直接抓取podspecFolderPath
目錄下驴娃,通過source_files
, exclude_files
, resources
這幾個值指定的源文件奏候。在這一階段,可以檢查獲取的源文件是否符合預(yù)期唇敞,如何利用通配符來指定源文件可參考:File patterns蔗草。
使用本地庫時,代碼文件會按照物理目錄去整理疆柔,而使用基于網(wǎng)絡(luò)托管的庫時咒精,代碼文件不再按照物理目錄那樣去整理,而是統(tǒng)一在一個目錄下旷档;對于使用resources
指定的資源文件模叙,都被會集中放在名為 Resources 的邏輯文件夾下。在工程里鞋屈,通過 pod 安裝的庫都被集中在 Pods 這個工程下范咨,其中安裝的本地庫在Development Pods
這個目錄下,其它庫在Pods
目錄下厂庇。
托管到網(wǎng)絡(luò)
代碼可以發(fā)布后渠啊,將其托管到網(wǎng)絡(luò)上,比如 Git权旷,為了盡可能減少錯誤昭抒,發(fā)布之前可以先進行測試,在上一個階段炼杖,可以測試是否按照預(yù)期的那樣獲取源文件灭返,現(xiàn)在可以利用pod spec lint
來過濾掉一些簡單的無意義的默認值,在 .podspec 文件所在目錄也就是庫的根目錄下運行測試命令坤邪。
下面這些值是使用pod lib create SDEDownloader
創(chuàng)建的 .podspec 的默認值:
s.version = '0.1.0'
s.summary = 'A short description of SDEDownloader.'
s.home = 'https://github.com/seedante/SDEDownloader'
s.source = { :git => 'https://github.com/seedante/SDEDownloader.git', :tag => s.version.to_s }
運行pod spec lint
后會出現(xiàn)1個錯誤和2個警告:錯誤是無法訪問source
指定的地址熙含,因為我們還沒有將代碼發(fā)布到 Github 上;其中一個警告是要求summary
修改掉默認值艇纺,寫點有意義的怎静,另外一個警告是home
指定的地址無法訪問邮弹,理由和source
相同,這里可以填一個可以訪問的地址就可以消除警告蚓聘,當(dāng)然還是保留這個有意義的地址最好腌乡。
這里home
和source
里相關(guān)的地址就是我們推送到 Git 后的地址,如果你是自己寫夜牡,這些地址也是很好推斷出來的与纽。如果沒有其它問題了,可以發(fā)布代碼了塘装,這里需要注意的是記得添加 tag 來定制版本號急迂,需要與version
值匹配:
git tag '0.1.0'
git push --tags
推送到 Github 后最好再次運行測試命令pod spec lint
來進行檢查并修改,熟悉流程以后這些值都可以提前填好蹦肴。
現(xiàn)在別人就可以通過 pod 來使用這個庫了僚碎,直接指定庫的地址,語法如下:
pod 'SDEDownloader', :git => 'https://github.com/seedante/SDEDownloader.git', :tag => '0.1.0'
不指定 tag 的話阴幌,使用 repo 下 master branch 最新 commit 里的 .podspec 獲取源文件和資源勺阐;指定 tag 后,則根據(jù)指定版本的 .podspec 獲取源文件和資源矛双。
這種直接指定庫地址的使用方式里皆看,.podspec 里的source
值依然被忽略了,你填個其它地址也沒有問題背零。
關(guān)于 .podspec 文件在代碼庫中的位置,前面提到將其放到根目錄下无埃,這是 pod 在File patterns 里硬性規(guī)定的:
Podspecs should be located at the root of the repository, and paths to files should be specified relative to the root of the repository as well.
畢竟不放在根目錄下的話就太麻煩了徙瓶,這個在創(chuàng)建 .podspec 文件時文檔就應(yīng)該指出的,我當(dāng)時想把這個文件挪到其它位置嫉称,花了差不多一天時間才試驗出來侦镇,結(jié)果試驗完了后才看到這條規(guī)定,唉织阅,總是會發(fā)生這種事情......
發(fā)布到 Trunk 服務(wù)器
不發(fā)到 Trunk 服務(wù)器也可以像上一階段那樣通過指定具體的庫地址來使用壳繁,不過為什么還要發(fā)布呢?我也說不上來荔棉,可以看看官方的解釋:CocoaPods Trunk闹炉。
完成這最后一步非常簡單恬汁,前提是這個名字還沒有被其他人搶注:
pod trunk push SDEDownloader.podspec
如果你沒有在 pod 注冊過侣监,運行上面這行命令后會得到提示的,按照提示做即可蟋字。.podspec 里的source
值應(yīng)該是在這里起作用壹若,這個庫名就和庫的地址進行了綁定嗅钻。
發(fā)布到 Trunk 服務(wù)器后皂冰,不必指定庫的地址就可以使用:
pod 'SDEDownloader'
Carthage
CocoaPods 會改變工程結(jié)構(gòu),將第三方庫與當(dāng)前的工程納入同一個 workspace 里养篓,而我們其實僅僅需要的是封裝好的庫秃流,Carthage 做的就是這件事,這樣讓通過 Carthage 使用第三方庫的時候比較麻煩柳弄,但是讓你的庫支持 Carthage 無比簡單舶胀,不需要配置文件,只需要將需要共享的源碼和資源所在的 scheme 標(biāo)記為Shared
就可以了:
在 Cartfile 里指定第三方庫的語法是這樣的:
github "seedante/SDEDownloader"
它會到 Github 的這個 repo 根目錄下的 .xcworkspace 或者 .xcodeproj 里尋找 shared 的 scheme 里獲取源文件和資源语御,如何確保文件在這個 scheme 里呢峻贮,看文件的歸屬,添加到這個工程的文件基本上是了:
這個支持過程太簡單了应闯,我當(dāng)初都沒有進行過本地測試纤控,Carthage 當(dāng)然也支持使用本地的庫:
// Use a local project
git "file:///directory/to/project"
不過有幾個限制條件:
- 必須納入 git 管理;
- 指定路徑是 .xcworkspace 或者 .xcodeproj 所在目錄的絕對路徑碉纺;
- 在這種沒有附加任何條件的情況下船万,庫必須用 tag 來劃分版本,即使上面的語法里沒有指定版本骨田,而上面的語法將使用最新版本號下的版本耿导,如果你提交了一個新的 commit,但是卻沒有給這個 commit 添加 tag态贤,上面的語法仍然使用最近的一個 tag 指定的版本而不是最新的 commit舱呻。
關(guān)于版本的指定,Carthage 還支持 branch 和 commit id悠汽,使用非常簡單:
// 將獲取這個 branch 下最新的 commit 的版本
git "file:///directory/to/project", "branchName"
// 將獲取指定的 commit 的版本
git "file:///directory/to/project", "commit_id"
加上 tag箱吕,一共三種方法來指定具體的版本,這三種方法不能混合使用柿冲。
Carthage 支持多種形式的庫地址茬高,詳細可以看 example-cartfile。
讓你的工程同時支持 CocoaPods 和 Carthage 并沒有什么沖突假抄,比較麻煩點的地方是Carthage 要在 repo 的根目錄下尋找 .xcworkspace 或者 .xcodeproj 文件怎栽,而 CocoaPods 只需要有 .podspec 文件就可以了,而你通過pod lib create SDEDownloader
創(chuàng)建庫文件夾時這兩種文件是在子目錄下的宿饱,把它們移動到根目錄還是有點麻煩的熏瞄。
對圖像文件的支持
在資源文件中,圖像文件有點特殊谬以,比如為了應(yīng)對不同的設(shè)備需要準備多種分辨率的版本巴刻。為了更好地管理資源文件,Xcode 引入了 Asset Catalog蛉签,使用它來管理圖像文件有如下優(yōu)點:
- 在更方便的界面里管理適應(yīng)不同設(shè)備的圖像文件沥寥,比在 Xcode Navigationer 里維護多個版本的文件要省心;
- 可以直接在控制面板里設(shè)置相關(guān)屬性邑雅,不必再去代碼里設(shè)置。
- 官方的 App 瘦身技術(shù)需要使用 Asset Catalog淮野。
在 Xcode 里 Asset Catalog 以 .xcassets 的格式存在吹泡,在進行編譯后,主工程和其它庫里所有的 .xcassets 文件各自都集中成了一個單獨的文件 Assets.car爆哑,那么庫里的 Assets.car 會和主工程下的 Assets.car 沖突嗎,會發(fā)生覆蓋的情況嗎队贱?
沒有放在 .xcassets 文件里的圖像文件在編譯后則依然以原始的形式存在,類似的問題來了柱嫌,庫和主工程會發(fā)生同名文件的沖突嗎屯换?
pod 有兩個屬性用于指定資源文件编丘,分別是resources
和resource_bundles
,后者是為了避免命名沖突設(shè)計的彤悔,它引入了命名空間的概念嘉抓,我們可以將資源像使用字典分類。官方強烈建議使用resource_bundles
來打包資源蜗巧,但并沒有注明原因以及適用范圍。
之前搜到了這篇2015年的文章《給 Pod 添加資源文件》蕾盯,pod 以往似乎直接將資源文件放主工程里幕屹,也就是 app 的根目錄下,這樣第三方庫里的資源文件可能與主工程里的資源文件發(fā)生命名沖突级遭,使用resource_bundles
來解決這個問題,它會將資源文件打包成這樣的文件:你指定的文件名.bundle挫鸽,這樣基本可以解決命名沖突了。
而我摸索的結(jié)果表明這樣的手法完全沒有必要丢郊,不過《給 Pod 添加資源文件》這篇文章作為前期的參考在我寫這部分內(nèi)容時給忘了医咨,直到昨天微信公眾號「知識小集」推送了一篇文章《 Pod 中資源引入方式對比》,里面運用了resource_bundles
來解決類似的問題架诞,想起來跟我這篇文章后面的結(jié)論相反,于是我下載了文章里的 Demo 進行了一番測試谴忧。鼓搗了一番后發(fā)現(xiàn):雙方都沒有錯很泊,但是我們都只考慮了一半,癥結(jié)在于我們以不同的方式編譯庫沾谓。于是我重寫了這部分委造。
從 iOS 8 和 Xcode 6 開始引入了 Cocoa Touch Framework,也就是我們常說的動態(tài)庫均驶,它和以往的靜態(tài)庫 Cocoa Touch Static Library 有什么區(qū)別呢昏兆,這又和這篇文章有什么聯(lián)系呢?
簡單來說辣恋,編譯后的靜態(tài)庫不包含資源文件亮垫,它的資源文件都移動到了 app 的根目錄里,所以在 pod 里需要resource_bundles
這種解決方案:庫里所有的 .xcassets 文件集中成一個文件 Assets.car伟骨,以及其它以原始形式存在的圖像文件都以"你指定的文件名.bundle"這樣的形式存在饮潦,放在 main bundle(app 根目錄下);如果不使用這樣的方法封裝携狭,而是resource
继蜡,庫里的 Assets.car 不會拷貝到 app 的根目錄里,而其它以原始形式的圖像文件會被拷貝逛腿,如果主工程下有同名的文件稀并,庫的同名文件會覆蓋這些文件。
動態(tài)庫處理 .xcassets 文件和原始形式的圖像文件的方式和靜態(tài)庫一樣单默,只不過動態(tài)庫可以包含資源文件碘举,也就是說主工程和動態(tài)庫獨立地存放和管理各自的資源文件,不會發(fā)生沖突搁廓,所以resource_bundles
這種解決方法就不需要了引颈。
在文件形式上,靜態(tài)庫是 xxx.a 這樣的格式境蜕,無法在 Finder 里查看蝙场;動態(tài)庫是 xxx.framework 這樣的格式,可以在 Finder 里查看它的內(nèi)容粱年。
Xcode 直到9才支持包含 Swift 代碼的靜態(tài)庫售滤,由于之前我的探索是基于動態(tài)庫,而上面提到的兩種文章里都使用的是靜態(tài)庫,我們雙方都只探討了一半內(nèi)容完箩,現(xiàn)在把兩種情況綜合一下:
Carthage 目前只支持動態(tài)庫赐俗,當(dāng)然它也能將庫編譯為靜態(tài)庫秃励,但如果庫里有資源文件币励,由于在其網(wǎng)頁里沒明確提及這方面的事情,我還不知道怎么處理(后續(xù)有空的話補上這部分內(nèi)容)仅胞;在 pod 里編譯動態(tài)庫需要在 Podfile 里添use_frameworks!
椎眯,如果沒有這句乳丰,則編譯為靜態(tài)庫。
在 pod 里拂酣,使用動態(tài)庫的話埃撵,一切都很簡單,使用resouces
打包資源即可虽另;使用靜態(tài)庫時暂刘,如果庫里使用了 .xcassets,則必須使用resource_bundles
捂刺,不然庫中 .xcassets 里的圖像都無法使用谣拣,而以原始形式存在的圖像文件,考慮到會與主工程下的文件發(fā)生命名沖突族展,推薦使用resource_bundles
森缠。
訪問使用resource_bundles
打包的資源會麻煩一點贵涵,而且使用resource_bundles
還需要考慮不同庫之間的 bundle 名沖突,建議盡量使用動態(tài)庫來避免這種麻煩恰画。接下來使用例子來講解resources
和resource_bundles
兩種方案的使用和區(qū)別宾茂。
CocoaPods: resources, or resource_bundles?
resource_bundles
的語法如下,和resources
一樣有復(fù)數(shù)形式拴还,其實也沒那么嚴格跨晴,之前一直沒注意,下面是官方的例子自沧,我加了點使用 Asset Catalog 管理的文件:
spec.resource = 'Resources/HockeySDK.bundle'
spec.resources = ['Images/*.png', 'Sounds/*', 'Assets/*.xcassets']
spec.ios.resource_bundle = { 'MapBox' => 'MapView/Map/Resources/*.png' }
spec.resource_bundles = {
'MapBox' => ['MapView/Map/Resources/*.png', 'Assets/*.xcassets'],
'OtherResources' => ['MapView/Map/OtherResources/*.png', 'Assets/*.xcassets']
}
對于resources
坟奥,使用靜態(tài)庫時,資源文件直接拷貝到 app 的根目錄下拇厢;使用動態(tài)庫時爱谁,則放在庫文件 LibraryName.framework 的根目錄下。
對于resource_bundles
孝偎,資源文件時文件被以"MapBox.bundle"和"'OtherResources.bundle"這樣的形式封裝访敌,編譯靜態(tài)庫時,這兩個文件存放在 app 的根目錄下衣盾;使用動態(tài)庫時寺旺,這兩個文件放在庫文件LibraryName.framework 的根目錄下。
可以這樣查看這些內(nèi)容在動態(tài)庫里是如何組織的:在 Pods 工程下(在Xcode里看) Products 目錄下找到 LibraryName.framework势决,右鍵菜單中選擇"Show in Finder"阻塑,在 Finder 里點擊打開。
pod 對resources
打包后的結(jié)構(gòu):
LibraryName.framework
--xxxx
--*.png
--Assets.car//所有使用 Asset Catalog 的圖像文件都集中成了這一個文件
使用resource_bundles
打包的結(jié)構(gòu):
LibraryName.framework
--xxxx
--*.png
--MapBox.bundle(other.file/*.png/Assets.car)//MapBox指定的所有 .xcassets 文件也集中成了一個文件
--OtherResources.bundle(other.file/*.png/Assets.car)
如何讀取這些圖像呢果复?
UIImage 的方法init?(named: String, in: Bundle?, compatibleWith: UITraitCollection?)
可以指定具體的 bundle(其實就是一個文件夾陈莽,相對地每個庫也可以視作一個 bundle),init?(named: String)
是這個方法的便捷形式,它在 main bundle (也就是主工程里走搁,app 的根目錄)尋找圖像独柑,這兩個方法優(yōu)先在 bundle 里的 Assets.car 里尋找,找不到后再在 bundle 根目錄下里查找私植。
使用resources
打包時忌栅,庫內(nèi)外的代碼這樣訪問庫里的圖像文件:
// 找到 frameworkBundle 的所在位置
let frameworkBundle = Bundle(for: classInLibrary.self)
// init?(named:in:compatibleWith:) 這個方法會優(yōu)先在 frameworkBundle 里面的 Assets.car 里查找,
// 如果沒有找到再在 frameworkBundle 的根目錄下查找曲稼,找到后會緩存起來索绪。
let image = UIImage(named: "imageName", in: frameworkBundle, compatibleWith: nil)
而使用resource_bundles
打包的文件額外打包了一層,無論在庫內(nèi)部還是外部贫悄,使用它們需要多一次解包:
// 在 frameworkBunlde 內(nèi)部的位置者春,withExtension 參數(shù)使用'.bundle'也可以
let mapBoxBundle = Bundle.init(url: frameworkBundle.url(forResource: "MapBox", withExtension: "bundle")!)
let image = UIImage(named: "imageName", in: mapBoxBundle, compatibleWith: nil)
在開發(fā) SDEDownloadManager 這個庫時,我將庫源碼單獨用 Cocoa Touch Framework 打包了清女,在庫的內(nèi)部使用圖像文件只需要一次解包钱烟,而使用resource_bundles
的話,通過 pod 安裝的庫則需要多一次解包才能使用內(nèi)部的資源嫡丙,這就造成了開發(fā)代碼和發(fā)布代碼不一致拴袭。所以總的來講,使用動態(tài)庫的時候曙博,沒有使用resource_bundles
的必要拥刻。
這里還有一點比較有趣,如果使用靜態(tài)庫的話父泳,上面的 frameworkBundle 指向的路徑和mainBundle
是一樣的般哼;而動態(tài)庫里,frameworkBundle 指向庫文件的路徑惠窄。
One More Thing
使用 Asset Catalog 有諸多優(yōu)點蒸眠,比如init?(named:in:compatibleWith:)
會緩存圖像,有時候圖像只需要使用一次杆融,這時候需要使用init?(contentsOfFile: String)
楞卡,這個方法不會緩存數(shù)據(jù),每次都會從指定路徑(.framework 以及 .bundle 里文件的路徑可以利用Bundle
這個類獲取)加載圖像脾歇,但這個方法無法對使用 Asset Catalog 的圖像使用蒋腮,因為 Assets.car 是個不透明的文件格式,無法獲取里面的圖像文件的路徑藕各。所以池摧,使用init?(contentsOfFile: String)
獲取圖像時,這個圖像文件不要放在 xcassets 里激况。