起因
理論功底
動態庫和靜態庫
介紹
靜態庫和動態庫的區別
舉個例子, iOS 項目中使用 Embeded Framework
靜態庫和動態庫如何構建和加載
靜態庫和動態庫依賴關系
Xcode 項目結構
iOS 依賴管理事實上的標準
解決問題
制作動態庫
剖析下動態庫 Framework 吧
回過頭來看 Embened Framework
Why Swift does not Support Staic Libraies
CocoaPods 使用 Use_framework!
動態庫 Framework 的文件結構
更愉快的導入文件
資源問題
參考
起因
去年,公司iOS端,之前由于所有的業務端代碼都是混亂管理,造成開發有很多痛點無法單測,團隊成員提交代碼沖突機率大,CI配合效果差,功能性代碼多端無法復用,單倉庫代碼量大,編譯時間長 等等痛點,領導和組內多次溝通開始著手組件化開發,希望能改進這些開發中的痛點,成立組件化團隊。
組件化的方案大同小異,基礎性代碼封裝私有庫,業務組件交互交由中間件負責,項目依賴工具用iOS項目事實上的標準CocoaPods
前期的基礎性組件拆分都較為順利,從依賴樹的葉子節點開發是最合適的方案。
隨著組件抽離的越來越多,私有庫的依賴體系也越來越復雜,慢慢過渡到了業務組件。業務組件用了Swift的第三方組件,用了Swift庫的同學都知道必須加上use_frameworks!,這個標記是說Pod管理的依賴全部編譯為動態庫,然后呢我們的很多組件又依賴了諸如百度地圖,微信分享等靜態庫,于是我在執行 pod install 報了一個沒有碰見過的錯誤。
1[!]?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 Linking
Static Loading + Dynamic Linking
Dynamic Loading + Dynamic Linking
~~Dynamic Loading + Static Linking~~
第一種是純靜態庫相關了
第二種就是靜態加載(啟動時),動態鏈接,鏈接時,動態庫參與鏈接,但是這時候只是給符號打了標記告訴我這個符號來自與動態庫,程序啟動時,iOS或者Mac OS操作系統的dyld自動load + link。
既然全部都是自動的。那么符號的調用方完全不知道你到底是源碼還是靜態庫,動態庫 。
第三種收到調用dlopen + performSelector通常iOS的APP不適用這里不討論
第四種,沒見過,個人也不是特別懂
有需求請參看文后的程序員的自我修養一書
靜態庫和動態庫依賴關系
既然有 2 種庫,那么依賴關系又是 2 * 2 嘍
libA.a dependency libB.a
UIKit.dylib dependency Foundation.dylib
libA.a dependency Foundation.dylib
MyXX.dylib dependency libA.a
第一種 靜態庫互相依賴,這種情況非常常見,制作靜態庫的時候只需要有被依賴的靜態庫頭文件在就能編譯出來。但是這就意味者你要收到告訴使用者你的依賴關系
幸運的是CocoaPod就是這樣做的
第二種動態庫依賴動態庫,兩個動態庫是相互隔離的具有隔離性,但是制作的靜態庫的時候需要被依賴動態庫參與鏈接,但是具體的符號決議交給dyld來做。
第三種,靜態庫依賴動態庫,也很常見,靜態庫制作的時候也需要動態庫參與鏈接,但是符號的決議交給dyld來做。
第四種,動態庫依賴靜態庫,這種情況就有點特殊了。首先我們設想動態庫編譯的時候需要靜態庫參與編譯,但是靜態庫交由dyld來做符號決議,but這和我們前面說的就矛盾了啊。靜態庫本質是一堆.o 的打包體,首先并不是二進制可執行文件,再者你無法保證主程序把靜態庫參與鏈接共同生成二進制可執行文件。這就尷尬了。
怎么辦?
目前的編譯器的解決辦法是,首先我無法保證主程序是否包含靜態庫,再者靜態庫也無法被dyld加載,那么我直接把你靜態庫的.o 偷過來,共同組成一個新的二進制。也被稱做吸附性
那么我有多份動態庫都依賴同樣的靜態庫,這就尷尬了,每個動態庫為了保證自己的正確性會把靜態庫吸附進來。然后兩個庫包含了同樣的靜態庫,于是問題就出現了。 看到這里想必前面出現的錯誤你已經能猜出來了把~_~
后面再詳細解釋
先來個總結
可執文件(主程序或者動態庫)在構建的鏈接階段
遇到靜態庫,吸附進來
遇到動態庫,打標記,彼此保持獨
Xcode 項目結構
target:對于一個產物(app,.a ,.framework)
project:一個項目包含多個 target
workspace: 一個包含多個 target
schema: 指定了一個產物是按照何種的依賴關系,編譯-鏈接到最終的一個產物
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 舉例
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文件夾,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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上面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#
#?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?seehttp://guides.cocoapods.org/syntax/podspec.html
#
Pod::?Spec.newdo|s|
??s.name?????????????=?'LJWXSDK'
??s.version??????????=?'0.1.0'
??s.summary??????????=?'A?short?description?of?LJWXSDK.'
??s.description??????=?<?'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 用來驗證之前我們的傳遞性依賴問題,
文件夾結構如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
.
├──?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版本吧
很多人會覺得麻煩的要死。首先每個公司多多少少都有歷史包袱,麻煩也要做,再者這是一次對基本功的補充,即便你們沒有用到,但是為了學習,這篇教程所做的也值得你嘗試一次。
剖析下動態庫 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
1"${SRCROOT}/Pods/Target?Support?Files/Pods-LJA_Example/Pods-LJA_Example-frameworks.sh"
動態庫Framework的文件結構
1
2
3
4
5
6
7
8
9
10
11
12
.
├──?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
1
2
3
4
5
6
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。所以會出現資源加載失敗。一定要謹慎使用。
抄自:?http://www.cocoachina.com/ios/20170427/19136.html
參考
Systems Programming: What is the exact difference between Dynamic loading and dynamic linking?
Dynamic Linking of Imported Functions in Mach-O
iOS - Umbrella Header 在 framework 中的應用