原文地址 ,此簡書只作備份,強烈推薦原文,畢竟格式比簡書好看,還清晰php
去年,鏈家網iOS端,以前因爲全部的業務端代碼都是混亂管理,形成開發有不少痛點
沒法單測
團隊成員提交代碼衝突機率大
CI配合效果差
功能性代碼多端沒法複用
單倉庫代碼量大
編譯時間長
等等痛點,領導和組內屢次溝通開始着手組件化開發,但願能改進這些開發中的痛點,成立組件化團隊。 組件化的方案大同小異,基礎性代碼封裝私有庫,業務組件交互交由中間件負責,項目依賴工具用 iOS項目事實上的標準CocoaPods
前期的基礎性組件拆分都較爲順利,從依賴樹的葉子節點開發是最合適的方案。 隨着組件抽離的愈來愈多,私有庫的依賴體系也愈來愈複雜,慢慢過渡到了業務組件。業務組件用了Swift的第三方組件,用了Swift庫的同窗都知道必須加上use_frameworks!
,這個標記是說Pod管理的依賴所有編譯爲動態庫
,而後呢咱們的不少組件又依賴了諸如百度地圖,微信分享等靜態庫
,因而我在執行pod install
報了一個沒有遇見過的錯誤html
[!] The 'Pods-LJA_Example' target has transitive dependencies that include static binaries: (/Users/nero/Desktop/Static_Dynamic/Componment/Example/Pods/libWeChatSDK/libWeChatSDK.a)
複製代碼
這就尷尬了,因而一陣瘋狂的搜索 google stackoverflow 等,然而並無什麼卵用,並且上面催得急,根本沒時間處理這些
小問題
業務重構是最主要的,以致於咱們的業務組件沒有作到獨立倉庫拆分。 直到最近終於找到了解決辦法:( 主要是本身的功力不夠深厚linux
首先靜態庫和動態庫都是以二進制提供代碼複用的代碼庫ios
.a
.dll(windows)
.dylib(mac)
so(linux)
靜態庫: 連接時會被完整的複製到可執行文件中,因此若是兩個程序都用了某個靜態庫,那麼每一個二進制可執行文件裏面其實都含有這份靜態庫的代碼 動態庫: 連接時不復制,在程序啓動後用dyld加載,而後再決議符號,因此理論上動態庫只用存在一份,好多個程序均可以動態連接到這個動態庫上面,達到了節省內存(不是磁盤是內存中只有一份動態庫),還有另一個好處,因爲動態庫並不綁定到可執行程序上,因此咱們想升級這個動態庫就很容易,windows 和linux上面通常插件和模塊機制都是這樣實現的。git
But咱們的蘋果爸爸在iOS平臺上規定不容許存在動態庫,而且全部的IPA都須要通過蘋果爸爸的私鑰加密後才能用,基本你用了動態庫也會由於簽名不對沒法加載,(越獄和非APP store除外)。因而就把開發者本身開發動態庫掐死在幻想中。 直到有一天,蘋果爸爸的iOS升級到了8,iOS出現了APP Extension
,swift
編程語言也誕生了,因爲iOS 主APP須要和Extension共享代碼,Swift語言的機制也只能有動態庫,因而蘋果爸爸尷尬了,不過這難不倒咱們的蘋果爸爸,畢竟我是爸爸,規則是我來定,我想怎樣就怎樣,因而提出了一個概念Embedded Framework
,這種動態庫容許APP
和 APP Extension
共享代碼,可是這份動態庫的生命被限定在一個APP進程內。簡單點能夠理解爲 被閹割的動態庫。程序員
若是你把某個本身開發的動態庫(系統的不算,畢竟蘋果是爸爸)放在了Linked Frameworks and Libraries
裏面,程序一啓動就會報Reason: Image Not Found
,你只能把它放在Embeded Binaries
裏面才能正常使用, 看圖: github
簡單點,說話的方式簡單點~~編程
上面的介紹貌似有點抽象啊 套用在美團技術分享大會上的話就是: 靜態庫: 一堆目標文件(.o/.obj)的打包體(並不是二進制文件) 動態庫: 一個沒有main函數的可執行文件json
這裏咱們來複習下C語言的基本功,編譯和連接 編譯:將咱們的源代碼文件編譯爲目標文件 連接:將咱們的各類目標文件加上一些第三方庫,和系統庫連接爲可執行文件。 因爲某個目標文件的符號(能夠理解爲變量,函數等)可能來自其餘目標文件,其實連接這一步最主要的操做就是 決議符號的地址。swift
因而連接加裝載就有了不一樣的狀況
而後組合起來就是2*2 = 4了
程序員的自我修養
一書既然有2種庫,那麼依賴關係又是2*2嘍
第一種 靜態庫互相依賴,這種狀況很是常見,製做靜態庫的時候只須要有被依賴的靜態庫頭文件在就能編譯出來。可是這就意味者你要收到告訴使用者你的依賴關係 幸運的是 CocoaPod
就是這樣作的 第二種動態庫依賴動態庫,兩個動態庫是相互隔離的具備隔離性
,可是製做的靜態庫的時候須要被依賴動態庫參與連接,可是具體的符號決議交給dyld
來作。 第三種,靜態庫依賴動態庫,也很常見,靜態庫製做的時候也須要動態庫參與連接,可是符號的決議交給dyld來作。 第四種,動態庫依賴靜態庫,這種狀況就有點特殊了。首先咱們設想動態庫編譯的時候須要靜態庫參與編譯,可是靜態庫交由dyld來作符號決議,but 這和咱們前面說的就矛盾了啊。靜態庫本質是一堆.o的打包體,首先並非二進制可執行文件,再者你沒法保證主程序把靜態庫參與連接共同生成二進制可執行文件。這就尷尬了。 怎麼辦? 目前的編譯器的解決辦法是,首先我沒法保證主程序是否包含靜態庫,再者靜態庫也沒法被dyld
加載,那麼我直接把你靜態庫的.o偷過來,共同組成一個新的二進制。也被稱作吸附性
那麼我有多份動態庫都依賴一樣的靜態庫,這就尷尬了,每一個動態庫爲了保證本身的正確性會把靜態庫吸附進來。而後兩個庫包含了一樣的靜態庫,因而問題就出現了。 看到這裏想必前面出現的錯誤你已經能猜出來了把~_~
後面再詳細解釋
先來個總結 可執⾏⽂件(主程序或者動態庫)在構建的連接階段
target-對於一個產物(app,.a ,.framework) project-一個項目包含多個target workspace:一個包含多個target schema: 指定了一個產物是按照何種的依賴關係,編譯-連接到最終的一個產物
這麼多年,Apple的博客和文檔也就告訴了咱們什麼是靜態庫 什麼是動態庫,如何製做等。可是並無給咱們提供一系列的依賴管理工具。因此CocoaPods成了事實上的標準。 一般CocoaPods管理的工程結構以下:
• CocoaPods
+ App.workspace
+ App.project
• Pods.project
• pod target => .a
複製代碼
那麼當咱們按下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舉例
2 按照微信的官方文檔。添加依賴庫(我是由於pod install巨慢 因此我直接拽進來了)
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" ]
then
INSTALL_DIR=${SRCROOT}/Products/${PROJECT_NAME}.framework
DEVICE_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework
SIMULATOR_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi
mkdir -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 = <<-DESC TODO: Add long description of the pod here. DESC
s.homepage = 'https://github.com/ValiantCat/LJWXSDK'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'ValiantCat' => '519224747@qq.com' }
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'
end
end
複製代碼
注意上面我是把二進制壓縮丟進了七牛的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第三方庫配合使用。 不少人可能會問 諸如百度地圖 微信這種sdk爲何官方不支持動態庫版(所說的都是embeded Framework),猜想是爲了兼容更低iOS7版本吧 不少人會以爲麻煩的要死。首先每一個公司多多少少都有歷史包袱,麻煩也要作,再者這是一次對基本功的補充,即使大家沒有用到,可是爲了學習,這篇教程所作的也值得你嘗試一次。
上述解決了咱們一開始遇到的問題。but既然動態庫和靜態庫壓根就不一回事,因此裏面仍是有不少細節值得咱們去了解的。
首先咱們以前記得若是一個動態庫加在LinkedFrameworksand Libraies
程序啓動就會報ImageNotFound,若是放在EmbededBinaries
裏面就能夠。這是爲何呢。咱們拿MacoView來看下兩種狀況下可執行文件的細節
其中@rpth這個路徑表示的位置能夠查看Xcode 中的連接路徑問題 這樣咱們就知道了其實加在EmbededBinaries
裏面的東西其實會被複制一份到xx.app裏面,因此這個名字起得仍是不錯的直譯就是嵌入的框架
形成這個的主要緣由是Swift的運行時庫(不等同於OC的runtime概念),因爲Swift的ABI不穩定,靜態庫會致使最終的目標程序中包含重複的運行庫,相關能夠看下最後的參考文章SwiftInFlux#static-libraries。等到咱們的SwiftABI穩定以後,咱們的靜態庫支持可能就又會出現了。固然也可能不出,Swift伴隨誕生的SPM(Swift,Package Manager),可能有更好的官方的
包依賴管理工具。讓咱們期待吧。
既然加了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"
複製代碼
.
├── Headers
│ ├── LJWXSDK.h
│ ├── WXApi.h
│ ├── WXApiObject.h
│ └── WechatAuthSDK.h
├── Info.plist
├── LJWXSDK
├── Modules
│ └── module.modulemap
└── _CodeSignature
└── CodeResources
複製代碼
不帶 main的二進制文件了
,.o的打包體@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> "xx/xx.h" ,咱們在開發本身主工程的時候會發現咱們導入主工程其餘類是不須要導入前綴的。 這又是怎麼回事。 看下面的配置
你們都知道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
,而且
#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。因此會出現資源加載 失敗。必定要謹慎使用。
程序員的自我修養,連接,裝載 和庫 iOS裏的動態庫和靜態庫 Systems Programming: What is the exact difference between Dynamic loading and dynamic linking? CocoaPods 都作了什麼? Dynamic Linking of Imported Functions in Mach-O OS裏的導入頭文件 iOS - Umbrella Header在framework中的應用 SwiftInFlux#static-libraries iOS裏的導入頭文件 iOS - Umbrella Header在framework中的應用 @import vs #import - iOS 7