swift制作framework靜態(tài)庫
swift工程化實踐(一)
swift工程化實踐(二)
一、認識.swiftmodule目錄文件
當OC使用Swift的時候秘症,是通過ProjectName-Swift.h
暴露給OC无午;
swift在使用OC的時候蕉堰,是通過ProjectName.Bridging-Header.h
姓言。
如下圖:
但是在創(chuàng)建Swift Framework
(swift組件)的時候莽龟,就不能使用這個ProjectName.Bridging-Header.h
橋接文件了!
Swift與OC之間混編中出現(xiàn)的三個問題:
- Swift 沒有頭文件地梨,只有 .swiftmodule 目錄
- Swift Framework 不能使用 ProjectName.Bridging-Header.h
- Swift設(shè)計到編譯參數(shù)
.swiftmodule概念:
在Xcode9之后菊卷,Swift
開始支持靜態(tài)庫。Swift
沒有頭文件概念宝剖,那外界要使用Swift
中用public
修飾的類和函數(shù)怎么辦呢洁闰?
Swift
庫引入了一個全新的文件 .swiftmodule
。
.swiftmodule
目錄文件在哪里万细?
在我們寫好的Framework編譯后扑眉,類似我們工程中產(chǎn)生的可執(zhí)行文件目錄里
.swiftmodule包含了三種文件:
-
.swiftmodule
: 包含序列化過后的AST
(抽象語法樹 Abstract Syntax Tree)以及SIL
(Swift中間語言 Swift Itermediate Language) -
.swiftdoc
:用戶文檔 -
.swiftinterface
: Module stability
疑問:為什么工程里A.swift訪問到了.swiftmodule
就能訪問到 B.swift?
我們知道訪問限制的幾個關(guān)鍵字 open
雅镊、public
襟雷、internal
刃滓、fileprivate
仁烹、private
。A.swift想要訪問到B.swift的類或函數(shù)判斷訪問限制的邏輯就在.swiftmodule
里邊咧虎。
二卓缰、了解使用xcconfig
Xcode就是一個大型的shell
環(huán)境,在這個環(huán)境中可以調(diào)各種工具 clang/swiftc
等砰诵,而這個工具里邊需要使用到很多的參數(shù)征唬;這些參數(shù)有兩種方式配置和管理:
- 使用Xcode內(nèi)置的
Build Settings
(沒有暴露的變量) - 使用
xcconfig
1、工程中配置xcconfig
新建一個Framework工程
新建一個xcconfig文件茁彭,取名就叫Config
xcconfig幫助文檔: https://help.apple.com/xcode/#/dev745c5c974
Xcode Build Settings 對應(yīng)的 xcconfig 變量
- 讓這個
xcconfig
生效:
找到Project
-> Info
-> Configurations
总寒,給想要使用這個文件的Project或者是Targets去設(shè)置(需區(qū)分Debug
和Release
環(huán)境的)
- 配置
xcconfig
里的shell參數(shù):
比如說給我們的鏈接器做配置,在Build Settings
里找到Other Linker Flags
就能找到對應(yīng)xcconfig
對應(yīng)參數(shù)的key:OTHER_LDFLAGS
于是乎就可以在xcconfig
設(shè)置參數(shù)的value:
當我們把xcconfig
的值設(shè)置好之后理肺,Build Settings
里的Other Linker Flags
就會發(fā)生變化了:
-
配置shell參數(shù)的優(yōu)先級問題:
比如我們Build Settings
里的Other Linker Flags
已經(jīng)有一個值是-framework "CoreImage"
摄闸,那我們還往xcconfig
配置OTHER_LDFLAGS
的值是-framework "Foundation"
,這個時候就沖突啦妹萨,會發(fā)現(xiàn)Build Settings
的值并沒有發(fā)生改變年枕,依舊是原來的值,那我們這個配置肯定是有一個優(yōu)先級的乎完。
優(yōu)先級由高到低:
1.手動配置 TARGETS 的 Build Settings熏兄;
2.TARGETS 中配置了 xcconfig 文件;
3.手動配置 PROJECT 的 Build Settings;
4.PROJECT 中配置了 xcconfig 文件摩桶。
$(inherited)
的作用是配置繼承
如果我們想要-framework "CoreImage"
和-framework "Foundation"
桥状,就可以在Build Settings
里的 Other Linker Flags
最前面添加$(inherited)
:、硝清、岛宦、
- 導(dǎo)入其它xcconfig配置:
1.在創(chuàng)建 xcconfig
文件可以根據(jù)需求創(chuàng)建多個。也就意味著耍缴,可以通過 include
關(guān)鍵字導(dǎo)入:
#include "Debug.xcconfig"
2.通過絕對路徑導(dǎo)入:
#include "/Users/xxx/Desktop/MyFramework/MyFramework/Debug.xcconfig"
3.通過相對路徑砾肺,已${SRCROOT}路徑為開始導(dǎo)入:
#include "MyFramework/Debug.xcconfig"
2、xcconfig中配置參數(shù)變量
變量定義按照OC命名規(guī)則防嗡,僅由大寫字母变汪、數(shù)字和下劃線組成,原則上是大寫實際上也可以不大寫蚁趁。字符串可以是"
號也可以是'
號裙盾。
變量有三種特殊情況:
1.xcconfig
與Build Settings
定義的變量是一致的,那么會發(fā)生覆蓋現(xiàn)象(上文已說明)他嫡,可以通過$(inherited)
番官,讓當前變量繼承該變量的原有值:
在Config.xcconfig
中
#include "Debug.xcconfig"
OTHER_LDFLAGS = $(inherited) -framework "CoreData"
在Debug.xcconfig
中
OTHER_LDFLAGS = $(inherited) -framework "CoreText"
來看Build Settings
并沒有顯示有-framework "CoreText"
其實我們已經(jīng)導(dǎo)入成功了洽瞬,來看看通過輸出環(huán)境變量贞间,再重新編譯一下忽肛,看看這個變量是否有導(dǎo)入這三個:
2.引入變量脖祈,使用$()
或者${}
都可行
3.條件變量呻右,根據(jù)SDK
唯咬、Arch
和Configuration
對設(shè)置進行條件化:
// 指定該 Configuration 是 Debug模式下生效
// 指定該 SDK 是 模擬器 還有 iphoneos* 或 macos* 等
// 指定生效框架位 x86_64
OTHER_LDFLAGS[config=Debug][sdk=iphonesimulator*][arch=x86_64] = $(inherited) -framework "AFNetworking"
注意:在Xcode11.4版本之后贮缕,可以使用 default
來指定變量空時的默認值:
$(BUILD_SETTING_NAME:default=value)
三惜浅、輸出.swiftmodule內(nèi)容
通過Swift REPL
來輸出.swiftmodule
目錄下的文件到底是什么染乌。REPL
有一種方式可以輸出.swiftmodule
到底是什么東西山孔。
(Swift REPL
是Swift解析器,用來調(diào)試swift代碼)
1.啟動REPL
環(huán)境
打開終端輸入
$ swift -frontend -repl
會報如下錯誤 error: unable to load standard library for target 'x86_64-apple-macosx12.0'
是因為在終端里使用的編譯工具(命令里的 swift) 其實是Xcode內(nèi)置的荷憋,我當前是Xcode12版本台颠,就需要這個x86_64-apple-macosx12.0
這個SDK。所以我們找一下這個SDK在哪:
$ xcrun -show-sdk-path
會輸出SDK的路徑:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
于是得到這個命令勒庄,滿心歡喜地嘗試
/**
-frontend:使用Swift前端工具
-repl:進入解釋器
-sdk:環(huán)境使用的SDK
-F:framework所在的路徑
-I:library所在的路徑
:print_module <name> :打印module聲
*/
$ swift -frontend -repl -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
這是因為這個命令里 swift 是Xcode內(nèi)置的命令行工具串前,一般出現(xiàn)的這樣的問題:首先要知道Xcode內(nèi)置編譯器都是由 LLVM
官方拉取分支,在這基礎(chǔ)上做了一些添加锅铅、修改和屏蔽酪呻,所以導(dǎo)致上面報錯我也辦法通過Xcode去使用REPL,并且提示你去使用LLDB
的方式盐须。 當你去使用該方式就會顯得復(fù)雜了許多許多不好整玩荠。
解決方式:
自己通過LLVM
編譯出自己的swift
編譯器。
打開編譯后的swift源碼找到目錄: swift-source -> build -> Ninja-RelWithDebInfoAssert+stdlib-DebugAssert -> swift-macosx-x86_64 -> bin -> swift和swiftc
把這個swift編譯器拖拽到命令行,繼續(xù) 啟動REPL
環(huán)境(文件路徑自行更改)
$ /Users/xxx/Desktop/swift-source/build/Ninja-RelWithDebInfoAssert+stdlib-DebugAssert/swift-macosx-x86_64/bin/swift -frontend -repl -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
我們是進入了REPL解析器阶冈,但我們沒辦法進入下一步闷尿,需要給定這個命令需要輸出的.swiftmodule
目錄:
// 先退出當前解析器: `$ quit`
$ quit
/**
-frontend:使用Swift前端工具
-repl:進入解釋器
-sdk:環(huán)境使用的SDK
-F:framework所在的路徑
-I:library所在的路徑
:print_module <name> :打印module聲
*/
$ /Users/xxx/Desktop/swift-source/build/Ninja-RelWithDebInfoAssert+stdlib-DebugAssert/swift-macosx-x86_64/bin/swift -frontend -repl -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -F /Users/xxx/Desktop/Framework/Products
2.輸出.swiftmodule
所代表的信息
進入了REPL解析器后,輸出Framework工程生成的Framework.swiftmodule
所代表的信息:
:print_module <name> // 這個name是module的名稱女坑,這里的我是Framework
于是乎又報了好多錯填具,這是因為我剛才通過LLVM編譯出來的swift編譯器與當前的SDK版本功能對應(yīng)不上。
那我可以把Framework工程匹配的SDK更換掉就可以了:往Config.xcconfig
添加參數(shù)匆骗,然后再重新編譯生成新的Framework.framework
// Xcode內(nèi)置的swift編譯器劳景,這里使用swiftc是因為比swift多了一些參數(shù)
SWIFT_EXEC = /Users/xxx/Desktop/swift-source/build/Ninja-RelWithDebInfoAssert+stdlib-DebugAssert/swift-macosx-x86_64/bin/swiftc
// 更換SDK版本
SDKROOT = /Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk
再打開命令行啟動REPL
環(huán)境
$ /Users/xxx/Desktop/swift-source/build/Ninja-RelWithDebInfoAssert+stdlib-DebugAssert/swift-macosx-x86_64/bin/swift -frontend -repl -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -F /Users/xxx/Desktop/Framework/Products
輸出.swiftmodule所代表的信息
:print_module Framework
最終輸出Framework.swiftmodule
內(nèi)容如下:
@_exported import Foundation // 根據(jù) Framework.h 導(dǎo)入的 #import <Foundation/Foundation.h>
var SWIFT_TYPEDEFS: Int32 {
get {
return
}
}
typealias char16_t = uint_least16_t
typealias char32_t = uint_least32_t
typealias swift_double2 = SIMD2<Double>
typealias swift_double3 = SIMD3<Double>
typealias swift_double4 = SIMD4<Double>
typealias swift_float2 = SIMD2<Float>
typealias swift_float3 = SIMD3<Float>
typealias swift_float4 = SIMD4<Float>
typealias swift_int2 = SIMD2<Int32>
typealias swift_int3 = SIMD3<Int32>
typealias swift_int4 = SIMD4<Int32>
typealias swift_uint2 = SIMD2<UInt32>
typealias swift_uint3 = SIMD3<UInt32>
typealias swift_uint4 = SIMD4<UInt32>
import Foundation // 這是在Teacher.swift里導(dǎo)入的Foundation,所以別的.swift文件也能訪問到
@_exported import Framework
import SwiftOnoneSupport
struct Teacher {
init()
var An: Int
var Lin: String
}
這是我的Teacher.swift的聲明:
import Foundation
public struct Teacher {
public init() {}
public var An = 1
public var Lin = "工程化"
}
這就解釋我們在一個swift文件中導(dǎo)入了頭文件碉就,在其他swift文件中就能訪問到了盟广,所導(dǎo)入的都會在swiftmodule!
.swiftmodule
保存了編譯器對swift代碼分析之后的記錄。
四瓮钥、.swiftinterface
模擬模塊不穩(wěn)定:
1.新建一個工程取名App當前Xcode的swift編譯器版本是5.5.1
2.再構(gòu)建一個Framework筋量,但是這個Framework是是在swift編譯器版本5.2.4下生成的
3.把Framework導(dǎo)入到工程A,并使用Framework里的API碉熄,編譯能成功桨武!
4.如果把Framework里面的module里包含的.swiftinterface移除
,再編譯的話就會報如下錯誤:
error: Module compiled with 5.2.4 cannot be imported by the Swift 5.5.1 compiler /Users/xxx/Desktop/App/Framework.framework/Modules/Framework.swiftmodule/x86_64-apple-macos.swiftmodule
.swiftinterface
:Module stability
模塊的穩(wěn)定性锈津,是swift5.1推出解決模塊之間編譯器版本兼容問題呀酸。這就意味著不同版本編譯器構(gòu)建的swift模塊可以在同一個應(yīng)用程序中一起使用。
實際上.swiftinterface
與.swiftmodule
是差不多的一姿,.swiftinterface
多了一個解決兼容性的東西七咧。
編譯速度上.swiftinterface
會更慢一些;在編譯期間沒有模塊兼容性問題的時候叮叹,優(yōu)先使用.swiftmodule
。
五爆存、Library Evolution
Library Evolution
:從swift5開始蛉顽,庫能夠聲明穩(wěn)定的 ABI
(二進制通用接口
),允許庫二進制文件替換為更新版本先较,而無需重新編譯客戶端程序携冤。
接下來看看一個案例:
1.創(chuàng)建一個工程取名App
2.創(chuàng)建一個動態(tài)庫Framework工程,保存在和App同一個目錄下
3.把兩個工程添加到一個xcworkspace里連調(diào)
打開App工程闲勺,在右上角選擇 file
-> Save As Workspace
曾棕,取名叫Muti
,然后關(guān)閉App工程菜循,再打開Muti.xcworkspace
將動態(tài)庫Framework工程添加到xcworkspace
App工程關(guān)聯(lián)編譯
當編譯App的時候翘地,也會把Framework一起編譯。
在App的main.swift里調(diào)用Framework里的Teacher的API
import Framework
print(Teacher().Lin)
此時運行打印的結(jié)果是 2 沒有問題,因為編譯App就會連同F(xiàn)ramework一起編譯衙耕。
此時如果把Framework的Library Evolution
關(guān)閉掉昧穿,把Teacher的An屬性注釋掉,重新單獨編譯Framework
public struct Teacher {
public init() {}
// public var An = "11111112312312"
public var Lin = 2
}
編譯完Framework后橙喘,把Teacher里的Teacher的An屬性打開时鸵,然后選擇App進行 Run Without Building
Run Without Building
的作用是不重新編譯App。此時main.swift打印的結(jié)果是一串數(shù)字不是2厅瞎。
因為Swift是靜態(tài)語言饰潜,它的底層數(shù)據(jù)結(jié)構(gòu)在編譯的時候就已經(jīng)確定了,而Framework的Teacher結(jié)構(gòu)更新了和簸,并沒有重新編譯到App囊拜,導(dǎo)致在訪問Lin的時候是通過偏移量和字節(jié)對齊去往內(nèi)存找,結(jié)果找到的值不是原來的東西了比搭。
蘋果在swift5.0的時候推出了Library Evolution
冠跷,把部分代碼從編譯器確定了推到運行期,引入了swift運行時身诺。
如果我們把Framework的Library Evolution
打開蜜托,就會打印出正常的2了
那開啟了Library Evolution
又會引發(fā)另外一個問題:本身swift就是靜態(tài)語音它的速度很快,如果把代碼推到運行時的話霉赡,會導(dǎo)致性能的下降橄务,于是為了解決這個問題,可以使用關(guān)鍵字@frozen
:
@frozen
public struct Teacher {
public init() {}
public var An = "11111112312312"
public var Lin = 2
}
@frozen
的作用:被@frozen標記的代碼塊凍住穴亏,保持靜態(tài)性而不推到運行時蜂挪。
六、.modulemap
module是什么嗓化?
module是用來管理一組頭文件的
1.模塊探究
新建一個OC
的項目取名為MyApp
棠涮,創(chuàng)建一個MyModule
模塊目錄,然后創(chuàng)建Teacher
類
在ViewController
里想使用Teacher
有兩種方式導(dǎo)入
那如果我想要用Module
去管理MyModule
模塊目錄的頭文件呢刺覆?
首先在MyModule
目錄下創(chuàng)建一個module.modulemap
的文件
聲明一個名為Teacher的module严肪;包含有Teacher.h
頭文件(header "Teacher.h");又因為Teacher.h
導(dǎo)入了Foundation庫谦屑,所以使用 export *
或者 export Foundation
要把這個module生效驳糯,就要告訴給編譯器,所以新建一個MyApp.Debug.config.xcconfig
(項目.環(huán)境.作用.xcconfig),由于當前編譯器是clang
氢橙,這個這個xcconfig這樣寫酝枢。(ps:也可以直接在Build Settings上設(shè)置)
當然依舊要使得這個xcconfig
生效,還需要配置
最后悍手,當然是使用這個Teacher module
綜上使用了名為Teacher
的module
來管理MyModule
模塊下的 Teacher.h
頭文件帘睦。
還有一種導(dǎo)入module的方式是 #import <Teacher/Teacher.h>
, 但是會發(fā)現(xiàn)它會報錯: 'Teacher/Teacher.h' file not found
因為這種書寫方式是 framework
專屬的書寫方式袍患。
但是這個module
寫法有些弊端:如果我的MyModule
模塊里有一百個的.h頭文件,那我總不可能一個一個寫 header "xxx.h"吧官脓。
新建一個Teacher-umbrella.h
把要導(dǎo)入的頭文件放到這里
這樣就可以做到通過一個頭文件去管理一組頭文件
2.framework module
繼續(xù)上面的例子协怒,把module
聲明成framework module
就會報錯: Umbrella header 'Teacher-umbrella.h' not found
framework module Teacher {
// umbrella -> 一組
umbrella header "Teacher-umbrella.h"
// Teacher.h -> Foundation
export *
}
那是因為 framework
是特殊的module
,它包含了Header+.a+簽名+資源+Module卑笨,它更像是一個文件夾
前面看過.framework包含的東西了孕暇,所有的.h文件都放在Headers目錄下
于是我嘗試新建一個Headers
目錄,把頭文件也放到這下面赤兴,這樣就編譯通過了妖滔!
既然聲明framework module
成功了,但是在ViewController里使用 #import <Teacher/Teacher.h>
依舊是不可以的桶良。
因為編譯器clang
在識別Headers
和module.modulemap
必須在 framework
目錄(.framework結(jié)尾)下座舍。
于是乎我把MyModule
目錄改成MyModule.framework
再把MyApp.Debug.config.xcconfig
的路徑映射改一下,此時我的ViewController
就能夠?qū)脒@個頭文件了(#import <MyModule/Teacher.h>
和 #import <MyModule/Teacher-umbrella.h>
都可以了)
如果說我想用這樣的方式導(dǎo)入Teacher類:@import Teacher.Teacher;
在module.modulemap
可以這樣設(shè)置(ViewController里四種導(dǎo)入方式都可以用了:@import Teacher;
@import Teacher.Teacher;
#import <MyModule/Teacher.h>
#import <MyModule/Teacher-umbrella.h>
)
explicit
關(guān)鍵字在注釋上也有說明陨帆。
番外番外:
像我們的Swift
生成的.framework
里面的.modulemap文件(以第五部分的Framework.framework為例子)
framework module Framework {
umbrella header "Framework.h"
export *
module * { export * }
}
// 子Framework.Self -> Framework-Swift.h
// requires objc: 使用Framework.Swift的源碼文件是一個OC文件的時候
module Framework.Swift {
header "Framework-Swift.h"
requires objc
}
來看看 Framework-Swift.h 的源碼是OC
使用.modulemap
的好處:.modulemap所管理的頭文件預(yù)編譯成pcm 預(yù)編譯的二進制文件曲秉,在編譯.m的時候就不用重復(fù)地去編譯.h,大大提升編譯效率和查找時間疲牵。
關(guān)于.modulemap相關(guān)demo可以自行下載