iOS项目中SDK的开发之SDK开发(二)
一、如何制作SDK(framework)
🐹 1、创建SDK工程
🐡 通过如上方式创建SDK工程,这里我创建名为 MyLibrarySDK 的SDK工程, 如下图所示:
生成的工程只有MyLibrarySDK.h
这一个头文件,一般我们将需要暴露的头文件都放在这个头文件中,以供外部调用。
!!!注意:默认生成的.h 头文件不是必须保留的,你可以删除,也可以保留,也可以重新定义新的名称。
🐹 2、开发SDK功能
🐡 如上图,我创建了一个名为 MyTestView 的类文件,并且将它设置为public
,这样就可以把这个文件导入MyLibrarySDK.h
头文件,以供外部调用

🐡 在 MyTestView 的类文件中实现SDK的相关功能,这里只实现一些简单方法作为参考你可以根据自己的业务需求开发更多的功能
🐹 3、SDK打包
🐝 1. 创建打包工程Aggregate
🐡 这里我创建了名为 MyLibrarySDK-Script 的打包脚本

🐝 2. 添加依赖的target
🐝 3. 创建Run script
🐡 添加如下脚本,脚本是通用的,可以直接拿去用,复制到工程中就行
#要build的target名 #!/bin/sh #要build的target名 TARGET_NAME=${PROJECT_NAME} if [[ $1 ]] then TARGET_NAME=$1 fi UNIVERSAL_OUTPUT_FOLDER="${SRCROOT}/${PROJECT_NAME}/" #创建输出目录,并删除之前的framework文件 mkdir -p "${UNIVERSAL_OUTPUT_FOLDER}" rm -rf "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework" #分别编译模拟器和真机的Framework xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphonesimulator BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build #拷贝framework到univer目录 cp -R "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework" "${UNIVERSAL_OUTPUT_FOLDER}" lipo "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}" -remove arm64 -output "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}" #合并framework,输出最终的framework到build目录 lipo -create -output "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${TARGET_NAME}.framework/${TARGET_NAME}" #删除编译之后生成的无关的配置文件 dir_path="${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/" for file in ls $dir_path do if [[ ${file} =~ ".xcconfig" ]] then rm -f "${dir_path}/${file}" fi done #判断build文件夹是否存在,存在则删除 if [ -d "${SRCROOT}/build" ] then rm -rf "${SRCROOT}/build" fi rm -rf "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator" "${BUILD_DIR}/${CONFIGURATION}-iphoneos" #打开合并后的文件夹 open "${UNIVERSAL_OUTPUT_FOLDER}"
🐡 修改在release环境下进行构建
🐝 4. SDK工程配置
🐡 1、设置SDK支持的 最低版本 和 支持设备类型
🐡 2、设置SDK为静(动)态库
Mach-O
是Mach目标文件格式的缩写,是一种用于可执行文件、目标代码、共享库、动态加载代码和核心转储的文件格式,是macOS X和iPhoneOS库中用于可执行文件的格式。
OS X 支持许多应用程序环境,每个环境都有自己的运行时规则、约定和文件格式。在 OS X 中,内核扩展、命令行工具、应用程序、框架和库(共享和静态)是使用 Mach-O(Mach 对象)文件实现的。
OS X 运行时架构决定了目标文件在文件系统中的布局方式以及程序与内核的通信方式。OS X 中使用的目标文件格式是 Mach-O 。
Mach-O
的不同类型定义
Executable - 已编译的机器目标程序,准备好以二进制格式运行。 Dynamic Library - 在运行时链接 - 引用动态库的程序将在启动时(或按需)加载并与库链接。 Bundles - 捆绑包标识符让 iOS 和 OSX 能够识别您的应用程序的任何更新。它使其在应用程序中具有独特的存在感。 Static Library - 文件在构建时链接。代码被复制到可执行文件中。程序未引用的库中的代码将被删除。只有静态库的程序在运行时没有任何依赖关系。 Relocatable Object File - 是动态库的另一个词。当您与动态库链接时,会根据库在内存中的加载位置计算其中包含的函数的地址。它们是“可重定位的”,因为包含函数的地址不是在链接时确定的。
(在静态库中,地址是在链接时计算的。)
🐡 3、设置SDK的Other Linker Flags
添加-ObjC

