起因
元件化的方案大同小異,基礎性程式碼封裝私有庫,業務元件互動交由中介軟體負責,專案依賴工具用iOS專案事實上的標準CocoaPods
前期的基礎性元件拆分都較為順利,從依賴樹的葉子節點開發是最合適的方案。
隨著元件抽離的越來越多,私有庫的依賴體系也越來越複雜,慢慢過渡到了業務元件。業務元件用了Swift的第三方元件,用了Swift庫的同學都知道必須加上use_frameworks!,這個標記是說Pod管理的依賴全部編譯為動態庫,然後呢我們的很多元件又依賴了諸如百度地圖,微信分享等靜態庫,於是我在執行 pod install 報了一個沒有碰見過的錯誤。
[!] The 'Pods-LJA_Example' target has transitive dependencies that include static binaries:複製程式碼
這就尷尬了,於是一陣瘋狂的搜尋google stackoverflow等,然而並沒有什麼卵用,而且上面催得急,根本沒時間處理這些小問題 業務重構是最主要的,以至於我們的業務元件沒有做到獨立倉庫拆分。
直到最近終於找到了解決辦法:( 主要是自己的功力不夠深厚)
理論功底
動態庫和靜態庫
介紹
首先靜態庫和動態庫都是以二進位制提供程式碼複用的程式碼庫
靜態庫 常見的是 .a動態庫常見的是 .dll(windows),.dylib(mac),so(linux)framework(in Apple): Framework 是Cocoa/Cocoa Touch程式中使用的一種資源打包方式,可以將程式碼檔案、標頭檔案、資原始檔、說明文件等集中在一起,方便開發者使用。也就是說我們的 framework其實是資源打包的方式,和靜態庫動態庫的本質是沒有關係的靜態庫和動態庫的區別
靜態庫: 連結時會被完整的複製到可執行檔案中,所以如果兩個程式都用了某個靜態庫,那麼每個二進位制可執行檔案裡面其實都含有這份靜態庫的程式碼
動態庫: 連結時不復制,在程式啟動後用dyld載入,然後再決議符號,所以理論上動態庫只用存在一份,好多個程式都可以動態連結到這個動態庫上面,達到了節省記憶體(不是磁碟是記憶體中只有一份動態庫),還有另外一個好處,由於動態庫並不繫結到可執行程式上,所以我們想升級這個動態庫就很容易,windows和linux上面一般外掛和模組機制都是這樣實現的。
But我們的蘋果爸爸在iOS平臺上規定不允許存在動態庫,並且所有的 IPA 都需要經過蘋果爸爸的私鑰加密後才能用,基本你用了動態庫也會因為簽名不對無法載入,(越獄和非 APP store 除外)。於是就把開發者自己開發動態庫掐死在幻想中。
直到有一天,蘋果爸爸的iOS升級到了8,iOS出現了APP Extension,swift程式語言也誕生了,由於iOS主APP需要和Extension共享程式碼,Swift語言的機制也只能有動態庫,於是蘋果爸爸尷尬了,不過這難不倒我們的蘋果爸爸,畢竟我是爸爸,規則是我來定,我想怎樣就怎樣,於是提出了一個概念Embedded Framework,這種動態庫允許APP和APP Extension共享程式碼,但是這份動態庫的生命被限定在一個APP程序內。簡單點可以理解為被閹割的動態庫。
舉個例子,iOS專案中使用Embeded Framework
如果你把某個自己開發的動態庫(系統的不算,畢竟蘋果是爸爸)放在了Linked Frameworks and Libraries裡面,程式一啟動就會報Reason: Image Not Found,你只能把它放在Embeded Binaries裡面才能正常使用,
看圖:
靜態庫和動態庫如何構建和載入
簡單點,說話的方式簡單點~~
上面的介紹貌似有點抽象啊套用在美團技術分享大會上的話就是:
靜態庫: 一堆目標檔案(.o/.obj)的打包體(並非二進位制檔案)動態庫: 一個沒有main函式的可執行檔案這裡我們來複習下C語言的基本功,編譯和連結
編譯: 將我們的原始碼檔案編譯為目標檔案連結: 將我們的各種目標檔案加上一些第三方庫,和系統庫連結為可執行檔案。由於某個目標檔案的符號(可以理解為變數,函式等)可能來自其他目標檔案,其實連結這一步最主要的操作就是決議符號的地址。
若符號來自靜態庫(本質就是.o 的集合包)或 .o,將其納入連結產物,並確定符號地址若符號來自動態庫,打個標記,等啟動的時候再說---交給dyld去載入和連結符號於是連結加裝載就有了不同的情況
Load 裝載:將庫檔案載入記憶體Static Loading:啟動時
Dynamic Loading:啟動後(使用時)
Link 連結:決議符號地址Static Linking:構建(連結)時
Dynamic Linking:執行時(啟動時或使用時)
然後組合起來就是 2 * 2 = 4 了
Static Loading + Static LinkingStatic Loading + Dynamic LinkingDynamic Loading + Dynamic Linking~~Dynamic Loading + Static Linking~~第一種是純靜態庫相關了
第二種就是靜態載入(啟動時),動態連結,連結時,動態庫參與連結,但是這時候只是給符號打了標記告訴我這個符號來自與動態庫,程式啟動時,iOS或者Mac OS作業系統的dyld自動load + link。
既然全部都是自動的。那麼符號的呼叫方完全不知道你到底是原始碼還是靜態庫,動態庫 。
第四種,沒見過,個人也不是特別懂
有需求請參看文後的程式設計師的自我修養一書
靜態庫和動態庫依賴關係
既然有 2 種庫,那麼依賴關係又是 2 * 2 嘍
libA.a dependency libB.aUIKit.dylib dependency Foundation.dyliblibA.a dependency Foundation.dylibMyXX.dylib dependency libA.a第一種 靜態庫互相依賴,這種情況非常常見,製作靜態庫的時候只需要有被依賴的靜態庫標頭檔案在就能編譯出來。但是這就意味者你要收到告訴使用者你的依賴關係
幸運的是CocoaPod就是這樣做的
第二種動態庫依賴動態庫,兩個動態庫是相互隔離的具有隔離性,但是製作的靜態庫的時候需要被依賴動態庫參與連結,但是具體的符號決議交給dyld來做。
第三種,靜態庫依賴動態庫,也很常見,靜態庫製作的時候也需要動態庫參與連結,但是符號的決議交給dyld來做。
第四種,動態庫依賴靜態庫,這種情況就有點特殊了。首先我們設想動態庫編譯的時候需要靜態庫參與編譯,但是靜態庫交由dyld來做符號決議,but這和我們前面說的就矛盾了啊。靜態庫本質是一堆.o 的打包體,首先並不是二進位制可執行檔案,再者你無法保證主程式把靜態庫參與連結共同生成二進位制可執行檔案。這就尷尬了。
怎麼辦?
目前的編譯器的解決辦法是,首先我無法保證主程式是否包含靜態庫,再者靜態庫也無法被dyld載入,那麼我直接把你靜態庫的.o 偷過來,共同組成一個新的二進位制。也被稱做吸附性
那麼我有多份動態庫都依賴同樣的靜態庫,這就尷尬了,每個動態庫為了保證自己的正確性會把靜態庫吸附進來。然後兩個庫包含了同樣的靜態庫,於是問題就出現了。 看到這裡想必前面出現的錯誤你已經能猜出來了把~_~
後面再詳細解釋
先來個總結
可執檔案(主程式或者動態庫)在構建的連結階段
遇到靜態庫,吸附進來遇到動態庫,打標記,彼此保持獨Xcode 專案結構
target:對於一個產物(app,.a ,.framework)project:一個專案包含多個 targetworkspace: 一個包含多個 targetschema: 指定了一個產物是按照何種的依賴關係,編譯-連結到最終的一個產物iOS 依賴管理事實上的標準
這麼多年,Apple的部落格和文件也就告訴了我們什麼是靜態庫什麼是動態庫,如何製作等。但是並沒有給我們提供一系列的依賴管理工具。所以CocoaPods成了事實上的標準。
通常CocoaPods管理的工程結構如下:
那麼當我們按下CMD + B的時候,整個專案按照先編譯被依賴Pod,然後依賴其他Pod的Pod也被構建出來,最終所有的元件被編譯為一個lib-Pods-XXXAPP.a被新增進專案進去。資源透過CocoaPods提供的指令碼也一併被複制進去。想了解CocoaPods做了什麼的讀者可以參看後面的連結
解決問題
這麼多理論功底的建立,相信我們已經能分析出來之前pod install的原因了。就是用了use_framework那麼我們的所有Pod都會以動態庫(Embeded Framework)的形式去構建,於是那些非開源的庫(如百度地圖,微信分享)如果被多個Pod依賴(元件化開發中太常見了)於是被吸附到動態庫裡面,所以CocoaPod直接就不讓我們install成功。因為你現在的依賴管理就是錯誤的。
在聽取美團葉樉老師分享的時候 他們的出發點是因為要繞過蘋果爸爸在iOS9以下對__text 段60M的限制使用了動態庫方案,我們是因為某些swift庫必須要用到(歷史遺留原因)動態庫。美團的做法是摘除依賴關係,自定義CocoaPods(開源的本來就是用著不爽我就改)。但是我是個小菜雞啊。我也不會 ruby(以後會學的),但是葉樉老師給我提了別的idea。前面我們知道 動態庫和動態庫是隔離性,動態庫依賴靜態庫具有吸附性,那麼我們可以自定義一個動態庫把百度地圖這種靜態庫吸附進來。對外整體呈現的是動態庫特性。其他的元件依賴我們自定義的動態庫,由於隔離性的存在,不會出現問題。
製作動態庫
1 建立動態庫專案這裡以 wx 舉例
3 將wx的PublicHeader暴露出來,注意由於我並沒有使用到wx相關API所以連結器幫我們連結動態庫的時候可能並不會把wx靜態庫吸附進來。我們手動在build Setting的other link flags加上-all_load標記
4.在Schema裡面跳轉編譯配置為Release,並且選擇所有的CPU架構
5 然後選擇模擬器或者 Generic iOS Device 執行編譯就會生成對應版本的 Framework 了。
6.但是為了保證開發者使用的時候是真機模擬器都能正常使用,我們需要合併不同架構
這裡在Build Phases裡新增以下指令碼,真機和模擬器都Build一遍之後就會在工程目錄下生成Products資料夾,
if [ "${ACTION}" = "build" ]thenINSTALL_DIR=${SRCROOT}/Products/${PROJECT_NAME}.frameworkDEVICE_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.frameworkSIMULATOR_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.frameworkif [ -d "${INSTALL_DIR}" ]thenrm -rf "${INSTALL_DIR}"fimkdir -p "${INSTALL_DIR}"cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"#ditto "${DEVICE_DIR}/Headers" "${INSTALL_DIR}/Headers"lipo -create "${DEVICE_DIR}/${PROJECT_NAME}" "${SIMULATOR_DIR}/${PROJECT_NAME}" -output "${INSTALL_DIR}/${PROJECT_NAME}"open "${DEVICE_DIR}"open "${SRCROOT}/Products"fi複製程式碼
於是我們有了我們自己的私有動態庫LJWXSDK,那麼我們來驗證我們之前的問題
首先指定一個LJWXSDK.podspec這裡我直接傳到了我的Github上面
## Be sure to run `pod lib lint LJPod.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 = 'LJWXSDK' s.version = '0.1.0' s.summary = 'A short description of LJWXSDK.' s.description = < 'MIT', : file => 'LICENSE' } s.author = { 'ValiantCat' => '[email protected]' } s.source = { : http => 'http://onk2m6gtu.bkt.clouddn.com/LJWXSDK.framework.zip' } s.ios.deployment_target = '8.0' s.default_subspec = 'zip' s.subspec 'zip' do |zip| puts '-------------------------------------------------------------------' puts 'Notice: LJWXSDK is zip now' puts '-------------------------------------------------------------------' zip.ios.vendored_frameworks = '*.framework' endend複製程式碼
注意上面我是把二進位制壓縮丟進了七牛的 oss 檔案儲存。畢竟免費還快。
然後透過 pod lib create 建立了一個 pod 用來驗證之前我們的傳遞性依賴問題,
資料夾結構如下
.├── Example│ ├── LJA│ │ ├── Base.lproj│ │ │ ├── LaunchScreen.storyboard│ │ │ └── Main.storyboard│ │ ├── Images.xcassets│ │ │ └── AppIcon.appiconset│ │ │ └── Contents.json│ │ ├── LJA-Info.plist│ │ ├── LJA-Prefix.pch│ │ ├── LJAppDelegate.h│ │ ├── LJAppDelegate.m│ │ ├── LJViewController.h│ │ ├── LJViewController.m│ │ ├── en.lproj│ │ │ └── InfoPlist.strings│ │ └── main.m│ ├── LJA.xcodeproj│ ├── LJA.xcworkspace│ ├── Podfile│ ├── Podfile.lock│ ├── Pods│ │ ├── Headers│ │ ├── LJWXSDK│ │ │ └── LJWXSDK.framework│ │ │ ├── Headers│ │ │ │ ├── LJWXSDK.h│ │ │ │ ├── WXApi.h│ │ │ │ ├── WXApiObject.h│ │ │ │ └── WechatAuthSDK.h│ │ │ ├── Info.plist│ │ │ ├── LJWXSDK│ │ │ ├── Modules│ │ │ │ └── module.modulemap│ │ │ ├── _CodeSignature│ │ │ │ └── CodeResources│ │ │ └── read_me.txt│ │ ├── Local\ Podspecs│ │ │ ├── LJA.podspec.json│ │ │ ├── LJB.podspec.json│ │ │ └── LJWXSDK.podspec.json│ │ ├── Manifest.lock│ │ ├── Pods.xcodeproj│ │ │ ├── project.pbxproj│ │ │ ├── project.xcworkspace│ │ ├── Target\ Support\ Files│ │ │ ├── LJA│ │ │ │ ├── Info.plist│ │ │ │ ├── LJA-dummy.m│ │ │ │ ├── LJA-prefix.pch│ │ │ │ ├── LJA-umbrella.h│ │ │ │ ├── LJA.modulemap│ │ │ │ └── LJA.xcconfig│ │ │ ├── LJB│ │ │ │ ├── Info.plist│ │ │ │ ├── LJB-dummy.m│ │ │ │ ├── LJB-prefix.pch│ │ │ │ ├── LJB-umbrella.h│ │ │ │ ├── LJB.modulemap│ │ │ │ └── LJB.xcconfig│ │ │ ├── Pods-LJA_Example│ │ │ │ ├── Info.plist│ │ │ │ ├── Pods-LJA_Example-acknowledgements.markdown│ │ │ │ ├── Pods-LJA_Example-acknowledgements.plist│ │ │ │ ├── Pods-LJA_Example-dummy.m│ │ │ │ ├── Pods-LJA_Example-frameworks.sh│ │ │ │ ├── Pods-LJA_Example-resources.sh│ │ │ │ ├── Pods-LJA_Example-umbrella.h│ │ │ │ ├── Pods-LJA_Example.debug.xcconfig│ │ │ │ ├── Pods-LJA_Example.modulemap│ │ │ │ └── Pods-LJA_Example.release.xcconfig│ │ │ └── Pods-LJA_Tests│ │ │ ├── Info.plist│ │ │ ├── Pods-LJA_Tests-acknowledgements.markdown│ │ │ ├── Pods-LJA_Tests-acknowledgements.plist│ │ │ ├── Pods-LJA_Tests-dummy.m│ │ │ ├── Pods-LJA_Tests-frameworks.sh│ │ │ ├── Pods-LJA_Tests-resources.sh│ │ │ ├── Pods-LJA_Tests-umbrella.h│ │ │ ├── Pods-LJA_Tests.debug.xcconfig│ │ │ ├── Pods-LJA_Tests.modulemap│ │ │ └── Pods-LJA_Tests.release.xcconfig│ │ └── libWeChatSDK│ │ ├── README.md│ │ ├── WXApi.h│ │ ├── WXApiObject.h│ │ ├── WechatAuthSDK.h│ │ └── libWeChatSDK.a├── LICENSE├── LJA│ ├── Assets│ └── Classes│ └── LJA.m├── LJA.podspec├── LJB│ ├── Assets│ └── Classes│ └── LJB.m├── LJB.podspec├── README.md└── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj複製程式碼
測試工程我也丟在7牛上面。下載測試即可
編譯執行。完美。我們又可以愉快的和swift第三方庫配合使用。
很多人會覺得麻煩的要死。首先每個公司多多少少都有歷史包袱,麻煩也要做,再者這是一次對基本功的補充,即便你們沒有用到,但是為了學習,這篇教程所做的也值得你嘗試一次。
剖析下動態庫 Framework 吧
上述解決了我們一開始遇到的問題。but既然動態庫和靜態庫壓根就不一回事,所以裡面還是有很多細節值得我們去了解的。
回過頭來看 Embened Framework
首先我們之前記得如果一個動態庫加在LinkedFrameworksand Libraies程式啟動就會報ImageNotFound,如果放在EmbededBinaries裡面就可以。這是為什麼呢。我們拿MacoView來看下兩種情況下可執行檔案的細節
其中@rpth 這個路徑表示的位置可以檢視Xcode中的連結路徑問題
這樣我們就知道了其實加在EmbededBinaries裡面的東西其實會被複制一份到xx.app裡面,所以這個名字起得還是不錯的直譯就是嵌入的框架
Why Swift does not Support Staic Libraies
造成這個的主要原因是Swift的執行時庫(不等同於OC的runtime概念),由於Swift的ABI不穩定,靜態庫會導致最終的目標程式中包含重複的執行庫,相關可以看下最後的參考文章SwiftInFlux#static-libraries。等到我們的SwiftABI穩定之後,我們的靜態庫支援可能就又會出現了。當然也可能不出Swift伴隨誕生的SPM(Swift,Package Manager),可能有更好的官方的包依賴管理工具。讓我們期待吧。
CocoaPods使用Use_framework!
既然加了Swift的第三方庫之後就需要在Podfile裡面加上use_framework! 那麼CocoaPods就會幫我們生成動態庫,但是奇怪的是,我們並沒有在主工程的embeded binaries看到這個動態庫,這又是什麼鬼。其實是CocoaPods使用指令碼幫我們加進去了。指令碼位置在主工程的build Phase下的Emded Pods frameworks
"${SRCROOT}/Pods/Target Support Files/Pods-LJA_Example/Pods-LJA_Example-frameworks.sh"複製程式碼
動態庫Framework的檔案結構
.├── Headers│ ├── LJWXSDK.h│ ├── WXApi.h│ ├── WXApiObject.h│ └── WechatAuthSDK.h├── Info.plist├── LJWXSDK├── Modules│ └── module.modulemap└── _CodeSignature └── CodeResources複製程式碼
Headers 一般是標頭檔案。非private裡面的標頭檔案都會在裡面info.plist 配置資訊,不深究Modules 這個資料夾裡有個module.modulemap檔案,後面在講解二進位制檔案,這就是上面提到的不帶main的二進位制檔案了,.o 的打包體_codeSignature簽名檔案 (蘋果爸爸的約束)more資原始檔。這裡暫時沒用到,所以沒有 ,但是這個也是個大坑更愉快的匯入檔案
@class,@protocol:不說了就是宣告一個類,並不匯入。#import <>, #import"":是加強版的#include<>,#include"" 防止重複匯入的。#import<> : 透過build setting裡面中的header Search Path裡面去找#import"" : 第一步先搜尋user Header search Path再搜尋 header search Path 。所以對我們的framework來說,CocoaPod幫我們加到了Header search Path目前2種匯入方式都是可以支援的。上面的匯入方式都帶了 某個framework的路徑"xx/xx.h" ,我們在開發自己主工程的時候會發現我們匯入主工程其他類是不需要匯入字首的。 這又是怎麼回事。
看下面的配置
目前的配置是non-recursive。如果把non去掉意思就是我可以遞迴的去查詢某些framework下面的標頭檔案了。 但是Xcode的效率肯定就會有影響。
還是不建議修改的好。
大家都知道iOS7之後多了@import,這又是什麼鬼。
簡單理解這個方式叫做Module匯入,好處就是使用了@import 之後不需要在project setting手動新增framework,系統會自動載入,而且效率更高。
最主要的是swift也只能這樣用。
匯入的時候系統會查詢如果有模組同名的檔案就會匯入這個檔案。如果沒有CocoaPods幫我們生成一個module-umbrela.hl檔案,然後就是匯入的這個檔案。
回過頭來看我們的framework的結構 裡面有個Modules資料夾,裡面有個檔案module.modulemap
framework module LJWXSDK { umbrella header "LJWXSDK.h" export * module * { export * }}複製程式碼
我們可以看到其實被暴露的header就是這個檔案,之前我在按照#import "/"的時候有個警告
而且按照@import 匯入的東西發現沒有匯入可用的標頭檔案就是因為並沒有在umbrella header的標頭檔案中加入其他標頭檔案。
加入之後我們就可以完美的使用@import ,並且#import"/" 也不會報warning
更多關於umbrella Header 參看文後參考
資源問題
首先我們來看常見的資原始檔: 主要分為圖片和其他類資源那麼載入圖片和載入其他資源都是怎麼做的?
1: [UIimage imageNamed:]
2: [NSbundle bundleForclass[XXX class]]
其實方式1去本質就是去mainBundle去拿資源,方式2從XXX所在的框架裡面去拿。
前面也說道framework只是資源的打包方式,本質上是有兩種的。
我們這個framework如果本質是靜態庫,那麼無需改變使用方式,資源最終都會打包到Main Bundle裡面
如果我們這個framework本質是動態庫,那麼我們的資源就發生了變化,資源就會被存放在framework裡面。所以我們需要使[NSbundle bundleForclass[XXX class]]。需要注意的是很多人為了簡單,下意
的使用self class傳遞,但是有可能這個self例項不在資源所屬的framework。所以會出現資源載入失敗。一定要謹慎使用。
總結:
動態庫的podspce不能依賴靜態庫