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
    設置爲能支持到的最低版本,儘量低
    在這裏插入圖片描述

  • 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.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 中的頭文件中的內容(整篇複製進來)
   <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

  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 <MyFramework/MyFramework.h>
  • 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 <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

參考

CocoaPods公有庫、遠程私有庫、本地私有庫的使用整理

CocoaPods發佈SDK

混編靜態庫(Static Framework) 升級 XCode到10.2 後在模擬器(或真機)上編譯失敗

pod lib lint fails for Swift-only vendored frameworks

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章