Other Linker Flags
添加 -ObjC
的说明:
从C代码到可执行文件经历的步骤是:源代码 > 预处理器 > 编译器 > 汇编器 > 机器码 > 链接器 > 可执行文件
预处理器(预编译):在该阶段,编译器将C或者OC中源代码中包含的stdio.h和#import 的库编译进来
编译器:在这个阶段,编译器首先要检查代码的规范性,是否有语法错误等,用来确定代码实际要做的工作,在检查无误后,编译器把代码翻译成汇编语言
汇编器:汇编阶段把编译阶段生成的汇编代码转化成二进制目标文件
链接器:将不同部分的代码和数据收集和组合成一个单一文件的过程,也就是把不同目标文件合并成最终可执行文件的过程,将编译输出的二进制文件链接成最终可执行的目标文件
在最后一步需要把.o文件和C语言运行库链接起来,这时候需要用到ld命令。源文件经过一系列处理以后,会生成对应的.obj文件,然后一个项目必然会有许多.obj文件,并且这些文件之间会有各种各样的联系,
例如函数调用。
!!!注意:在链接阶段时,将编译阶段一些只有函数声明的头文件,而没有函数实现的库链接到可执行文件中来库分为 "动态库" 和 "静态库":
静态库:是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时就不需要额外的库文件(.a)。
动态库:在程序执行时,由运行时链接文件加载库,这样可以节省系统的开销(.so)。
通过这个流程你也应该知道,为什么在编译的过程中没事,而在运行的时候就会报错了。
那我们为什么要设置Other Linker Flags呢?
因为Other Linker Flags其实就是链接器工作时,除了默认参数外的其他参数。Other linker flags设置的值实际上就是ld命令执行时后面所加的参数。
常用参数:
-ObjC:加了这个参数后,链接器就会把静态库中所有的Objective-C类和分类都加载到最后的可执行文件中。 这样编译之后的app会变大(因为加载了其他的Objc代码进来)。但是如果静态库中有类和category的话,只有加入这个flag才行。 但是Objc也不是万能的,当静态库中只有分类而没有类的时候,Objc就失效了,这就需要使用-all_load或者-force_load了。
-all_load:这个flag是专门处理-ObjC的一个bug的。用了-ObjC以后,如果类库中只有category没有类的时候这些category还是加载不进来。变通方法就是加入-all_load或者-force-load。
-all_load会让链接器把所有找到的目标文件都加载到可执行文件中,即使没有Objc代码,所以千万不要随便使用这个参数!假如你使用了不止一个静态库文件,然后又使用了这个参数,
那么你很有可能会遇到ld: duplicate symbol错误,因为不同的库文件里面可能会有相同的目标文件。
这里会有两种方法解决:1. 用命令行就行拆包;2. 在遇到-ObjC失效的情况下使用-force_load参数 。
-force_load:所做的事情跟-all_load其实是一样的,但是-force_load需要指定要进行全部加载的库文件的路径,这样的话,你就只是完全加载了一个库文件,不影响其余库文件的按需加载。
🐡 4、设置
Build Active Architecture Only
1. Debug 时设置为YES,可以在调试时只编译当前所调试设备的架构类型的SDK包,如当你iPhone真机调试时,只编译支持iPhone运行的arm64的SDK架构包; 2. Release 时设置为NO,使得在发布SDK打包时,编译支持所有架构的SDK包,如支持iPhone、iPad、iWatch等;
🐡 5、查看并添加对外暴露的头文件
可以根据需要将 Private 和Object中的文件拖动到 Public 中,以暴露给外部调用。
Public(公共):最终确定了接口,并打算由您的产品的客户端使用。公共标头作为可读源代码包含在产品中,不受限制。
Private(私有):该接口不是为您的客户端设计的,或者它还处于开发的早期阶段。私有头部包含在产品中,但它被标记为“private”。
因此,这些符号对所有客户端都是可见的,但是客户端应该明白,他们不应该使用它们。Project(项目):该接口仅供当前项目中的实现文件使用。目标中不包括项目标头,但目标代码中除外。这些符号对客户根本不可见,只对您可见。
🐡 6、设置Perform Single-Object Prelink
为 YES
作用是xcode会在链接前将所有目标文件合并成一个中间文件
,这样可以减少链接时的重复动作,提高构建性能;特别是在大型项目中可以明显减少构建时间。

