这是公司iOS Framework的制作与发布流程的踩坑记录。
主要需求和情况为:
1.Swift工程
2.无其他第三方库的依赖
3.无xib、storyboard等资源文件
4.打包为Framework
5.发布到CocoaPods
使用的工具、语言版本
Xcode:10.2.1
Swift:5.0.1
CocoaPods:1.7.4
修改支持的设备和操作系统版本
TARGETS-MyFramewrok-General-Deployment Info-Deployment Target
设置为能支持到的最低版本,尽量低
TARGETS-MyFramewrok-Build Settings-Architectures-Build Active Architecture Only改为No
YES:只会选择编译、链接对应目标设备的指令集。
NO:编译、链接会涵盖所有指令集,必要时选择执行对应的指令集。
Debug一般设置为YES,执行效率高。
Release一般为NO,以支持所有可能的架构。
添加armv7s架构(可选)
TARGETS-MyFramewrok-Build Settings-Architectures-Architectures-other加号,输入armv7s
Xcode6后,默认不支持armv7s。如果项目需要支持armv7s而其引入的库不支持的话,会出错
xxxx does not contain a(n) armv7s slice:xxxxx for architecture armv7s
所以作为供别人使用的sdk最好提供支持(如果项目确实不需要支持某个架构,是可以在已打包的framework中删除的。)
模拟器:4s ~ 5 : i386; 5s以后 : x86_64。
真机:
armv6:iPhone1、2、3G;iPod Touch1、2.
armv7设备:iPhone 3GS、 4、4S;iPad1、2,iPod Touch 3G、 4.
armv7s设备:iPhone 5、5C,iPad4.
arm64设备:iPhone 5S以后、iPad Air以后
指令集向下兼容 armv7s >> armv7 >> armv6 (iPhone5可以跑armv7架构的指令集,但可能无法充分发挥特性)
设置为静态framework
TARGETS-MyFramewrok-Build Settings-Linking-Math-O Type
修改为Static Library
添加需要暴露出的头文件
OC工程:TARGETS-MyFramewrok-Build Phases下的Headers(如果没有Headers,点击左上角+号,New Headers Phases)在Public下添加
Swift工程:不需要配置。在新建工程时会自动生成一个工程名.h的头文件,并已经默认添加到暴露的头文件中,在打包时还会生成一个工程名-Swift.h的桥接文件,会把工程里带@objc的Swift类/方法/属性暴露出去。
编译模式改为Release
点击左上角target(设备左边),Edit Scheme-Run-Info-Build Configuration
OC SDK:注意在Build Phases-Headers的Public下添加要提供出去的头文件,其中不想暴露的方法不要写在.h @interface
里。
Swift SDK:为了支持OC项目能够使用,类、方法、属性等外部能够调用的,可见性至少要为public
,同时还要加上@objc
以支持OC调用(类还必须继承自NSObject)
在xcode中分别选择模拟器和真机,build,生成两个.framework,分别支持模拟器的架构和真机架构。具体位置在~/Library/Developer/Xcode/DerivedData/项目名-一串字符串/Build/Products
,或者在Xcode中Products
目录下右键-show in finder。为了方便使用者调试,需要合并两个framework。
打开其中一个MyFramework.framework(其实是一个文件夹),可以看到其结构:
MyFramework:存储Framework代码的关键代码文件,无后缀名,
需要利用lipo合并
Headers文件夹:包含MyFramework.h
和MyFramework-Swift.h
。
需要合并
Modules文件夹:包含一个module.modulemap
和 .swiftmodule 文件夹,其中 .swiftmodule文件夹下有架构名.swiftdoc、架构名.swiftmodule等文件。
需要合并
Info.plist 存储Framework的配置信息 目前看不需要合并
在这个文件中一些字段值,模拟器、真机版本是不一样的(iphoneos/iphonesimulator),但参考目前看到的文章,没有一个有对该文件有做修改,目前测试来看也不需要,所以复制其中一个即可,考虑到可能的影响,我们使用真机版本。
(1) 复制一份真机.framework,当作合并后的目标.framework
(2) 合并framework二进制文件。在终端中输入:
lipo -create 模拟器Framework路径 真机Framework路径 -output 合并后的目标.framework/MyFramework
(3) 合并MyFramework.framework/Modules/MyFramework.swiftmodule下的文件
纯OC写的framework,到第二步就可以结束了,而Swift库还需要合并该目录。
在第1步中我们以真机版本的framework为基准,所以这一步把模拟器版本下相同路径里的文件全部复制进来即可。
(4) 合并MyFramework.framework/Headers/MyFramework-Swift.h
这是Swift5(xcode10.2)带来的变化,如果不对该文件进行合并而直接使用真机/模拟器版本,将无法同时能在两种环境下编译 通过。
在 Xcode 更新日志中跟编译有关的 issue 提到
If you’re building a framework containing Swift code and using lipo to create a binary that supports both device and simulator platforms, you must also combine the generated Framework-Swift.h headers for each platform to create a header that supports both device and simulator platforms. (48635615)
(如果你的 Framework中是使用混编(包含 Swift代码),然后使用 lipo 这个工具生成同时支持真机和模拟器平台的二进制库,你就需要拼接两个不同环境生成的 Header 文件( YourFramework-Swift.h)的内容到一起,作为新的 Header 文件同时来支持这两个平台)
拼接两个文件的内容,打开目标MyFramework-Swift.h(照之前的步骤里面是真机的内容),具体修改如下:
#if TARGET_OS_SIMULATOR
// 你编译生成模拟器环境下的 Framework 中的头文件中的内容(整篇复制进来)
#else
// 你编译生成真机环境下的 Framework 中的头文件中的内容(整篇复制进来)
#endif
生成之后的文件内容大致如下:
#if TARGET_OS_SIMULATOR
/************** 模拟器环境下的 Framework 中的头文件中的内容 *********/
#if 0
#elif defined(__x86_64__) && __x86_64__
// Generated by Apple Swift version 5.0.1 effective-4.2 (swiftlang-1001.0.82.4 clang-1001.0.46.5)
...
# pragma clang attribute pop
#endif
/******************************************************************/
#else
/************** 真机环境下的 Framework 中的头文件中的内容 *********/
#elif defined(__arm64__) && __arm64__
// Generated by Apple Swift version 5.0.1 effective-4.2 (swiftlang-1001.0.82.4 clang-1001.0.46.5)
...
#endif
#pragma clang diagnostic pop
#endif
/******************************************************************/
#endif
手动步骤很繁琐,不利于后续更新维护,所以我们利用Xcode的script phase编写合并脚本,在编译时自动完成上述工作。
在Xcode的framework工程中,点击左边工程名,选择TARGETS-Build Phases-加号-New Run Script Phase,粘贴以下shell脚本,分别选择真机和模拟器各编译一次即可自动完成合并,成功合并后会在finder中打开目标framework位置。
# 脚本功能:合并对应模拟器cpu架构和真机架构的不同framework
# 使用方法:在framework工程中,TARGETS-Build Phases-加号-New Run Script Phase,
# 粘贴脚本,分别选择真机和模拟器各编译一次即可,成功合并后会在finder中打开。
# (很多教程写新建一个Aggregate Target再添加脚本,但是
# 由于默认已经有一个与项目名相同的TARGET,不能重名,如果又要保持framework名称一致
# 又需要修改,不如这样方便。)
# 用到的xcode环境变量: 参考https://www.jianshu.com/p/b5c85dcd6b04
# ${SRCROOT} 项目根目录
# ${PROJECT_NAME} 项目名
# ${BUILD_ROOT} 编译输出根目录,通常为~/Library/Developer/Xcode/DerivedData/项目名-乱七八糟的字符串/Build/Products
# ${CONFIGURATION} release或debug
# ${ACTION} 编译时为build
# 真机编译时生成的framework位置
DEVICE_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework
# 模拟器编译时生成的framework位置
SIMULATOR_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework
# 定义合并后framework的存放位置 这里放在项目根目录
INSTALL_DIR=${SRCROOT}/${PROJECT_NAME}.framework
# build时执行,且两类cpu架构均已编译成功生成framework
if [ "${ACTION}" = "build" ] && [ -d "${DEVICE_DIR}" ] && [ -d "${SIMULATOR_DIR}" ]
then
# 删除原有的合并文件(.framework其实是个文件夹)
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi
# 新建合并文件
mkdir -p "${INSTALL_DIR}"
# 将真机framework拷贝至合并文件(因为后面的lipo -create只合并输出.framework下的"项目名"二进制文件,
# 还需要剩余的其他文件才能被使用,本脚本以真机framework的为基准,
# 这一步合并了Modules/xxx.swiftmodule文件夹,以及下面提到的Headers/xxx-Swift.h
cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
# 利用lipo合并两个.framework里的二进制文件,结果保存在合并后目录
lipo -create "${DEVICE_DIR}/${PROJECT_NAME}" "${SIMULATOR_DIR}/${PROJECT_NAME}" -output "${INSTALL_DIR}/${PROJECT_NAME}"
# ***如果是swift工程,还需要拷贝.swiftmodule下的文件
SIMULATOR_SWIFT_MODULES_DIR=${SIMULATOR_DIR}/Modules/${PROJECT_NAME}.swiftmodule/.
if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]
then
cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${INSTALL_DIR}/Modules/${PROJECT_NAME}.swiftmodule"
fi
# *** xcode10.2以后,如果包含Swift文件,
# 还需要合并处理xx.framework/Headers/PROJECT_NAME-Swift.h里的内容
SIMULATOR_SWIFT_HEADER_FILE=${SIMULATOR_DIR}/Headers/${PROJECT_NAME}-Swift.h
DEVICE_SWIFT_HEADER_FILE=${DEVICE_DIR}/Headers/${PROJECT_NAME}-Swift.h
INSTALL_SWIFT_HEADER_FILE=${INSTALL_DIR}/Headers/${PROJECT_NAME}-Swift.h
if [ -e "${SIMULATOR_SWIFT_HEADER_FILE}" ] && [ -e "${DEVICE_SWIFT_HEADER_FILE}" ]
then
# 合并-Swift.h
# 写入.h文件
echo "#if TARGET_OS_SIMULATOR" > "${INSTALL_SWIFT_HEADER_FILE}"
# 模拟器
cat "${SIMULATOR_SWIFT_HEADER_FILE}" >> "${INSTALL_SWIFT_HEADER_FILE}"
echo "#else" >> ${INSTALL_SWIFT_HEADER_FILE}
# 真机
cat "${DEVICE_SWIFT_HEADER_FILE}" >> "${INSTALL_SWIFT_HEADER_FILE}"
echo "#endif" >> "${INSTALL_SWIFT_HEADER_FILE}"
fi
# 合并-Swift.h结束
# 打开项目目录,得到合并后的.framework
open "${SRCROOT}"
fi
发布开源代码或framework到pods的方式基本一致。
可以参考这篇文章
在测试完全都跑通了之后,正式打包发到pod上引入,怎么也调用不了库方法,然而本地直接引入framework就可以。重新尝试了好几次后发现:
把pods库的名字改一下,跟.framework的名不一样就好了orz
比如framework叫MyFramework,pods就改成了MyFrameworkPod。
(这是我爬了好久坑发现的解决方法,如果大家有其他合适的方法可以指出)
使用pod lib create 'MyFrameworkPod'
命令创建pod共有库,根据提示输入选择自己需要的配置
What platform do you want to use?? [ iOS / macOS ]
> iOS
What language do you want to use?? [ Swift / ObjC ]
> Swift
Would you like to include a demo application with your library? [ Yes / No ]
> No
Which testing frameworks will you use? [ Specta / Kiwi / None ]
> None
Would you like to do view based testing? [ Yes / No ]
> No
What is your class prefix?
> MF
复制准备好的MyFramewrok.framework文件到MyFrameworkPod/MyFrameworkPod/
下,
修改MyFrameworkPod.podspec文件。
Pod::Spec.new do |s|
s.name = 'MyFrameworkPod'
s.version = '0.1.0'
s.summary = 'MyFrameworkPod'
s.description = <<-DESC
My Framework
DESC
s.homepage = 'https://github.com/wmadao/MyFrameworkPod'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'wmadao11' => '[email protected]' }
s.source = { :git => "https://github.com/wmadao/MyFrameworkPod.git", :tag => "#{s.version}" }
s.ios.deployment_target = '8.0'
s.platform = :ios, '8.0'
s.requires_arc = true
# swift版本
s.swift_versions = "5.0"
# 静态库framework位置
s.vendored_frameworks = 'MyFrameworkPod/*.{framework}'
s.source_files = 'MyFrameworkPod/Classes/**/*'
# s.frameworks = 'Foundation'
# s.resource_bundles = {
# 'MyFrameworkPod' => ['MyFrameworkPod/Assets/*.png']
# }
# s.public_header_files = 'Pod/Classes/**/*.h'
# s.dependency 'AFNetworking', '~> 2.3'
end
其中重要的点有:
s.swift_versions = "5.0"
s.vendored_frameworks = 'MyFrameworkPod/*.{framework}'
s.source_files = 'MyFrameworkPod/Classes/**/*'
尽管只是用到了framework文件,没有任何其他源代码文件,如果framework是由Swift写的,并且xcode版本(准确说是xcode command line tools版本)为10.2时,还是需要添加一个源码文件路径,并且在该路径下放一个随意的Swift文件,空的也可以,只要后缀是.swift即可。否则会导致后续验证podspec失败。出现以下错误:
- ERROR | [iOS] xcodebuild: Returned an unsuccessful exit code. You can use `--verbose` for more information.
- NOTE | xcodebuild: note: Using new build system
- NOTE | [iOS] xcodebuild: note: Planning build
- NOTE | [iOS] xcodebuild: note: Constructing build description
- NOTE | xcodebuild: ld: warning: Could not find auto-linked library 'swiftFoundation'
- NOTE | xcodebuild: ld: warning: Could not find auto-linked library 'swiftMetal'
- NOTE | xcodebuild: ld: warning: Could not find auto-linked library 'swiftDarwin'
...
创建tag,推送到自己的github仓库,发布一个release。注意podspec里的source路径要和仓库地址一致
git tag -a 0.1.0 -m "first release"
git push origin --tags
如果没有 pod trunk账号首先需要注册,描述部分可以没有。注册需要邮箱验证
pod trunk register [邮箱] [用户名] --description=[描述]
pod trunk me
可以查看当前自己的信息和拥有的库
验证代码和podspec文件是否有错
pod lib lint MyFrameworkPod.spec
如果有warning也会不通过,根据提示修改后消除所有warning,或者加上--allow-warnings
忽略warning
上传podspec
pod trunk push MyFrameworkPod.podspec
同样的,有warning会不通过,--allow-warnings
忽略warning
上传成功,pod search MyFrameworkPod
可以查询某个库的homepage、source、当前版本等信息
或者用pod trunk info MyFrameworkPod
可以查询某个库的所有版本和开发者
准备:由于是Swift库,如果项目是纯oc而且没有混编过Swift会无法编译。
解决方法:在项目里新建一个Swift文件,Xcode会提示是否需要创建Bridging Header,选择创建即可。
拖入.framework到项目中。引入头文件即可使用(无需配置Embeded Binaries)
#import
import MyFramework
sudo gem install cocoapods
pod init
,会生成一个podfile文件,编辑该文件,引入SDKpod 'MyFrameworkPod'
,默认使用最新版本。或使用 pod 'MyFrameworkPod', '0.1.1'
指定版本pod install
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'MyProject' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
pod 'MyFrameworkPod'
end
在需要用到的地方引入头文件,
注意这里是MyFramework而不是MyFrameworkPod,这就是要修改pod名字的原因,否则同名时import时总会import Pod而不是framework文件,导致调用不到SDK
@import MyFramework
#import
import MyFramework
如果 SDK 有用到 Category,注意在 项目 中设置Build Settings - Linking - Other Linker Flags 添加 -ObjC
移除不需要的架构,比如移除模拟器架构。可以是逐个分离出真机架构然后再合并
cd MyFramework.framework
lipo MyFramework -thin arm64 -output MyFramework-arm64
lipo MyFramework -thin armv7 -output MyFramework-armv7
lipo MyFramework -thin armv7s -output MyFramework-armv7s
lipo -create MyFramework-arm64 MyFramework-armv7 MyFramework-armv7s -output MyFramework-device
或者也可以直接删除某个模拟器架构:
cd MyFramework.framework
lipo -remove x86_64 MyFramework -output tmp
lipo -remove i386 tmp -output MyFramework-device
查看framework支持的架构:
lipo -info MyFramework-device
输出:Architectures in the fat file: MyFramework-device are: armv7 armv7s arm64
包括bundle资源和其他依赖的framework
CocoaPods公有库、远程私有库、本地私有库的使用整理
CocoaPods发布SDK
混编静态库(Static Framework) 升级 XCode到10.2 后在模拟器(或真机)上编译失败
pod lib lint fails for Swift-only vendored frameworks