美文网首页iOS成长之路
Swift:编译流程

Swift:编译流程

作者: 时光啊混蛋_97boy | 来源:发表于2021-12-18 11:44 被阅读0次

    原创:知识点总结性文章
    创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 一、LLVM
    • 二、编译流程
    • 三、 Package Manager

    一、LLVM

    在深入 SIL 之前,先简单介绍 LLVM,经典的 LLVM 三段式架构如下图所示,分为前端(Frontend),优化器(Optimizer)和后端(Backend)。当需要支持新语言时只需实现前端部分,需要支持新的架构只需实现后端部分,而前后端的连接枢纽就是 IR(Intermediate Representation),IR 独立于编程语言和机器架构,故 IR 阶段的优化可以做到抽象而通用。

    Frontend

    前端经过词法分析(Lexical Analysis),语法分析(Syntactic Analysis)生成 AST,语义分析(Semantic Analysis),中间代码生成(Intermediate Code Generation)等步骤,生成 IR。

    IR

    IR 是 LLVM 前后端的桥接语言,其主要有三种格式,这三种格式完全等价:

    • 可读的格式,以.ll 结尾
    • Bitcode 格式,以.bc 结尾
    • 运行时在内存中的格式
    Optimizer

    IR经过优化器进行优化,优化器会调用执行各类 Pass。所谓 Pass,就是遍历一遍 IR,在进行针对性的处理的代码。LLVM 内置了若干 Pass,开发者也可自定义 Pass 实现特定功能,比如插桩统计函数运行耗时等。

    Xcode Optimization Level

    Xcode - Build Setting - Apple Clang - Code Generation - Optimization Level中,可以选定优化级别,-O0表示无优化,即不调用任何优化 Pass。其他优化级别则调用执行对应的 Pass

    Backend

    后端将 IR 转成生成相应 CPU 架构的机器码。


    二、Swift 编译流程

    Swiftc

    不同于 OC 使用 clang 作为编译器前端,Swift 自定义了编译器前端 swiftc,如下图所示。这里就体现出来 LLVM 三段式的好处了,支持新语言只需实现编译器前端即可。对比clang,Swift 新增了对 SIL(Swift Intermediate Language)的处理过程。SIL 是 Swift 引入的新的高级中间语言,用以实现更高级别的优化。

    Swift 编译流程

    Swift 源码经过词法分析,语法分析和语义分析生成 AST。SILGen 获取 AST 后生成 SIL,此时的 SIL 称为 Raw SIL。在经过分析和优化,生成 Canonical SIL。最后,IRGen 再将 Canonical SIL 转化为 LLVM IR 交给优化器和后端处理。


    三、 Package Manager

    前言

    Swift Package Manager是 Apple 为了弥补当前 iOS 开发中缺少官方组件库管理工具的产物。相较于其他组件管理控件,他的定义文件更加轻松易懂,使用起来也很 Magic,只需将源码放入对应的文件夹内,Xcode 就会自动生成工程文件,并生成编译目标产物所需要的相关配置。同时,SPM 与 Cocoapods 相互兼容,可以在特性上提供互补。这篇文章将主要介绍该组件管理器的现状,常见使用方法,和将来的一些思考。

    本人是十分喜欢Swift Package 的,本地集成方便快捷,也给我很大的权力让我所想落实到几乎不可能落实的上游仓库。配合 Swift Access Control,例如 module 内可访问的 internal属性,很大程度上解决了写 App 后台的时候被 UI 意外调用造成的 crash,弥补上 Swift 没有class-private 访问控制关键字的遗憾。调试可以直接打到代码上,速度也很快。如果能为 Package 提供 .patch的扩展文件,再配合优化后的远端仓库,这将很有可能取代臃肿的 Cocoapod。pod 会修改编译目标的 xcconfig,而 Swift Package 通过提供libraryworkspace 的集成方式,侵入性非常低。最后,Swift Package 的多平台编译的能力也非常好,UIKit 一次编写即可适配 iOS/iPadOS/tvOS/watchOS,编译配置 CI 只需要调用 xcodebuild 即可自动解析,如有缺失自动拉取,省时省力。个人项目我可能不会再碰 Cocoapods。

    开源组件使用情况

    查看当前开源组件的 SPM 接入供应情况,不难发现几乎全部还在维护的框架都支持使用这种方式集成。大到微软的 APM SDK,小到界面 UI 组件,均有良好的兼容支持。下面列举一些可能会在后续开发中用到的组件。资源选自 https://github.com/ivanvorobei/awesome-ios。检测规则为是否在仓库主目录下存在 Package.swift 文件。统计中,56%左右的框架已经适配了 SPM 接入,且已经开始出现如 MarkdownUI 等框架仅适配 SPM 的情况。

    优势
    • 简化的定义流程:将文件放入约定的目录内即可一键打包。
    • 简化的 SPM 版本管理:Xcode 会根据定义文件首行说明自动查找兼容的解决方案。
    • 简化的上手流程:不需要安装工具,也不需要命令行安装组件。
    • 良好的持续集成能力:在完成项目配置以后,xcodebuild 无缝衔接,自动拉仓。
    • 良好的兼容性:可与现有的大多数组件管理方案混用。
    • 良好的调试能力:断点快狠准。
    缺点
    • 文档难找。
    • 使用远端仓库对网络要求非常高。
    创建组件

    创建组件可以在 Xcode 中选择Swift Package,也可以在命令行中写入 swift package init。命令行创建会将当前目录名称用作包名。

    Creating library package: Desktop
    Creating Package.swift
    Creating README.md
    Creating .gitignore
    Creating Sources/
    Creating Sources/Desktop/Desktop.swift
    Creating Tests/
    Creating Tests/DesktopTests/
    Creating Tests/DesktopTests/DesktopTests.swift
    
    定义组件
    // swift-tools-version:5.5
    import PackageDescription
    
    let package = Package(
      name: "MyLibrary",
      products: [
        .library(name: "MyLibrary", targets: ["MyLibrary"]),
      ],
      dependencies: [ ],
      targets: [
        .target(name: "MyLibrary", dependencies: []),
        .testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),
      ]
    )
    

    请勿忽略swift-tools-version:5.5行,当打包编译出现工具链版本不匹配、 SDK 版本、系统 API 最低版本等问题时需要首先到这里排查可能存在的问题。

    Targets

    先看 targets,定义是 A target can define a module or a test suite. 翻译来说,就是一个 target 对应一个 clang module 或者 一个测试目标。一个 target 内只允许使用一类语言,比方说 Swift 或者 Objective-C/C/CPP。此处的name 只对当前 package 可见,可以填写在任意一个 dependencies 内。Target 支持 binary Target,可使用 XCFramwork.a .so 等二进制。

    Products

    再看 products,定义是 Products define the executables and libraries a package produces, and make them visible to other packages. 一个 product 可以包含多个 target,他们会被编译成产物提供给项目。如果其他项目依赖当前的 Swift Package,此处的 name 可以填写入其他Package的依赖需求内,一般对内不可用。最后来看一下product 的几种类型。一般来说,常见的 .librarytype 包含 .static(默认) 和 .dynamic。除开.library还有 .executable可选,用于编译测试用二进制和 macOS 命令行工具。

    .library(name: "MorphingLabel", targets: ["MorphingLabel"]),
    .library(name: "MorphingLabelDynamic", type: .dynamic, targets: ["MorphingLabel"]),
    
    .executable(name: "appdecrypt", targets: ["appdecrypt"])
    
    资源文件

    Swift Package 需要对每一个文件指明用途。代码文件会自动识别并编译打包,资源文件需要指定和说明。Swift Package 会为每一个 Package 生成一个 module 扩展,以便直接调用。使用命令行将项目文件 Package.swift 转换成 xcproj 则不会生成该模版定义文件。以下定义会在 Bundle 类内生成.module属性专门用于获取 Particles 文件夹内的资源。

    .target(
        name: "MorphingLabel",
            exclude: ["Info.plist", "tvOS-Info.plist"],
            resources: [ .process("Particles") ] // <-- 资源文件
    ),
    
    目录结构
    • Swift Package 推荐使用原生目录结构,不推荐自定义 Path
    • Swift Package 导出头文件有规定的位置,在当前 Source Path 内创建 include 会自动导出。
    • Swift Package 需要对每一个资源文件/文件夹显示声明,对通配符的适配存在 Bug。
    • 当需要特定的文件目录组织的时候可以使用 符号连接 来链接目标文件。

    总体来说Swift Package中一个Target对应一个 name,而 项目根目录/Sources/name会作为当前Target的工作搜索路径。

    ├── Package.swift        # 定义文件
    ├── README.md            # 可忽略
    ├── Sources              # 此文件夹内全部文件都需要定义 不然会报错
    │  └── demo             # target demo 的默认目录
    │    ├── Particles.     # 在 target 内声明为资源文件
    │    │  └── fire.png   # 会自动打包成 bundle 拷贝并传递
    │    ├── demo.swift     # target demo 的项目源代码
    │    └── include        # 导出头文件
    │      └── export.h     # 头文件
    
    # 在 Sources / target name 厘头的资源文件可在 Package.swift 内定义。
    # 需要在 target 内添加 resources: [ .process("Particles") ]
    
    XCFramework

    关于编译产物,基础的 Swift Package 可以生成静态库、动态库,在这以后可以手动打包成 XCFrameworkSPM 的打包工作流对XCFramework 非常友好,可以参考下面这个脚本。

    xcodebuild -create-xcframework \
            -framework "$BUILD_FOLDER/iOS.xcarchive/Products/Library/Frameworks/MorphingLabel.framework" \
            -framework "$BUILD_FOLDER/tvOS.xcarchive/Products/Library/Frameworks/MorphingLabel.framework" \
            -framework "$BUILD_FOLDER/Simulator.xcarchive/Products/Library/Frameworks/MorphingLabel.framework" \
            -framework "$BUILD_FOLDER/tvOSSimulator.xcarchive/Products/Library/Frameworks/MorphingLabel.framework" \
            -output Build/LTMorphingLabel.xcframework
    

    目前有针对 Package.swift 生成并编译 XCFrameowrk 的懒人工具,但是由于其依赖将项目转换成 xcproj 的编译方法,携带资源文件的 Swift Package 并不能用。

    实践

    目前笔者有一个开源的私人项目使用了 SPM,可以拉下仓库来看一看。Xcode 在解析各种依赖方面并不稳定,所以项目采用的方案是将所有代码拉到本地并通过修改dependencies 的方式采用本地解析集成。本地集成的方式非常稳定,而且最大程度的保证了你修改源码的能力。Swift 发展非常快,目前不推荐url直接集成远端仓库。

    https://github.com/SailyTeam/Saily 在本地创建 xcworkspace以后便可以直接将 Package.swift 中的product添加到项目的编译流程内。这里再次赞赏Swift Package的多元兼容,其中有一些库是纯 Objective-C 撰写的,可以一键无缝集成。

    本地集成的其他好处自然也包含 0 编译警告,遇到任何问题你都可以直接打断点到 Swift Package的代码上。而 Cocoapod 经常不灵。关于编译警告,养眼准备!

    其中可以重点关注几个混合编译的库的定义和 Fluent Icon 库的定义文件。其中就如上面描述的一样,include 文件会被自动导出给 Swift 使用。

    常见问题

    Q: 我导入了 Swift Package 到项目,但无法import
    A: 请command + shift + K清理项目重新编译。Swift Packagemodule缓存。

    Q: 我的include指向上级目录的头文件,导出失败了
    A: 请清理项目重新编译,有时需要重启 Xcode。

    Q: 我在编译的时候指定了最低要求 iOS 13,为何 Swift Package 无法调用 API?
    A: 请检查Package.swift是否有在platform内指定版本,如有请升级 swift-tools-version 定义行。

    Q: 我的资源文件在添加 process 以后仍然有警告
    A: 请使用文件夹名字或指定每一个文件的名字,通配符并不能很好的工作。

    Q: 我在定义 Package.swift 的时候没有找到你说的这个几个字段
    A: 请升级第一行的 swift-tools-version

    Q: 联网拉取 Swift Package 无法完成
    A: 请考虑清除~/Library/Caches/org.swift.swiftpm/,并换个好一些的网络。如果依然失败请删除 Package.resolved 文件

    相关文章

      网友评论

        本文标题:Swift:编译流程

        本文链接:https://www.haomeiwen.com/subject/fqcgfrtx.html