swift制作framework静态库
swift工程化实践(一)
swift工程化实践(二)
一、认识.swiftmodule目录文件
当OC使用Swift的时候,是通过ProjectName-Swift.h
暴露给OC;
swift在使用OC的时候,是通过ProjectName.Bridging-Header.h
。
如下图:
但是在创建Swift Framework
(swift组件)的时候,就不能使用这个ProjectName.Bridging-Header.h
桥接文件了!
Swift与OC之间混编中出现的三个问题:
- Swift 没有头文件,只有 .swiftmodule 目录
- Swift Framework 不能使用 ProjectName.Bridging-Header.h
- Swift设计到编译参数
.swiftmodule概念:
在Xcode9之后,Swift
开始支持静态库。Swift
没有头文件概念,那外界要使用Swift
中用public
修饰的类和函数怎么办呢?
Swift
库引入了一个全新的文件 .swiftmodule
。
.swiftmodule
目录文件在哪里?
在我们写好的Framework编译后,类似我们工程中产生的可执行文件目录里
.swiftmodule包含了三种文件:
-
.swiftmodule
: 包含序列化过后的AST
(抽象语法树 Abstract Syntax Tree)以及SIL
(Swift中间语言 Swift Itermediate Language) -
.swiftdoc
:用户文档 -
.swiftinterface
: Module stability
疑问:为什么工程里A.swift访问到了.swiftmodule
就能访问到 B.swift?
我们知道访问限制的几个关键字 open
、public
、internal
、fileprivate
、private
。A.swift想要访问到B.swift的类或函数判断访问限制的逻辑就在.swiftmodule
里边。
二、了解使用xcconfig
Xcode就是一个大型的shell
环境,在这个环境中可以调各种工具 clang/swiftc
等,而这个工具里边需要使用到很多的参数;这些参数有两种方式配置和管理:
- 使用Xcode内置的
Build Settings
(没有暴露的变量) - 使用
xcconfig
1、工程中配置xcconfig
新建一个Framework工程
新建一个xcconfig文件,取名就叫Config
xcconfig帮助文档: https://help.apple.com/xcode/#/dev745c5c974
Xcode Build Settings 对应的 xcconfig 变量
- 让这个
xcconfig
生效:
找到Project
-> Info
-> Configurations
,给想要使用这个文件的Project或者是Targets去设置(需区分Debug
和Release
环境的)
- 配置
xcconfig
里的shell参数:
比如说给我们的链接器做配置,在Build Settings
里找到Other Linker Flags
就能找到对应xcconfig
对应参数的key:OTHER_LDFLAGS
于是乎就可以在xcconfig
设置参数的value:
当我们把xcconfig
的值设置好之后,Build Settings
里的Other Linker Flags
就会发生变化了:
-
配置shell参数的优先级问题:
比如我们Build Settings
里的Other Linker Flags
已经有一个值是-framework "CoreImage"
,那我们还往xcconfig
配置OTHER_LDFLAGS
的值是-framework "Foundation"
,这个时候就冲突啦,会发现Build Settings
的值并没有发生改变,依旧是原来的值,那我们这个配置肯定是有一个优先级的。
优先级由高到低:
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)
:、、、
- 导入其它xcconfig配置:
1.在创建 xcconfig
文件可以根据需求创建多个。也就意味着,可以通过 include
关键字导入:
#include "Debug.xcconfig"
2.通过绝对路径导入:
#include "/Users/xxx/Desktop/MyFramework/MyFramework/Debug.xcconfig"
3.通过相对路径,已${SRCROOT}路径为开始导入:
#include "MyFramework/Debug.xcconfig"
2、xcconfig中配置参数变量
变量定义按照OC命名规则,仅由大写字母、数字和下划线组成,原则上是大写实际上也可以不大写。字符串可以是"
号也可以是'
号。
变量有三种特殊情况:
1.xcconfig
与Build Settings
定义的变量是一致的,那么会发生覆盖现象(上文已说明),可以通过$(inherited)
,让当前变量继承该变量的原有值:
在Config.xcconfig
中
#include "Debug.xcconfig"
OTHER_LDFLAGS = $(inherited) -framework "CoreData"
在Debug.xcconfig
中
OTHER_LDFLAGS = $(inherited) -framework "CoreText"
来看Build Settings
并没有显示有-framework "CoreText"
其实我们已经导入成功了,来看看通过输出环境变量,再重新编译一下,看看这个变量是否有导入这三个:
2.引入变量,使用$()
或者${}
都可行
3.条件变量,根据SDK
、Arch
和Configuration
对设置进行条件化:
// 指定该 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内容
通过Swift REPL
来输出.swiftmodule
目录下的文件到底是什么。REPL
有一种方式可以输出.swiftmodule
到底是什么东西。
(Swift REPL
是Swift解析器,用来调试swift代码)
1.启动REPL
环境
打开终端输入
$ swift -frontend -repl
会报如下错误 error: unable to load standard library for target 'x86_64-apple-macosx12.0'
是因为在终端里使用的编译工具(命令里的 swift) 其实是Xcode内置的,我当前是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:环境使用的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内置的命令行工具,一般出现的这样的问题:首先要知道Xcode内置编译器都是由 LLVM
官方拉取分支,在这基础上做了一些添加、修改和屏蔽,所以导致上面报错我也办法通过Xcode去使用REPL,并且提示你去使用LLDB
的方式。 当你去使用该方式就会显得复杂了许多许多不好整。
解决方式:
自己通过LLVM
编译出自己的swift
编译器。
打开编译后的swift源码找到目录: swift-source -> build -> Ninja-RelWithDebInfoAssert+stdlib-DebugAssert -> swift-macosx-x86_64 -> bin -> swift和swiftc
把这个swift编译器拖拽到命令行,继续 启动REPL
环境(文件路径自行更改)
$ /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:环境使用的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版本功能对应不上。
那我可以把Framework工程匹配的SDK更换掉就可以了:往Config.xcconfig
添加参数,然后再重新编译生成新的Framework.framework
// Xcode内置的swift编译器,这里使用swiftc是因为比swift多了一些参数
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
环境
$ /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
内容如下:
@_exported import Foundation // 根据 Framework.h 导入的 #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里导入的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文件中导入了头文件,在其他swift文件中就能访问到了,所导入的都会在swiftmodule!
.swiftmodule
保存了编译器对swift代码分析之后的记录。
四、.swiftinterface
模拟模块不稳定:
1.新建一个工程取名App当前Xcode的swift编译器版本是5.5.1
2.再构建一个Framework,但是这个Framework是是在swift编译器版本5.2.4下生成的
3.把Framework导入到工程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
模块的稳定性,是swift5.1推出解决模块之间编译器版本兼容问题。这就意味着不同版本编译器构建的swift模块可以在同一个应用程序中一起使用。
实际上.swiftinterface
与.swiftmodule
是差不多的,.swiftinterface
多了一个解决兼容性的东西。
编译速度上.swiftinterface
会更慢一些;在编译期间没有模块兼容性问题的时候,优先使用.swiftmodule
。
五、Library Evolution
Library Evolution
:从swift5开始,库能够声明稳定的 ABI
(二进制通用接口
),允许库二进制文件替换为更新版本,而无需重新编译客户端程序。
接下来看看一个案例:
1.创建一个工程取名App
2.创建一个动态库Framework工程,保存在和App同一个目录下
3.把两个工程添加到一个xcworkspace里连调
打开App工程,在右上角选择 file
-> Save As Workspace
,取名叫Muti
,然后关闭App工程,再打开Muti.xcworkspace
将动态库Framework工程添加到xcworkspace
App工程关联编译
当编译App的时候,也会把Framework一起编译。
在App的main.swift里调用Framework里的Teacher的API
import Framework
print(Teacher().Lin)
此时运行打印的结果是 2 没有问题,因为编译App就会连同Framework一起编译。
此时如果把Framework的Library Evolution
关闭掉,把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打印的结果是一串数字不是2。
因为Swift是静态语言,它的底层数据结构在编译的时候就已经确定了,而Framework的Teacher结构更新了,并没有重新编译到App,导致在访问Lin的时候是通过偏移量和字节对齐去往内存找,结果找到的值不是原来的东西了。
苹果在swift5.0的时候推出了Library Evolution
,把部分代码从编译器确定了推到运行期,引入了swift运行时。
如果我们把Framework的Library Evolution
打开,就会打印出正常的2了
那开启了Library Evolution
又会引发另外一个问题:本身swift就是静态语音它的速度很快,如果把代码推到运行时的话,会导致性能的下降,于是为了解决这个问题,可以使用关键字@frozen
:
@frozen
public struct Teacher {
public init() {}
public var An = "11111112312312"
public var Lin = 2
}
@frozen
的作用:被@frozen标记的代码块冻住,保持静态性而不推到运行时。
六、.modulemap
module是什么?
module是用来管理一组头文件的
1.模块探究
新建一个OC
的项目取名为MyApp
,创建一个MyModule
模块目录,然后创建Teacher
类
在ViewController
里想使用Teacher
有两种方式导入
那如果我想要用Module
去管理MyModule
模块目录的头文件呢?
首先在MyModule
目录下创建一个module.modulemap
的文件
声明一个名为Teacher的module;包含有Teacher.h
头文件(header "Teacher.h");又因为Teacher.h
导入了Foundation库,所以使用 export *
或者 export Foundation
要把这个module生效,就要告诉给编译器,所以新建一个MyApp.Debug.config.xcconfig
(项目.环境.作用.xcconfig),由于当前编译器是clang
,这个这个xcconfig这样写。(ps:也可以直接在Build Settings上设置)
当然依旧要使得这个xcconfig
生效,还需要配置
最后,当然是使用这个Teacher module
综上使用了名为Teacher
的module
来管理MyModule
模块下的 Teacher.h
头文件。
还有一种导入module的方式是 #import <Teacher/Teacher.h>
, 但是会发现它会报错: 'Teacher/Teacher.h' file not found
因为这种书写方式是 framework
专属的书写方式。
但是这个module
写法有些弊端:如果我的MyModule
模块里有一百个的.h头文件,那我总不可能一个一个写 header "xxx.h"吧。
新建一个Teacher-umbrella.h
把要导入的头文件放到这里
这样就可以做到通过一个头文件去管理一组头文件
2.framework module
继续上面的例子,把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结尾)下。
于是乎我把MyModule
目录改成MyModule.framework
再把MyApp.Debug.config.xcconfig
的路径映射改一下,此时我的ViewController
就能够导入这个头文件了(#import <MyModule/Teacher.h>
和 #import <MyModule/Teacher-umbrella.h>
都可以了)
如果说我想用这样的方式导入Teacher类:@import Teacher.Teacher;
在module.modulemap
可以这样设置(ViewController里四种导入方式都可以用了:@import Teacher;
@import Teacher.Teacher;
#import <MyModule/Teacher.h>
#import <MyModule/Teacher-umbrella.h>
)
explicit
关键字在注释上也有说明。
番外番外:
像我们的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所管理的头文件预编译成pcm 预编译的二进制文件,在编译.m的时候就不用重复地去编译.h,大大提升编译效率和查找时间。
关于.modulemap相关demo可以自行下载