Swift+第三方库的framework制作流程详解

4,941 阅读5分钟

背景说明

公司要求在其他项目组中提供一个入口, 可以进入访问到我们这个项目的界面, 所以我们决定采用framework的形式, 将我们的项目打包成一个framework给其他项目组, 然后提供相应的接口调用。但是另一个项目组由于历史原因, 他们的项目没办法用cocoaPod的方式依赖第三方, 所以我们这边也只能通过第三方源码的方式集成到我们的framework中。至于如何通过cocoaPod制作依赖第三方库的framework, 后续会再详细说明。

制作前的知识梳理

什么是framework

framework其实就是一个库, 可以提供给别人使用, 但是隐藏内部的具体实现。系统的.framework是动态库,我们自己建立的.framework是静态库。

静态库和动态库

静态库 连接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。 存在形式有.a 和 .framework
动态库 连接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。存在形式有.framework, .dylib, .tbd

.a与.framework的区别

1 .a是一个纯二进制文件,.framework中除了有二进制文件之外还有资源文件
2 .a文件不能直接使用,至少要有.h文件配合,.framework文件可以直接使用
3 .a + .h + sourceFile = .framework

接下来就是具体创建流程

1 创建Demo项目

快捷键command + shift + n, 选择App类型, 这个Demo项目是用来运行和测试framework, 因为framework不是一个项目, 无法直接运行

2 创建framework

TARGETS -> +

3 配置framework

Deployment Info 最低的系统要求

建议当然低一些好,(示例 ios9)

Build Active Architecture Only 仅构建活动架构

设置为NO, 即不是只编译当前架构, 如果为Yes, 则只会编辑当前选择的架构, 比如选择iPhone 13模拟器编译, 则编译后的framework只能用于iPhone 13模拟器

Excluded Architecture 排除架构

即移除重复的架构, 因为真机和模拟器编译后,framework中arm64架构重复,会导致合并失败,所以移除模拟器中的arm64架构

iOS指令集知识

armv6

iPhone iPhone2 iPhone3G 第一代和第二代iPod Touch

armv7

iPhone4 iPhone4S

armv7s

iPhone5 iPhone5C

arm64

iPhone5S iPhone6 iPhone6+

指令是向下兼容的,如iPhone5s CPU支持arm64, 但它同时兼容armv7s,只是如果程序使用armv7s指令进行编译,可能无法充分发挥它的64位特性。

Architecture

是指该程序编译时的目标设备(就是ARM指令集,如armv7, armv7s…),编译期会为不同的指令集(设备)生成专有的安装包。不同设备上会执行该设备对应的指令集,如iPhone5s会优执行arm64(如果有)

Dead Code Stripping 死代码剥离

即编译选项优化,是对程序编译出的可执行二进制文件中没有被实际使用的代码进行Strip操作, 给framework包瘦身, 但是对于framework来说, 应该设置为NO, 避免代码、调试符号等被剥离。

Mach-O Type 类型

设置类型为静态库

Build Configutation 编译配置

设置为release

Build Libraries for Distribution 为分发构建库

设置为Yes, 使编译出来的framework向下兼容, 即用高版本Xcode自带的Swift高版本编译出来的framework, 放到低版本Xcode低Swift版本中, 也能运行。否则Swift编译器不会生成必要的".swiftinterface文件,这是将来编译器能够加载旧库的关键。不然在不同Swift版本的Xcode运行, 会报错 Module compiled with Swift 5.6 cannot be imported by the Swift 5.5.1 compiler

4 开发代码

swift不像OC可以暴露接口,在swift中要想给别的工程调用接口,记得在类,方法或属性前加public或者open。

swift权限控制符说明

open

权限最大,可以被外界模块访问,继承重写
public

可以被外界工程访问
internal

默认文件创建时的权限,可以在本工程的访问
private

只可以在创建的文件内访问

项目原因, 只能通过源码方式引入第三方框架, 以SnapKit为例, 将SnapKit的源码拖进项目里

同时创建一个测试文件GPKitController.swift, targets选择GPKit, 代码如下

//
//  GPKitController.swift
//  GPKit
//
//  Created by Darren on 2022/3/25.
//

import UIKit

/**
 GPKit控制器
 */
public class GPKitController: UIViewController {

    public override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        navigationItem.title = "GPKit"
        
        let label = UILabel()
        label.text = "我是GPKit里面的控制器"
        label.textColor = .red
        view.addSubview(label)
        label.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }
    }
}

5 添加编译合并脚本

framework的编译分为模拟器编译和真机编译, 而我们提供给别人使用的framework, 一般都是得模拟器和真机都能运行的, 所以必须将两个版本的framework合并成一个通用的framework

targets -> +

没有使用cocoaPod的合并脚本代码

#!/bin/sh
# SDK名字, 改成自己的SDK名字即可
SDK_NAME=GPKit

