在iOS開發中,事實標准是我們使用CocoaPods生成、管理和使用library。這裡的library就是一個模塊、組件或庫。二進制化指的是通過編譯把組件的源碼轉換成靜態庫或動態庫,以提高該組件在App項目中的編譯速度。
我們的方案是轉換成靜態庫,也就是.a格式的文件加上暴露出來的頭文件。
在我們App開發中,我們逐漸的抽象了很多模塊、業務、UI等把他轉換成私有CocoaPod庫。其中有一個是用C++和Objective-C混寫的,源碼格式為.mm。在app項目編譯時.mm部分代碼編譯非常慢。這作為一個契機讓我們去考慮如何加快編譯速度。
這個混寫的CocoaPod庫叫做YTXChart,之後會以此庫為例反復提到。
另外隨著業務的擴展,私有CocoaPod庫和第三方CocoaPod庫越來越多,App項目中的文件也越來越多。每次pod install
安裝新庫或pod update
更新庫的時候,重新編譯的過程需要等待很長時間。這也向我們提出了加快編譯速度的需求。
另外如果想要做組件化的話,一定要做二進制化。
所以我們想到了二進制化的方案來解決這個問題,並且很多大公司也是這麼做的。
對我們來說,這是一個嘗試,不可能開始就決定把所有的私有CocoaPod庫二進制化,也不可能決定把所有第三方CocoaPod庫二進制化。當務之急的情況是加快YTXChart庫編譯速度。所以必須找到一個方案平滑過度。
我們的App中的podflie是這樣的
target 'jryMobile' do pod 'AFNetworking', '~> 2.6.3' pod 'Mantle', '~> 1.5.7' pod 'DateTools', '~> 1.7.0' pod 'ReactiveCocoa', '~> 2.3.1' pod 'CocoaAsyncSocket', '~> 7.4.1' pod 'FMDB', '~> 2.5' pod 'MWPhotoBrowser', '~> 1.4.1' pod 'MZFormSheetController', '~> 2.3.6' pod 'HMSegmentedControl', '~> 1.5.1' pod 'UMengAnalytics', '~> 3.5.8' pod 'UMengFeedback', '~> 2.3.4' pod 'TSMessagesNW', '~> 0.9.15' pod 'TPKeyboardAvoiding', '~> 1.2.9' pod 'SDWebImage', '~> 3.7' pod 'JHChainableAnimations', '~> 1.3.0' pod 'BarrageRenderer', '~> 1.7.0' pod 'MJRefresh', '~> 3.1.7' pod 'YTXAnimations', '~> 1.2.4', :subspecs => ["AnimateCSS", "Transformer"] pod 'YTXMediaIJKPlayer', '~> 0.2.1' pod 'YTXTradeBusinessType', '~> 1.1.0' pod 'YTXServerId', '~> 0.1.4' pod 'YTXUtilCategory','~> 1.2.0' pod 'YTXScreenShotManager', '~> 0.1.7' pod 'YTXRequest', '~> 1.0.0' pod 'YTXCommonSocket', '~> 0.1.9' pod 'YTXChartSocket', '~> 0.5.1' # 希望是二進制化的 pod 'YTXChart', '~> 0.17.0' pod 'YTXRestfulModel', '~> 1.2.2', :subspecs => ["RACSupport", "YTXRequestRemoteSync", "FMDBSync", "UserDefaultStorageSync"] pod 'YTXWebViewJavaScriptBridge', '~> 0.1.2' pod 'YTXCheckForAppUpdates', '~> 1.0.0' # pod 'YTXVideoAVPlayer', '~> 0.5.0' pod 'YTXChatUI', '~> 0.3.2' pod 'PNChart', '~>0.8.9' #pod 'EaseMobSDKFull', :git => 'https://github.com/easemob/sdk-ios-cocoapods-integration.git', :tag => '2.2.0' # EaseMobSDKFull 更新地址'https://github.com/easemob/sdk-ios-cocoapods-integration.git' #pod 'AFgzipRequestSerializer', '~> 0.0.2' pod 'AdhocSDK', '~> 2.2.1' pod 'FLEX', '~> 2.0', :configurations => ['Debug'] pod 'React', :path => './ReactComponent/node_modules/react-native', :subspecs => [ 'Core', 'RCTImage', 'RCTNetwork', 'RCTText', 'RCTWebSocket', # 添加其他你想在工程中使用的依賴。 ] pod 'CodePush', :path => './ReactComponent/node_modules/react-native-code-push' end
其他的CocoaPod庫都還是源碼。YTXChart為二進制化。
以後能夠逐步迭代把更多的以YTX開頭的CocoaPod庫進行二進制化,而不影響主App。
能夠提供一種方式把二進制化CocoaPod庫切換回源碼CocoaPod庫以便調試。盡量做的方便。
解決YTXChart引用依賴的問題。(YTXChart還依賴了第三方AFNetworking和私有YTXServerId。保證生成的靜態庫中不會含有AFNetworking的內容和YTXServerId的內容並且能夠編譯通過)
利用原來的YTXChart.git,不創建新項目,不創建新的git庫。因為我們的二進制化庫的生成還是來自於源碼,當源碼更新時,我們需要一種非常快捷的方式去生成二進制的東西,不希望copy源碼到某處,或者增加一個git submodule。
希望App源碼和YTXChart中的源碼盡量少或者沒有改動。
希望App中的Podfile盡量少或者沒有改動。
希望Podfile中的版本號保持風格一致,不會出現'~> 2.2.1.binary'
這種情況。
用原來的那一個CocoaPods Repo Spec。
注意,以下的例子基於[email protected],而且目前只能是1.0.1
如果你是通過命令pod lipo create
創建的CocoaPod庫並且pod install
的話,它的目錄結構應該像這樣子(只列出重要的):
YTXChart |-Example |-YTXChart |-Pods |-YTXChart.xcodeproj |-YTXChart.xcworkspace |-Podfile \-Podfile.lock |-Pod |-Assets \-Classes \-YTXChart.podspec
在xcode中創建新Target YTXChartBinaryFile->New->Target->Framework & Library->Cocoa Touch Static Library
如果你們的項目最低支持到iOS8可以創建Dynamic Framework
注意在Podfile中加入以下這段
target 'YTXChartBinary' do end
然後pod install
解釋:[email protected]會在Header Search Path自動加入內容。如果你用[email protected]則需要自己加Header Search Path保證依賴庫YTXServerId和AFNetwork能夠被找到。如圖:
然後把Pod/Classes中的源碼拖入到YTXChartBinary中,這樣選擇(這樣會link源碼而不是復制):
然後變成這樣子:
Headers需要自己加,裡面是你需要暴露的頭文件
在YTXChartBinary Target中的Build Settings下找到iOS Deployment Target選擇和YTXChart.podspec中的s.platform保持一致。這裡是7.0YTXChartBinary Target->Build Settings->iOS Deployment Target
在根目錄創建shell腳本buildbinary.sh
你也可以創建一個Aggregate Target用來執行shell腳本
代碼如下:
PROJECT_NAME="YTXChart" BINARY_NAME="${PROJECT_NAME}Binary" cd Example INSTALL_DIR=$PWD/../Pod/Products rm -fr "${INSTALL_DIR}" mkdir $INSTALL_DIR WRK_DIR=build BUILD_PATH=${WRK_DIR} DEVICE_INCLUDE_DIR=${BUILD_PATH}/Release-iphoneos/usr/local/include DEVICE_DIR=${BUILD_PATH}/Release-iphoneos/lib${BINARY_NAME}.a SIMULATOR_DIR=${BUILD_PATH}/Release-iphonesimulator/lib${BINARY_NAME}.a RE_OS="Release-iphoneos" RE_SIMULATOR="Release-iphonesimulator" xcodebuild -configuration "Release" -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${BINARY_NAME}" -sdk iphoneos clean build CONFIGURATION_BUILD_DIR="${WRK_DIR}/${RE_OS}" LIBRARY_SEARCH_PATHS="./Pods/build/${RE_OS}" xcodebuild ARCHS=x86_64 ONLY_ACTIVE_ARCH=NO -configuration "Release" -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${BINARY_NAME}" -sdk iphonesimulator clean build CONFIGURATION_BUILD_DIR="${WRK_DIR}/${RE_SIMULATOR}" LIBRARY_SEARCH_PATHS="./Pods/build/${RE_SIMULATOR}" if [ -d "${INSTALL_DIR}" ] then rm -rf "${INSTALL_DIR}" fi mkdir -p "${INSTALL_DIR}" cp -rp "${DEVICE_INCLUDE_DIR}" "${INSTALL_DIR}/" INSTALL_LIB_DIR=${INSTALL_DIR}/lib mkdir -p "${INSTALL_LIB_DIR}" lipo -create "${DEVICE_DIR}" "${SIMULATOR_DIR}" -output "${INSTALL_LIB_DIR}/lib${BINARY_NAME}.a" lipo -remove i386 "${INSTALL_LIB_DIR}/lib${BINARY_NAME}.a" -output "${INSTALL_LIB_DIR}/lib${PROJECT_NAME}.a" rm -r "${WRK_DIR}"
這個腳本寫的並不是很好。說說主要做了什麼。Release不同的靜態庫,真機和模擬器的。只構建x86_64,不構建i386加快速度
xcodebuild -configuration "Release" -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${BINARY_NAME}" -sdk iphoneos clean build CONFIGURATION_BUILD_DIR="${WRK_DIR}/${RE_OS}" LIBRARY_SEARCH_PATHS="./Pods/build/${RE_OS}" xcodebuild ARCHS=x86_64 ONLY_ACTIVE_ARCH=NO -configuration "Release" -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${BINARY_NAME}" -sdk iphonesimulator clean build CONFIGURATION_BUILD_DIR="${WRK_DIR}/${RE_SIMULATOR}" LIBRARY_SEARCH_PATHS="./Pods/build/${RE_SIMULATOR}"
*通過lipo命令合並。新.a使用project name是因為要和App項目的OTHER_LDFLAGS兼容-l"YTXChart"
lipo -create "${DEVICE_DIR}" "${SIMULATOR_DIR}" -output "${INSTALL_LIB_DIR}/lib${PROJECT_NAME}.a"
結果:
實際上,二進制化方案就是以空間換時間。我們這個YTXChart庫生成的.a去除i386之後大小有166.3M。上傳到git倉庫後,git會壓縮。增加了33M左右。而作為二進制文件,git是沒法做增量的。所以每次上傳.a都會大大增加git庫大小,增加硬盤使用量。考慮到我們的服務器硬盤只有60個G能用,以後還會二進制化很多組件。
所以得出:盡量壓縮二進制文件大小;盡量不上傳.a,直到發布某個版本時才上傳。
當然,如果你的服務器硬盤是1T的話,我覺得你也可以隨便搞。
現在文件目錄是這樣子的:
YTXChart |-Example |-YTXChart |-Pods |-YTXChart.xcodeproj |-YTXChart.xcworkspace |-YTXChartBinary //空的 |-Podfile \-Podfile.lock |-Pod |-Assets |-Classes //裡面是源碼 \-Products |-include |-xxx.h |-... \-xxx.h \-lib \- libYTXChartBinary.a \-YTXChart.podspec
修改YTXChart.podspec如下:
Pod::Spec.new do |s| s.name = "YTXChart" s.version = "0.17.7" s.summary = "YTXChart for pod" # This description is used to generate tags and improve search results. # * Think: What does it do? Why did you write it? What is the focus? # * Try to keep it short, snappy and to the point. # * Write the description between the DESC delimiters below. # * Finally, don't worry about the indent, CocoaPods strips it! s.description = "銀天下Chart, 依賴AFNetworking" s.homepage = "http://gitlab.baidao.com/ios/YTXChart.git" # s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2" s.license = 'MIT' s.author = { "caojun-mac" => "[email protected]" } s.source = { :git => "http://gitlab.baidao.com/ios/YTXChart.git", :tag => s.version } # s.social_media_url = 'https://twitter.com/' s.platform = :ios, '7.0' s.requires_arc = true s.source_files = 'Pod/Products/include/**' s.public_header_files = 'Pod/Products/include/*.h' s.ios.vendored_libraries = 'Pod/Products/lib/libYTXChart.a' s.libraries = 'sqlite3', 'c++' s.dependency 'YTXServerId' s.dependency 'AFNetworking', '~> 2.0' end
注意s.sourcefiles和s.publicheaderfiles和s.ios.vendoredlibraries的路徑
Exampl/Podfile是長這樣子的:
source 'http://gitlab.baidao.com/ios/ytx-pod-specs.git' source 'https://github.com/CocoaPods/Specs.git' target 'YTXChart_Example' do pod "YTXChart", :path => "../" pod 'ReactiveCocoa', '~> 2.5' pod 'YTXChartSocket' pod 'AFNetworking', '~> 2.0' end target 'YTXChartBinary' do end target 'YTXChart_Tests' do pod "YTXChart", :path => "../" pod 'Kiwi' end
執行pod install
後應該是這樣子的,然後跑起來沒問題
執行pod lib lint --sources='http://gitlab.baidao.com/ios/ytx-pod-specs.git,master'--verbose --use-libraries --fail-fast
也是好的
證明:把s.dependency 'AFNetworking', '~> 2.0'
去除再執行pod lib lint 'http://gitlab.baidao.com/ios/ytx-pod-specs.git,master' --verbose --use-libraries --fail-fast
會報出找不到AFNetwork相關文件。
題外話:因為CocoaPods1.0.1不支持C++項目的lint(這是一個defect),所以這個時候我會切回[email protected]來lint和publish。而前面pod instal增加Search Path是依靠[email protected]。如果你不是.mm混寫的,是不會有這個問題的。盡管使用[email protected]。強行當作沒看到這個題外話。
下一步解決如何在源碼和二進制中切換修改YTXChart.podspec為以下內容:
# # Be sure to run `pod lib lint YTXChart.podspec' to ensure this is a # valid spec before submitting. # # Any lines starting with a # are optional, but their use is encouraged # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| s.name = "YTXChart" s.version = "0.17.7" s.summary = "YTXChart for pod" # This description is used to generate tags and improve search results. # * Think: What does it do? Why did you write it? What is the focus? # * Try to keep it short, snappy and to the point. # * Write the description between the DESC delimiters below. # * Finally, don't worry about the indent, CocoaPods strips it! s.description = "銀天下Chart, 依賴AFNetworking" s.homepage = "http://gitlab.baidao.com/ios/YTXChart.git" # s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2" s.license = 'MIT' s.author = { "caojun-mac" => "[email protected]" } s.source = { :git => "http://gitlab.baidao.com/ios/YTXChart.git", :tag => s.version } # s.social_media_url = 'https://twitter.com/' s.platform = :ios, '7.0' s.requires_arc = true if ENV['IS_SOURCE'] puts '-------------------------------------------------------------------' puts 'Notice:YTXChart is source now' puts '-------------------------------------------------------------------' s.source_files = "Pod/Classes/painter/*.{h,m,mm}", "Pod/Classes/painterview/*.{h,m,mm}", "Pod/Classes/chart/*.{h,m,mm}", "Pod/Classes/core/*.{h,mm}", "Pod/Classes/core/**/*.{h,m,mm,inl}" else puts '-------------------------------------------------------------------' puts 'Notice:YTXChart is binary now' puts '-------------------------------------------------------------------' s.source_files = 'Pod/Products/include/**' s.public_header_files = 'Pod/Products/include/*.h' s.ios.vendored_libraries = 'Pod/Products/lib/libYTXChart.a' end s.libraries = 'sqlite3', 'c++' s.dependency 'YTXServerId' s.dependency 'AFNetworking', '~> 2.0' end
注意這段if ENV['IS_SOURCE']
。我們的需求是優先使用二進制,偶爾才會切回源碼。
刪除Example/Pods目錄。
執行IS_SOURCE=1 pod install
。你會看到Example/Pods/YTXChart/裡面都是源碼
輸出Notice:YTXChart Now is source
進一步跑起模擬器,因為是源碼編譯用了很長時間,模擬器起來,一切也是好的
再試下pod cache clean --all && IS_SOURCE=1 pod lib lint
也是好的
再試下pod cache clean --all && pod lib lint
也是好的
現在我們通過if else
簡單地實現了本地Example App項目切換源碼和二進制。
發布就和正常發布沒有任何區別。
Podfile修改為pod 'YTXChart', '~> 0.17.7'
以下兩步很重要
pod cache clean --all
刪除Example/Pods
然後pod install
檢查Example/Pods/YTXServerId/和Example/Pods/AFNetwork/發現都是.h .m源碼。
檢查Example/Pods/YTXChart/裡的是二進制.a和頭文件。跑起App並沒有問題。
如果你直接IS_SOURCE=1 pod install
你會發現Example/Pods/YTXChart/裡的內容都變成了空
這是為什麼呢,因為pod cache了一個podspec.json。可以通過pod cache list
查看。他cache了一個描述如何從s.source中找到相關文件。現在的描述還是從Pod/Products/下去找,自然為空。
為了避免這個問題,所以必須執行上面兩步。這個是唯一的問題,目前我還找不到更好的解決方案。切換的行為只是偶爾發生,這是可以接受的。
執行2步。再次IS_SOURCE=1 pod install
你就發現Example/Pods/YTXChart/裡的內容都變成了.h .mm源碼。跑起App也是好的。
為什麼lint之前要cache clean。原理是一樣的。如果YTXChart依賴的YTXServerId也被做成了二進制化就需要cache clean。不過你也可以這樣pod cache clean YTXServerId
pod install
可能會有某幾個已經二進制化的庫使用二進制的內容。IS_SOURCE=1 pod install
時,所有的庫都將會是源碼的內容。請參考這篇我的文章CocoaPod版本規范
當你發布完成之後,查看。我們發現在Spec Repo中對應版本的podspec就是我們的YTXChart/podspec。CocoaPod從s.source
git地址和tag下載對應的代碼,Pod/Products和Pod/Classes裡的內容都存在當你使用IS_SOURCE=1
時ENV['IS_SOURCE']
會為true。CocoaPods通過
s.source_files從下載代碼的路徑找到源碼構建Example/Pods和YTXChart.xcworkspace
明白了上面的過程,來再分析下為什麼要在切換源碼和二進制化時刪除cache和Pods目錄。放幾張圖就明白了
刪除cache和Pods目錄。IS_SOURCE=1 pod install
觀察json。
沒有使用submodule或新的git倉庫來構建出一個不包含依賴內容的靜態庫。一份原來的git倉庫。
沒有因為構建二進制庫而需要增加冗余的源代碼。所以當你修改Pod/Classes中的源碼,可以方便簡單地執行buildbinary.sh腳本來構建出靜態庫。一份源碼。
共用了一份YTXChart.podspec
沒有大量修改YTXChart.podspec
使用pod lib lint
和IS_SOURCE=1 pod lib lint
檢查通過。
沒有修改Podfile。這個Example的Podflie只是測試需要才改的。主App項目中的Podfile可以一行都不改。不會出現'~> 2.2.1.binary'
。
App中的源碼不會因為使用了二進制CocoaPods組件而做任何修改。
沒有手動配置Search Path,這樣更容易。
在Example App中可以通過IS_SOURCE
靈活地切換源碼和二進制靜態庫。唯一一個問題每次切換要刪除Pods目錄和pod cache clean --all
跑起Example App總是好的。
沒有影響到其他庫,我可以逐步平滑地把YTXXXX一個一個做成二進制。
逐步平滑地把YTXXXX一個一個做成二進制
進一步的把第三方如AFNetwork在私有spec repo中做份鏡像也提供二進制化
把Podfile中絕大部分組件都做成二進制(RN這種本地安裝模式和有sub spec的庫目前不打算二進制化)
兩個方案:
提供一個全集
對每一個sub spec都做份二進制並保持它們之間依賴的相互關系
現在這個解決方案看起來簡單,但在當初的探索過程中並不是那麼順利。以下是不成功的嘗試!
把生成的Products目錄放到YTXChartBinary下
把YTXChartBinary.podspec目錄放到YTXChartBinary下
Podfile中通過增加Binary字段安裝二進制化如pod 'YTXChartBinary', '~> 0.17.7'
問題
要維護2個podspec。版本號很可能不統一。
當pod spec lint
報錯:找不到相關文件。
Podfile中通過增加Binary字段來切換,非常不方便。
要改App源碼。當安裝二進制的時候
需要改成
。來回切換都需要改,極不方便。
要改App源碼。這次一勞永逸。直接這樣使用"YTXChart.h"
。但這樣也不好。
解決了要改App源碼的問題。只需要在Podfile中加個source。
不同的source例子source 'http://gitlab.baidao.com/ios/ytx-binary-pod-specs.git'
source 'http://gitlab.baidao.com/ios/ytx-pod-specs.git'
問題
發布兩次,lint兩次。
創建了2個Spec Repo。