🐝 5. 构建SDK
🐡 点击红色箭头指向的按钮构建SDK
❌ 运行报错 : Sandbox: xcodebuild(26258) deny(1) file-read-data /Users/xxx/Desktop/MyLibrarySDK/MyLibrarySDK.xcodeproj/project.xcworkspace
这个错误信息表明,rm
命令在尝试读取文件/Users/xxx/Desktop/MyLibrarySDK/MyLibrarySDK.xcodeproj/project.xcworkspace 时被沙盒机制拒绝了。沙盒机制限制了应用程序对文件系统的访问,以提高安全性。
✅ 解决编译报错:User Script Sandboxing 设为 NO
User Script Sandboxing
是 xcode的一种安全机制,通过限制脚本对系统资源的访问,将脚本执行限制在受控的环境中,防止潜在的恶意代码执行和未经授权的资源访问。
🐡 重新执行得到SDK包
🔊:通过以上步骤就完成了SDK的开发与构建了,但是这种只能先开发完,再将SDK放到Demo中进行调试,对开发者来说特别不友好,那么我们该如何一边开发一边调试呢?
🐹 4、边开发边调试SDK
🐡 首先将SDK放在一个文件目录中
🐡 在SDK所在目录下创建xcworkspace和一个demo工程 , 并将SDK工程、demo工程和.xcworkspace文件放在同一目录下。
🐡 打开.xcworkspace,依次将SDK工程和demo工程的project拖入.xcworkspace的同级目录下。

🐡 将SDK和demo工程进行关联,在demo工程中导入SDK
🐡 在demo工程中导入SDK头文件并调用了SDK的方法
这样我们就可以一边开发一边调试SDK了
🐹 5、如何让SDK和demo工程依赖第三方库
🐡 在对工程进行pod 管理时,一般我们是在.xcodeproj
所在的目录执行pod init
从而创建podfile
文件,再在podfile
文件 中引入第三方库后,执行pod install
,之后会自动创建一个.xcworkspace
文件来管理 主工程 和 pod 工程;但是如下图所示,我们已经自己创建了 .xcworkspace文件,并且将 SDK工程 和 demo工程 导入 xcworkspace 进行关联,这种情况下再使用以前pod的创建方式就不行了。
🐡 创建
podfile
在SDK的根目录上执行pod init
创建podfile文件,并将其移动到xcworkspace所有的目录
🐡 打开podfile
文件并设置ruby脚本如下:
# Uncomment the next line to define a global platform for your project #这里的意思是创建名为MyLibrarySDK的xcworkspace文件,你也可以使用其他的名称,自己随便取 workspace 'MyLibrarySDK.xcworkspace' platform :ios, '15.0' #为SDK工程添加三方依赖 target 'MyLibrarySDK' do #因为podfile文件所在目录没有project工程,所以需要为'MyLibrarySDK'这个target指定一个project project './MyLibrarySDK/MyLibrarySDK.xcodeproj'
use_frameworks!
# Pods for MyLibrarySDK pod 'Masonry' end #为SDK Demo工程添加三方依赖 target 'MyLibrarySDKDemo' do #因为podfile文件所在目录没有project工程,所以需要为'MyLibrarySDKDemo'这个target指定一个project project './MyLibrarySDKDemo/MyLibrarySDKDemo.xcodeproj'
use_frameworks!
# Pods for MyLibrarySDKDemo pod 'Masonry' end
🐡 删除
之前创建的xcworkspace
文件,因为pod install
后会自动创建
并管理SDK工程、demo工程和pod工程的xcworkspace
🐡 执行pod install
后的目录文件如下图所示