# framework最后输出的路径的文件夹
UNIVERSAL_OUTPUTFOLDER="${SRCROOT}/Products/"

# 工作区间, 因为没有用到cocoaPod, 所以是${PROJECT_NAME}.xcodeproj
# 如果用到cocoaPod, 就是${PROJECT_NAME}.xcworkspace
WORKSPACE_NAME=${PROJECT_NAME}.xcodeproj

# 创建输出路径文件夹
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"

# 移除上次编译生成的framework
rm -rf "${UNIVERSAL_OUTPUTFOLDER}/${SDK_NAME}.framework"

# 编译真机版framework
xcodebuild -target "${SDK_NAME}" -configuration ${CONFIGURATION} -sdk iphoneos ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build

# 编译模拟器版framework
xcodebuild -target "${SDK_NAME}" -configuration ${CONFIGURATION} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build

# 拷贝编译生成的真机版framework到最终输出的路径
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${SDK_NAME}.framework" "${UNIVERSAL_OUTPUTFOLDER}"

# 将模拟器框架的swift模块复制到最终输出的路径
SIMULATOR_SWIFT_MODULES_DIR="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${SDK_NAME}.framework/Modules/${SDK_NAME}.swiftmodule/."
if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]; then
cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${UNIVERSAL_OUTPUTFOLDER}/${SDK_NAME}.framework/Modules/${SDK_NAME}.swiftmodule"
fi

# 合并模拟器和真机framework, 生成通用framework
lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${SDK_NAME}.framework/${SDK_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${SDK_NAME}.framework/${SDK_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${SDK_NAME}.framework/${SDK_NAME}"

# 删除编译之后生成的无关的配置文件
dir_path="${UNIVERSAL_OUTPUTFOLDER}/${SDK_NAME}.framework/"
for file in ls $dir_path
do
if [[ ${file} =~ ".xcconfig" ]]
then
rm -f "${dir_path}/${file}"
fi
done

# 打开合并后的文件夹
open "${UNIVERSAL_OUTPUTFOLDER}"

使用cocoaPod的合并脚本代码

即有通过cocoaPod创建生成.xcworkspace文件, 则脚本代码如下

#!/bin/sh
# SDK名字, 改成自己的SDK名字即可
SDK_NAME=GPKit

# framework最后输出的路径的文件夹
UNIVERSAL_OUTPUTFOLDER="${SRCROOT}/Products/"

WORKSPACE_NAME=${PROJECT_NAME}.xcworkspace

# 创建输出路径文件夹
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"

# 移除上次编译生成的framework
rm -rf "${UNIVERSAL_OUTPUTFOLDER}/${SDK_NAME}.framework"

# 编译真机版framework
xcodebuild -workspace "${WORKSPACE_NAME}" -scheme "${SDK_NAME}" -configuration ${CONFIGURATION} -sdk iphoneos ONLY_ACTIVE_ARCH=NO   BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build

# 编译模拟器版framework
xcodebuild -workspace "${WORKSPACE_NAME}" -scheme "${SDK_NAME}" -configuration ${CONFIGURATION} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build

# 拷贝编译生成的真机版framework到最终输出的路径
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${SDK_NAME}.framework" "${UNIVERSAL_OUTPUTFOLDER}"

# 将模拟器框架的swift模块复制到最终输出的路径
SIMULATOR_SWIFT_MODULES_DIR="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${SDK_NAME}.framework/Modules/${SDK_NAME}.swiftmodule/."
if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]; then
cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${UNIVERSAL_OUTPUTFOLDER}/${SDK_NAME}.framework/Modules/${SDK_NAME}.swiftmodule"
fi

# 合并模拟器和真机framework, 生成通用framework
lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${SDK_NAME}.framework/${SDK_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${SDK_NAME}.framework/${SDK_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${SDK_NAME}.framework/${SDK_NAME}"

# 删除编译之后生成的无关的配置文件
dir_path="${UNIVERSAL_OUTPUTFOLDER}/${SDK_NAME}.framework/"
for file in ls $dir_path
do
if [[ ${file} =~ ".xcconfig" ]]
then
rm -f "${dir_path}/${file}"
fi
done

# 打开合并后的文件夹
open "${UNIVERSAL_OUTPUTFOLDER}"

6 生成framework

执行脚本, 只需要选中GPKitAggregate, 然后执行run就行

最后生成的目录如下

7 测试framework

随便新建一个项目, 然后将生成的framework拖进去, 创建一个跳转入口

然后分别用模拟器和真机运行, 如果都能成功运行且跳转, 则framework制作成功

8 第三方重复使用问题

测试新建立一个新的framework叫GPKit1, 里面也引用到SnapKit的源码, 然后将GPK和GPKit1都同时导入到测试项目中, 分别跳转进对应的页面, 此时是可以运行成功和成功跳转, 并不会出现冲突