說在前面
這是公司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
設置爲能支持到的最低版本,儘量低
-
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 -
Dead Code Stripping 設置爲No
-
添加需要暴露出的頭文件
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
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(其實是一個文件夾),可以看到其結構:
-
MyFramework:存儲Framework代碼的關鍵代碼文件,無後綴名,
需要利用lipo合併
-
Headers文件夾:包含
MyFramework.h
和MyFramework-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 中的頭文件中的內容(整篇複製進來)
<contents>
#else
// 你編譯生成真機環境下的 Framework 中的頭文件中的內容(整篇複製進來)
<contents>
#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
-
如果沒有 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)
- OC
#import <MyFramework/MyFramework.h>
- Swift
import MyFramework
遠程Pod引入
- 安裝CocoaPods,在終端輸入
sudo gem install cocoapods
- 創建podfile,如果項目沒有使用過Pod,在終端中到項目根目錄執行
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
- OC
@import MyFramework
或者
#import <MyFramework/MyFramework.h>
- 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