🐡 打开xcworkspace并选中demo工程执行,并在SDK工程和demo工程中引入第三方库头文件,并使用第三方库的功能
SDK中使用
demo中使用
至此我们就完成了给SDK工程和Demo工程同时引入pod管理第三方库的功能。
二、自动化脚本编译SDK
下面我们看一下如何使用shell脚本自动化打包SDK
🐡 在xcworkspace所在目录下创建shell脚本
🐡 编写脚本执行下面脚本,最终会得到一个xcframework后缀的SDK,
xcframework
是一种打包方式,可以将多种平台
(iPhone、iPad、Mac、Simulator或arm64、i386、x86_64)的库文件打包在一起。
之所以需要打包成xcframework
主要是因为,当真机
打包的SDK是arm64架构
的,模拟器
打包的是x86_64和arm64
的,使用xcframework可以将他们合并在一起
。
#!/bin/bash # 停止脚本遇到任何错误时 set -e # 获取脚本所在目录 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # 项目配置 WORKSPACE_PATH="$SCRIPT_DIR/MyLibrarySDK.xcworkspace" #替换成自己的 SCHEME="MyLibrarySDK" #替换成自己的 CONFIGURATION="Release" DERIVED_DATA_DIR="$SCRIPT_DIR/../DerivedData" FRAMEWORK_NAME="MyLibrarySDK" #替换成自己的 # 创建输出目录 mkdir -p ${DERIVED_DATA_DIR}/universal # 编译真机架构 xcodebuild -configuration $CONFIGURATION -derivedDataPath ${DERIVED_DATA_DIR}/iOS -workspace $WORKSPACE_PATH -scheme $SCHEME -sdk iphoneos clean build # 编译模拟器架构 xcodebuild -configuration $CONFIGURATION -derivedDataPath ${DERIVED_DATA_DIR}/simulator -workspace $WORKSPACE_PATH -scheme $SCHEME -sdk iphonesimulator clean build # 创建XCFramework xcodebuild -create-xcframework \ -framework ${DERIVED_DATA_DIR}/iOS/Build/Products/Release-iphoneos/${FRAMEWORK_NAME}.framework \ -framework ${DERIVED_DATA_DIR}/simulator/Build/Products/Release-iphonesimulator/${FRAMEWORK_NAME}.framework \ -output ${DERIVED_DATA_DIR}/universal/${FRAMEWORK_NAME}.xcframework echo "Universal XCFramework is created at: ${DERIVED_DATA_DIR}/universal/${FRAMEWORK_NAME}.xcframework" echo "-------------编译完成---------------------------"
如果只想打包单一架构的SDK,可以使用如下脚本:
echo "------------------Start-----------------------" #获取脚本所在目录 project_path=$(cd `dirname $0`; pwd) #设置framework名称 framework_name="MyLibrarySDK" #替换成自己的 #设置workspace路径 work_space="${project_path}/MyLibrarySDK.xcworkspace" #替换成自己的 #设置framework产物路径 work_dir="${project_path}/BuildDeriver/Build/Products" #设置工作路径 deriver_data_dir="${project_path}/../BuildDeriver" #生成的SDK所在的路径 #【真机包】 #device_dir=${work_dir}/'Release-iphoneos'/${framework_name}'.framework' #echo "device_dir is: ${device_dir}" #【模拟器包】 simulator_dir=${work_dir}/'Release-iphoneosimulator'/${framework_name}'.framework' echo "simulator_dir is:${simulator_dir}" #删除上一次的文件 echo "------------------clean begin----------------------" sudo rm -r ${deriver_data_dir} echo "------------------clean end------------------------" #xcode编译 #【真机包】 #echo "------------------build device begin----------------------" #xcodebuild -configuration 'Release' -derivedDataPath ${deriver_data_dir} -workspace ${work_space} -scheme ${framework_name} ONLY_ACTIVE_ARCH=NO -sdk iphoneos clean build #echo "------------------build device completed------------------" #【模拟器包】 echo "------------------build simulator begin----------------------" xcodebuild -configuration 'Release' -derivedDataPath ${deriver_data_dir} -workspace ${work_space} -scheme ${framework_name} ONLY_ACTIVE_ARCH=NO -sdk iphonesimulator clean build echo "------------------build simulator completed------------------"
!!!注意:使用单一架构的SDK时,在真机或者模拟器下脚本内容别忘了修改

🐡 执行脚本(这里以单一架构脚本为例)
发现报错了,脚本执行被拒绝了,这是因为没有给脚本设置权限导致的
🐡 设置脚本的执行权限
使用sudo chmod +wrx build.sh
的意思是给build.sh脚本设置可读可写可执行权限,这样我们再执行./build.sh
进就可以正常编译了
三、iOS如何引入一个SDK
🐹 1、新建示例工程
🐹 2、将SDK拖进工程
🐹 3、修改工程相应配置
🐡 设置 Embed
🐝 Embed:嵌入,用于动态库,动态库在运行时链接,所以它们需要被打进bundle里面。
如何判断呢?使用终端执行:
file MyLibrarySDK.framework/MyLibrarySDK
如果返回:
current ar archive: 说明是静态库,选择Do not embed Mach-0 dynamically:说明是动态库,选择Embed
!!!注意:系统的.framework是动态库,我们自己建立的.framework一般是静态库。
🐝 Signing:签名,只用于动态库,如果已经有签名了就不需要再签名。
如何判断呢?使用终端执行:
codesign -dv MyLibrarySDK.framwork
如果返回:
code object is not signed at all 或者 adhoc: 选择Embed and sign 其它:表示已经正确签名,选择Embed Without Signing
🐡 User Script Sandboxing 设为 NO
🐹 4、在ViewController补充测试代码,并运行
疑问🤔:如果SDK用到了相关三方依赖,就像上图中一样,产生了崩溃,这个时候,我们只需要在主工程中通过pod引入相关三方依赖即可。这里是SDK中用到了Masonry第三方,我们通过主工程创建pod引入。
🐹 5、配置SDK需要第三方依赖
运行成功