iOS Swift Framework静态库制作与发布

说在前面

这是公司iOS Framework的制作与发布流程的踩坑记录。
主要需求和情况为

1.Swift工程
2.无其他第三方库的依赖
3.无xib、storyboard等资源文件
4.打包为Framework
5.发布到CocoaPods
使用的工具、语言版本
Xcode:10.2.1
Swift:5.0.1
CocoaPods:1.7.4

一、Framework创建

0. 新建Cocoa Touch Framework

1. 修改配置

  • 修改支持的设备和操作系统版本

    TARGETS-MyFramewrok-General-Deployment Info-Deployment Target
    设置为能支持到的最低版本,尽量低
    iOS Swift Framework静态库制作与发布_第1张图片

  • TARGETS-MyFramewrok-Build Settings-Architectures-Build Active Architecture Only改为No
    iOS Swift Framework静态库制作与发布_第2张图片

YES:只会选择编译、链接对应目标设备的指令集。
NO:编译、链接会涵盖所有指令集,必要时选择执行对应的指令集。
Debug一般设置为YES,执行效率高。
Release一般为NO,以支持所有可能的架构。

  • 添加armv7s架构(可选)
    TARGETS-MyFramewrok-Build Settings-Architectures-Architectures-other加号,输入armv7s
    iOS Swift Framework静态库制作与发布_第3张图片
    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

  • Dead Code Stripping 设置为No
    iOS Swift Framework静态库制作与发布_第4张图片

  • 添加需要暴露出的头文件

    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
    iOS Swift Framework静态库制作与发布_第5张图片
    iOS Swift Framework静态库制作与发布_第6张图片

2. 编写SDK

OC SDK:注意在Build Phases-Headers的Public下添加要提供出去的头文件,其中不想暴露的方法不要写在.h @interface里。

Swift SDK:为了支持OC项目能够使用,类、方法、属性等外部能够调用的,可见性至少要为public,同时还要加上@objc以支持OC调用(类还必须继承自NSObject)

3. 编译framework、合并架构

在xcode中分别选择模拟器和真机,build,生成两个.framework,分别支持模拟器的架构和真机架构。具体位置在~/Library/Developer/Xcode/DerivedData/项目名-一串字符串/Build/Products,或者在Xcode中Products目录下右键-show in finder。为了方便使用者调试,需要合并两个framework。

打开其中一个MyFramework.framework(其实是一个文件夹),可以看到其结构:
iOS Swift Framework静态库制作与发布_第7张图片

  • MyFramework:存储Framework代码的关键代码文件,无后缀名,

    需要利用lipo合并

  • Headers文件夹:包含MyFramework.hMyFramework-Swift.h

    需要合并

  • Modules文件夹:包含一个module.modulemap.swiftmodule 文件夹,其中 .swiftmodule文件夹下有架构名.swiftdoc架构名.swiftmodule等文件。

    需要合并

  • Info.plist 存储Framework的配置信息 目前看不需要合并

    在这个文件中一些字段值,模拟器、真机版本是不一样的(iphoneos/iphonesimulator),但参考目前看到的文章,没有一个有对该文件有做修改,目前测试来看也不需要,所以复制其中一个即可,考虑到可能的影响,我们使用真机版本。

3.1 手动合并(不推荐)

(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
3.2 使用脚本(推荐)

手动步骤很繁琐,不利于后续更新维护,所以我们利用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到CocoaPods公有库

发布开源代码或framework到pods的方式基本一致。
可以参考这篇文章

0. 注意

在测试完全都跑通了之后,正式打包发到pod上引入,怎么也调用不了库方法,然而本地直接引入framework就可以。重新尝试了好几次后发现:

把pods库的名字改一下,跟.framework的名不一样就好了orz

比如framework叫MyFramework,pods就改成了MyFrameworkPod。
(这是我爬了好久坑发现的解决方法,如果大家有其他合适的方法可以指出)

1. 创建pod lib

使用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'
    ...
    

2. 推送整个pod lib到GitHub

创建tag,推送到自己的github仓库,发布一个release。注意podspec里的source路径要和仓库地址一致

git tag -a 0.1.0 -m "first release"
git push origin --tags

3. 推送MyFrameworkPod.podspec到CocoaPods

  1. 如果没有 pod trunk账号首先需要注册,描述部分可以没有。注册需要邮箱验证

    pod trunk register [邮箱] [用户名] --description=[描述]

    pod trunk me可以查看当前自己的信息和拥有的库

  2. 验证代码和podspec文件是否有错

    pod lib lint MyFrameworkPod.spec

    如果有warning也会不通过,根据提示修改后消除所有warning,或者加上--allow-warnings忽略warning

  3. 上传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)

  • OC
    #import
  • Swift
    import MyFramework

远程Pod引入

  1. 安装CocoaPods,在终端输入sudo gem install cocoapods
  2. 创建podfile,如果项目没有使用过Pod,在终端中到项目根目录执行pod init,会生成一个podfile文件,编辑该文件,引入SDKpod 'MyFrameworkPod',默认使用最新版本。或使用 pod 'MyFrameworkPod', '0.1.1'指定版本
  3. 执行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

  • OC
    @import MyFramework
    或者
    #import
  • Swift
    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

你可能感兴趣的:(iOS)