這篇文章主要想介紹以下幾個部分:
- subspecs基本概念
- 實際應用的好處
- 如何對含有subspecs的CocoaPods庫進行二進制化
什么是CocoaPods的subspecs
來一個直觀點的。順便為自己的YTXAnimations做個廣告。
在Podfile中,它是這樣的:
pod 'YTXAnimations', '~> 1.2.4', :subspecs => ["AnimateCSS", "Transformer"]
在App中Pods/YTXAnimations文件目錄下它是這樣的:

在CocoaPods項目開發(fā)時是這樣的:

在podspec里是這樣的:
YTXAnimateCSS = { :spec_name => "AnimateCSS" }
YTXCSShake = { :spec_name => "CSShake" }
YTXMagicCSS = { :spec_name => "MagicCSS" }
$animations = [YTXAnimateCSS, YTXCSShake, YTXMagicCSS]
$animations.each do |sync_spec|
s.subspec sync_spec[:spec_name] do |ss|
specname = sync_spec[:spec_name]
sources = ["Pod/Classes/UIView+YTX#{specname}.*", "Pod/Classes/YTXAnimationsUtil.{h,m}"]
ss.source_files = sources
if sync_spec[:dependency]
sync_spec[:dependency].each do |dep|
ss.dependency dep[:name], dep[:version]
end
end
end
end
s.subspec "Transformer" do |ss|
ss.source_files = ["Pod/Classes/YTXGooeyCircleLayer.{h,m}", "Pod/Classes/YTXCountDownShowLayer.{h,m}"]
end
==在一個podspec里我可以定義它的subspecs,給使用方提供了一種靈活的方式去獲取相關源碼,而不是全部源碼。subspec之間也可以有依賴關系,依賴其他第三方庫等。==
如果是這樣的用的話,就是全量。
pod 'YTXAnimations', '~> 1.2.4'
也有不少在github第三方庫用了subspecs。比如:ARAnalytics。
談談作用:subspecs這種模式特別適合組件化開發(fā)。
比如有兩個業(yè)務team A和B。他們各自維護一個業(yè)務組件A和業(yè)務組件B。原則上業(yè)務組件A和業(yè)務組件B之間不能相互依賴。但是很多時候組件A需要調(diào)用組件B的功能,接受組件B的回調(diào)。架構的時候我們會使用依賴協(xié)議或者依賴下沉等等方式去除他們之間的耦合。
但問題是我們還是需要集成在一起調(diào)試的。
一般做法就是在業(yè)務組件A的Example項目的Podfile中,加上依賴業(yè)務組件B:
target 'TestBusinessAExampleApp' do
pod 'BusinessA', :path => "../"
pod 'BusinessB', '~>1.2.1'
end
然后在Example App中串聯(lián)起A和B,以達到調(diào)試的目的。
組件B作為一個業(yè)務肯定是很龐大的,所以編譯慢。二進制化可以解決這個問題。作為Team A的人,我不需要關注組件B是否太大編譯慢,依賴等問題。
舉個例子,比如外賣和電影,外賣會送電影票。
很容易想到!業(yè)務組件A只依賴業(yè)務組件B的部分。組件B應該把這部分其實對外的內(nèi)容盡量做成一個subspec或者正常結構劃分,劃分成依賴其中幾個subspec。這樣業(yè)務組件A需要關心的事就更少了。當發(fā)生問題,Team A不得已想要查看業(yè)務組件B的源代碼以查看是否問題出在了業(yè)務組件B的時候,Team A的人員面對的不是整個業(yè)務組件B的業(yè)務源代碼,而是部分其實對外的源代碼??s小依賴的二進制文件大小或源代碼數(shù)量也是有顯而易見的好處的。
嗯,調(diào)試源代碼,我們應該:
- 刪除Pods目錄
- pod cache clean --all
- IS_SOURCE=1 pod install
嗯,這已經(jīng)在公司內(nèi)部達成了一致,使用IS_SOURCE。
現(xiàn)在的業(yè)務組件A的Example項目的Podfile中應該變成了這樣:
target 'TestBusinessAExampleApp' do
pod 'BusinessA', :path => "../"
pod 'BusinessB', '~>1.2.1' , :subspecs => ["SomeBusinessXXX"]
end
進一步的如果有個業(yè)務組件C要和業(yè)務組件B打交道,它的Example項目的Podfile應該這樣寫:
target 'TestBusinessCExampleApp' do
pod 'BusinessC', :path => "../"
pod 'BusinessB', '~>1.2.1' , :subspecs => ["SomeBusinessTTT"]
end
在主項目App中的Podfile是這樣寫:
target 'DaMeiTuanApp' do
pod 'BusinessA', '~>3.0.5'
pod 'BusinessB', '~>1.2.1'
pod 'BusinessC', '~>2.2.0'
end
下面開始說說subspec如何二進制化,如何在podspec中定義
==如果沒有特別說明,沒有講到細節(jié)的內(nèi)容或方式都應該在教程一里==
在教程一里面提到有subspecs的CocoaPods的組件二進制化方案,說了兩個方案。最后選擇的方案:是對每一個subspec都做份二進制并保持它們之間依賴的相互關系。
為什么不使用全集?也就是把所有源碼都變成.a呢?
- 使用方的Podfile就需要改寫了。
- 使用方本來希望只用AnimateCSS和Transformer,現(xiàn)在不得不把CSShake和MagicCSS也包含進來了。
接下來以實際項目YTXUtilCategory作為例子來講解。
方案就是:是對每一個subspec都做份二進制并保持它們之間依賴的相互關系。
YTXUtilCategory是我們的一個提供公共通用方法和Category的類。在二進制化之前它大概是長這個樣子的:

二進制化之前它的podspec是這樣的:
Pod::Spec.new do |s|
.......
_all_names = []
_GTMBase64 = { :spec_name => "GTMBase64", :source_files => ['Pod/Classes/GTMBase64/GTM*.{h,m}' ] }
_UIColor = { :spec_name => "UIColor", :source_files => ['Pod/Classes/UIColor/UIColor+*.{h,m}' ] }
_UIView = { :spec_name => "UIView", :source_files => ['Pod/Classes/UIView/UIView+*.{h,m}' ] }
_UIImage = { :spec_name => "UIImage", :source_files => ['Pod/Classes/UIImage/UIImage+*.{h,m}' ] }
_UIDevice = { :spec_name => "UIDevice", :source_files => ['Pod/Classes/UIDevice/UIDevice+*.{h,m}' ] }
_UITableView = { :spec_name => "UITableView", :source_files => ['Pod/Classes/UITableView/UITableView+*.{h,m}' ] }
_UIViewController = { :spec_name => "UIViewController", :source_files => ['Pod/Classes/UIViewController/UIViewController+*.{h,m}' ] }
_UIButton = { :spec_name => "UIButton", :source_files => ['Pod/Classes/UIButton/UIButton+*.{h,m}' ] }
_NSURL = { :spec_name => "NSURL", :source_files => ['Pod/Classes/NSUR/NSURL+*.{h,m}' ] }
_NSArray = { :spec_name => "NSArray", :source_files => ['Pod/Classes/NSArray/NSArray+*.{h,m}' ] }
_NSDictionary = { :spec_name => "NSDictionary", :source_files => ['Pod/Classes/NSDictionary/NSDictionary+*.{h,m}' ] }
_NSDate = { :spec_name => "NSDate", :source_files => ['Pod/Classes/NSDate/NSDate+*.{h,m}' ] , :dependency => [{:name => "DateTools", :version => "~> 1.0" }] }
_NSString = { :spec_name => "NSString", :source_files => ['Pod/Classes/NSString/NSString+*.{h,m}' ],
:sub_dependency => [_GTMBase64] }
_Util = { :spec_name => "Util", :source_files => ['Pod/Classes/Util/*.{h,m}' ]}
_FoundationAll = { :spec_name => "FoundationAll", :sub_dependency => [_NSString, _NSURL, _NSDate, _NSArray, _NSDictionary ] }
_UIAll = { :spec_name => "UIAll", :sub_dependency => [_UIColor, _UIView, _UIImage, _UIButton, _UIDevice, _UITableView, _UIViewController ] }
_all_subspec = [_GTMBase64, _UIColor, _UIView, _UIImage, _UIDevice, _UITableView, _UIViewController, _UIButton, _NSURL, _NSDate, _NSArray, _NSString, _NSDictionary, _Util, _FoundationAll, _UIAll]
_all_subspec.each do |spec|
s.subspec spec[:spec_name] do |ss|
specname = spec[:spec_name]
_all_names << specname
if spec[:source_files]
ss.source_files = spec[:source_files]
end
if spec[:sub_dependency]
spec[:sub_dependency].each do |dep|
ss.dependency "YTXUtilCategory/#{dep[:spec_name]}"
end
end
if spec[:dependency]
spec[:dependency].each do |dep|
ss.dependency dep[:name], dep[:version]
end
end
end
end
spec_names = _all_names[0...-1].join(", ") + " 和 " + _all_names[-1]
s.description = "拆分了這些subspec:#{spec_names}"
end
通過分析代碼可以知道:
- 有這些subspecs:GTMBase64, UIColor, UIView, UIImage, UIDevice, UITableView, UIViewController, UIButton, NSURL, NSDate, NSArray, NSString, NSDictionary, Util, FoundationAll, UIAll
- NSDate依賴第三方DateTools。
- NSString依賴兄弟GTMBase64。
- FoundationAll依賴兄弟subspecs,自己沒什么內(nèi)容,或者說提供一個靈活的方式一口氣納入所有相關Foundation內(nèi)容。
- UIAll依賴兄弟subspecs,和FoundationAll想要做的是一樣的。
相信各位讀者看了這個podspec也就知道怎么創(chuàng)建自己的subspec了?;蛘呖纯?a target="_blank" rel="nofollow">ARAnalytics的podspec。
在App中的Podflie是這樣用的:
pod 'YTXUtilCategory','~> 1.2.0'
或
pod 'YTXUtilCategory','~> 1.2.0', :subspecs => ["UIColor", "FoundationALL"]
從分析的結果來看我應該創(chuàng)建這些target:

在Example/Podfile中根據(jù)target的名字增加以下內(nèi)容并pod install:
target 'YTXUtilCategoryGTMBase64Binary' do
end
.........省略
target 'YTXUtilCategoryNSDateBinary' do
pod 'DateTools', '~> 1.0'
end
target 'YTXUtilCategoryUtilBinary' do
end
注意YTXUtilCategoryNSDateBinary,把它的第三方依賴加上。版本和podspec里描寫的一致。
在根目錄增加兩個shell腳本。
buildbinary.sh和教程一的基本一致,只是改了第一行:
#獲得第一個參數(shù)
PROJECT_NAME=$1
# 編譯工程
BINARY_NAME="${PROJECT_NAME}Binary"
buildallbinary.sh
pushd "$(dirname "$0")" > /dev/null
SCRIPT_DIR=$(pwd -L)
popd > /dev/null
#也可以寫個for循環(huán)
./buildbinary.sh YTXUtilCategoryGTMBase64
......省略
./buildbinary.sh YTXUtilCategoryUtil
執(zhí)行
./buildallbinary.sh
得到結果:
![subspecsbinarybuildresult] (https://cloud.githubusercontent.com/assets/2350193/16981721/6ec1af32-4e9e-11e6-9c4a-37e6018ebf5c.png)
更改podspec內(nèi)容為:
Pod::Spec.new do |s|
......省略
_all_names = []
_GTMBase64 = { :spec_name => "GTMBase64"}
_UIColor = { :spec_name => "UIColor"}
_UIView = { :spec_name => "UIView"}
_UIImage = { :spec_name => "UIImage"}
_UIDevice = { :spec_name => "UIDevice"}
_UITableView = { :spec_name => "UITableView"}
_UIViewController = { :spec_name => "UIViewController"}
_UIButton = { :spec_name => "UIButton"}
_NSURL = { :spec_name => "NSURL"}
_NSArray = { :spec_name => "NSArray"}
_NSDictionary = { :spec_name => "NSDictionary"}
_NSDate = { :spec_name => "NSDate", :dependency => [{:name => "DateTools", :version => "~> 1.0" }] }
_NSString = { :spec_name => "NSString", :sub_dependency => [_GTMBase64] }
_Util = { :spec_name => "Util"}
_temp = [_GTMBase64, _UIColor, _UIView, _UIImage, _UIDevice, _UITableView, _UIViewController, _UIButton, _NSURL, _NSArray, _NSDictionary, _NSDate, _NSString, _Util]
puts '-------------------------------------------------------------------'
if ENV['IS_SOURCE']
puts '-------------------------------------------------------------------'
puts "Notice:#{s.name} is source now"
puts '-------------------------------------------------------------------'
_temp.each do |spec|
spec[:source_files]=["Pod/Classes/#{spec[:spec_name]}/*.{h,m}"]
end
else
puts '-------------------------------------------------------------------'
puts "Notice:#{s.name} is binary now"
puts '-------------------------------------------------------------------'
_temp.each do |spec|
spec[:source_files]=["Pod/Products/#{s.name}#{spec[:spec_name]}/include/**"]
spec[:public_header_files]=["Pod/Products/#{s.name}#{spec[:spec_name]}/include/*.h"]
spec[:vendored_libraries]=["Pod/Products/#{s.name}#{spec[:spec_name]}/lib/*.a"]
end
end
_FoundationAll = { :spec_name => "FoundationAll", :sub_dependency => [_NSString, _NSURL, _NSDate, _NSArray, _NSDictionary ] }
_UIAll = { :spec_name => "UIAll", :sub_dependency => [_UIColor, _UIView, _UIImage, _UIButton, _UIDevice, _UITableView, _UIViewController ] }
_all_subspec = [_GTMBase64, _UIColor, _UIView, _UIImage, _UIDevice, _UITableView, _UIViewController, _UIButton, _NSURL, _NSDate, _NSArray, _NSString, _NSDictionary, _Util, _FoundationAll, _UIAll]
_all_subspec.each do |spec|
s.subspec spec[:spec_name] do |ss|
specname = spec[:spec_name]
_all_names << specname
if spec[:source_files]
ss.source_files = spec[:source_files]
end
if spec[:public_header_files]
ss.public_header_files = spec[:public_header_files]
end
if spec[:vendored_libraries]
ss.ios.vendored_libraries = spec[:vendored_libraries]
end
if spec[:sub_dependency]
spec[:sub_dependency].each do |dep|
ss.dependency "YTXUtilCategory/#{dep[:spec_name]}"
end
end
if spec[:dependency]
spec[:dependency].each do |dep|
ss.dependency dep[:name], dep[:version]
end
end
end
end
spec_names = _all_names[0...-1].join(", ") + " 和 " + _all_names[-1]
s.description = "拆分了這些subspec:#{spec_names}"
end
難點其實在于podspec如何寫,如何描述subspec之間的關系,如何拆分subspec。
假如a.h和b.h都用到了c.h,而a.h隸屬于subspec A,而b.h隸屬于subspec B。那你應該做一個subspec C其中包含c.h。而A和B都依賴C。
要避免a.h依賴b.h,b.h依賴a.h這種循環(huán)依賴的問題。
到此為止,含有subspec的CocoaPods庫就這么簡單的完成了。
后記,在做subspec二進制化遇到的問題
_all_sync.each do |sync_spec|
...
ss.prefix_header_contents = "#define YTX_#{specname.upcase}_EXISTS 1"
...
end
注意ss. ss.prefix_header_contents這段。加了一個宏。然后在這里會用到:
#import "YTXRestfulModel.h"
#ifdef YTX_USERDEFAULTSTORAGESYNC_EXISTS
#import "YTXRestfulModelUserDefaultStorageSync.h"
#endif
#ifdef YTX_AFNETWORKINGREMOTESYNC_EXISTS
#import "AFNetworkingRemoteSync.h"
#endif
#ifdef YTX_YTXREQUESTREMOTESYNC_EXISTS
#import "YTXRestfulModelYTXRequestRemoteSync.h"
#endif
#ifdef YTX_FMDBSYNC_EXISTS
#import "YTXRestfulModelFMDBSync.h"
#import "NSValue+YTXRestfulModelFMDBSync.h"
#endif
在源碼的情況下,如果我想要用YTXRequestRemote這個subspec,那么引入YTXRequestRemote時會自帶宏YTX_YTXREQUESTREMOTESYNC_EXISTS,YTXRestfulModel在編譯時會根據(jù)宏引入相關頭文件。
問題來了,宏都是預編譯的。當我編譯出二進制時,內(nèi)容已經(jīng)決定了。這樣就喪失了subspec的動態(tài)性了。所以關鍵的問題在于當初設計的時候沒有考慮好。
希望大家看到這個例子后,避免將來遇到相思的問題。目前沒有想到好的解決方案,所以這個庫并沒有二進制化。