背景说明
公司要求在其他项目组中提供一个入口, 可以进入访问到我们这个项目的界面, 所以我们决定采用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都同时导入到测试项目中, 分别跳转进对应的页面, 此时是可以运行成功和成功跳转, 并不会出现冲突