Swift进阶-工程化实践(一)

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?
我们知道访问限制的几个关键字 openpublicinternalfileprivateprivate。A.swift想要访问到B.swift的类或函数判断访问限制的逻辑就在.swiftmodule里边。

番外:swift制作framework静态库

二、了解使用xcconfig

Xcode就是一个大型的shell环境,在这个环境中可以调各种工具 clang/swiftc等,而这个工具里边需要使用到很多的参数;这些参数有两种方式配置和管理:

  • 使用Xcode内置的 Build Settings(没有暴露的变量)
  • 使用xcconfig
1、工程中配置xcconfig

新建一个Framework工程

新建一个Framework工程

新建一个xcconfig文件,取名就叫Config


新建一个xcconfig文件1
新建一个xcconfig文件2

xcconfig帮助文档: https://help.apple.com/xcode/#/dev745c5c974
Xcode Build Settings 对应的 xcconfig 变量

  • 让这个xcconfig生效:

找到Project-> Info -> Configurations,给想要使用这个文件的Project或者是Targets去设置(需区分DebugRelease环境的)

  • 配置xcconfig里的shell参数:

比如说给我们的链接器做配置,在Build Settings里找到Other Linker Flags就能找到对应xcconfig对应参数的key:OTHER_LDFLAGS

于是乎就可以在xcconfig设置参数的value:

xcconfig设值

当我们把xcconfig的值设置好之后,Build Settings里的Other Linker Flags就会发生变化了:

Build Settings发生变化
  • 配置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):、、、

$(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.xcconfigBuild Settings定义的变量是一致的,那么会发生覆盖现象(上文已说明),可以通过$(inherited),让当前变量继承该变量的原有值:
Config.xcconfig

#include "Debug.xcconfig"
OTHER_LDFLAGS = $(inherited) -framework "CoreData"

Debug.xcconfig

OTHER_LDFLAGS = $(inherited) -framework "CoreText"

来看Build Settings并没有显示有-framework "CoreText"

Build Settings

其实我们已经导入成功了,来看看通过输出环境变量,再重新编译一下,看看这个变量是否有导入这三个:

设置命令
导入成功证明

2.引入变量,使用$()或者${}都可行

引入变量

3.条件变量,根据SDKArchConfiguration对设置进行条件化:

// 指定该 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  :打印module声
*/
$ swift -frontend -repl -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
进入repl解析器失败

这是因为这个命令里 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源码目录

把这个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解析器

我们是进入了REPL解析器,但我们没办法进入下一步,需要给定这个命令需要输出的.swiftmodule目录:

Products目录
// 先退出当前解析器: `$ quit`
$ quit 
/**
 -frontend:使用Swift前端工具
 -repl:进入解释器
 -sdk:环境使用的SDK
 -F:framework所在的路径
 -I:library所在的路径
 :print_module  :打印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
最终启动并进入了REPL解析器

2.输出.swiftmodule所代表的信息
进入了REPL解析器后,输出Framework工程生成的Framework.swiftmodule所代表的信息:

:print_module  // 这个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 
var SWIFT_TYPEDEFS: Int32 {
    get {
        return
    }
}
typealias char16_t = uint_least16_t
typealias char32_t = uint_least32_t
typealias swift_double2 = SIMD2
typealias swift_double3 = SIMD3
typealias swift_double4 = SIMD4
typealias swift_float2 = SIMD2
typealias swift_float3 = SIMD3
typealias swift_float4 = SIMD4
typealias swift_int2 = SIMD2
typealias swift_int3 = SIMD3
typealias swift_int4 = SIMD4
typealias swift_uint2 = SIMD2
typealias swift_uint3 = SIMD3
typealias swift_uint4 = SIMD4
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

.swiftinterfaceModule 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的文件

创建一个module.modulemap 的文件

声明一个名为Teacher的module;包含有Teacher.h头文件(header "Teacher.h");又因为Teacher.h导入了Foundation库,所以使用 export * 或者 export Foundation

module.modulemap

要把这个module生效,就要告诉给编译器,所以新建一个MyApp.Debug.config.xcconfig(项目.环境.作用.xcconfig),由于当前编译器是clang,这个这个xcconfig这样写。(ps:也可以直接在Build Settings上设置)

MyApp.Debug.config.xcconfig

当然依旧要使得这个xcconfig生效,还需要配置

配置xcconfig

最后,当然是使用这个Teacher module

综上使用了名为Teachermodule来管理MyModule模块下的 Teacher.h头文件。

还有一种导入module的方式是 #import , 但是会发现它会报错: '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 依旧是不可以的。
因为编译器clang在识别Headersmodule.modulemap 必须在 framework 目录(.framework结尾)下。
于是乎我把MyModule目录改成MyModule.framework

再把MyApp.Debug.config.xcconfig的路径映射改一下,此时我的ViewController就能够导入这个头文件了(#import #import 都可以了)

如果说我想用这样的方式导入Teacher类:@import Teacher.Teacher;

module.modulemap可以这样设置(ViewController里四种导入方式都可以用了:@import Teacher; @import Teacher.Teacher; #import #import

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


Framework-Swift.h

使用.modulemap的好处:.modulemap所管理的头文件预编译成pcm 预编译的二进制文件,在编译.m的时候就不用重复地去编译.h,大大提升编译效率和查找时间。

关于.modulemap相关demo可以自行下载

你可能感兴趣的:(Swift进阶-工程化实践(一))