Swift制作Framework,提供給OC項目和Swift項目通用

Swift制作framework

公司的需要需要制作sdk給其他團隊用,其實就是framework
簡直炸裂!踩了一個又一個的坑!
遍體鱗傷之后,決定一定要記錄下來,方便以后自己和有需要的人查閱,能有一點點幫助也是好的

進入正題

官方提供的是.framework.a兩種方式制作SDK的方式。
分別對應創建工程時下方的Cocoa Touch FrameworkCocoa Touch Static Library
.framework.a兩者區別自行百度吧,簡書上制作Framework相關的文章基本都有,就懶得copy了。
.framework其實是分動態靜態的,所以.framework即可滿足我的要求,想要制作動態或者靜態創建framework后可以修改,往后面看。

ps: 
在 iOS 8 之前,
iOS 平臺不支持開發者使用用戶自己的動態 Framework,這種限制可能是出于安全的考慮。
換一個角度講,因為 iOS 應用都是運行在沙盒當中,不同的程序之間不能共享代碼,
同時動態下載代碼又是被蘋果明令禁止的,沒辦法發揮出動態庫的優勢,
實際上動態庫也就沒有存在的必要了。

但是,iOS 8/Xcode 6 推出之后,
iOS添加了對動態庫的支持,為什么 iOS 8 要添加動態庫的支持?唯一的理由大概就是Extension 的出現。
Extension 和 App 是兩個分開的可執行文件,同時需要共享代碼,這種情況下動態庫的支持就是必不可少的了。
但是這種動態 Framework 和系統的UIKit.Framework 還是有很大區別。
系統的 Framework 不需要拷貝到目標程序中,我們自己做出來的 Framework 哪怕是動態的,
最后也還是要拷貝到 App 中(App 和Extension 的 Bundle 是共享的),
因此蘋果又把這種 Framework 稱為 Embedded Framework

手洗干凈,挽起袖子干 0_0

第一步:創建Framework工程

運行 XCode -> Cocoa Touch Framework -> 取個名, 語言選擇 Swift -> 創建成功

第二步:基本設置

創建完不急著編寫代碼,先做一些設置:

  1. 修改最低的系統要求,建議當然低一些好

  2. mach -0 type ,即選擇動態庫or靜態庫(甚至Object File)
    想知道這幾種type的區別可以移步
    參考淺談 SDK 開發(一)五種 Mach-O 類型的凜冬之戰
    這里我選擇默認的Dynamic Library即動態庫

  3. Architectures 該編譯選項指定了工程將被編譯成支持哪些指令集,支持指令集是通過編譯生成對應的二進制數據包實現的,如果支持的指令集數目有多個,就會編譯出包含多個指令集代碼的數據包,造成最終編譯的包很大。
    那么指令集是什么呢:

    **iPhone指令集**,蘋果處理器支持兩個不同的指令集:
    32位ARM指令集(armv6|armv7|armv7s)和
    64位ARM指令集(arm64),i386|x86_64 是Mac處理器的指令集,
    i386是針對intel通用微處理器32架構的。
    x86_64是針對x86架構的64位處理器。
    當使用iOS模擬器的時候會遇到i386|x86_64,iOS模擬器沒有arm指令集。
    

    所以我們看看我們需要什么指令集

    1、debug環境下
    設備:arm64(測試機型有限: 6P、5、7 )
    模擬器:iPhone7-Plus:x86_64、iPhone4s:i386
    2、release環境下
    設備:armv7、arm64
    模擬器:i386、x86_64
    

    以上所生成的framework均不包含armv7s, 在 Building Setting 中設置一下 Architectures,在原有基礎上添加一行 armv7s ,如下:



    在原有基礎上增加 armv7s


  4. Build Active Architecture Only 意思是: 該編譯項用于設置是否只編譯當前使用的設備對應的arm指令集
    當該選項設置成YES時,你連上一個armv7指令集的設備,就算你的Valid Architectures和Architectures都設置成armv7/armv7s/arm64,還是依然只會生成一個armv7指令集的二進制包
    Release模式為發布模式,需要支持各種設備指令集,所以設置為NO


  5. Valid Architectures 設置的支持arm指令集。指令集的版本有:armv7/armv7s/arm64。
    假設Architectures設置的支持arm指令集版本只有:arm64時,這時Xcode只會生成一個arm64指令集的二進制包
    所以這里我們都不用改,都包含進來就好了

  6. Dead Code Stripping, 設置為 NO 關閉對代碼中“dead”,“unreachable”代碼過濾

  7. Link With Standard Libraries 設置為 NO 避免重復鏈接

  8. Build 環境 設置build環境為release環境下


第三步:編寫代碼

在此之前,我們先command+B 看看是否成功


build success 并看到Products 下的 文件 XPKit.framework 由紅變黑,說明制作成功,右鍵show in finder

這就是我們的framework,只是里面啥都還沒寫- -。
好了,咱們抓緊寫幾句,饑渴難耐了吧,但是還是先跟著我來吧,弄明白了再去寫自己的代碼,少踩好多坑
創建一個Manager類繼承自 NSObject

寫上這么個 func

@objc public class XPManager: NSObject {
    
    @objc public func sayHello(){
        print("XPKit-->: hello")
    }
    
    public func sayWorld() {
        print("XPKit-->: world")
    }
    
    @objc func saySwift() {
        print("XPKit-->: Swift")
    }
}

完畢, command + B ,報錯了!來看看為啥:

ld: symbol dyld_stub_binder not found (normally in libSystem.dylib).  Needed to perform lazy binding to function __T0s23_ContiguousArrayStorageCMa for architecture i386
clang: error: linker command failed with exit code 1 (use -v to see invocation)

這里提示我們少了一個系統庫libSystem.dylib,我也不知道為啥,那我們就給加上唄,來到PROJECT->Build Phases->Link Binary With Libraries->+ 加上libSystem.tbd


這時候再回來編譯,發現Build Success。目的達到了 0-0!

第四步:測試寫好的Framework

1. 先來測試Swift項目調用swift的framework

先到Products 下的 XPKit.framework 右鍵 show in finder找到 XPKit.framework

緊接著 create 一個 swift 工程app single view app,并將??的XPKit.framework拖到工程中,記得勾選 copy if needed

import XPKit

然后 command + 左鍵 進入,可以看到暴露出來能用的方法

image.png

這里我們能發現我們寫了public 修飾的 都暴露出來供app調用

import UIKit
import XPKit
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let manager = XPManager()
        manager.sayHello()
        manager.sayWorld()
    }
}

command + R Run一下發現報錯了

dyld: Library not loaded: @rpath/XPKit.framework/XPKit
  Referenced from: /Users/midoo/Library/Developer/CoreSimulator/Devices/8F8ADF9B-D5DD-45A1-B925-03BCBB11D9FF/data/Containers/Bundle/Application/C7D5F85A-DDC8-4BC7-964A-FDC4B5154C5A/SwiftInvokDemo.app/SwiftInvokDemo
  Reason: image not found
(lldb) 

這里因為我們制作的frameworkdynamic library動態的,所以我們到Project->General->滑到最下方


選中 linked frameworks and libraries 中的 XPKit.framework-號 刪除,再在上方 Embedded Binaries+號XPKit.framework 加回來,發現上下都有了,如:

這時候再command + R Run,就不報錯了,并且成功打印:

2. 再來測試Swift項目調用swift的framework

同理 create 一個 OC 的 app,拖進我們的 XPKit.framework 并在 Project->General -> Embedded Binaries 下添加進去
這個時候就要導入#import <XPKit/XPKit-Swift.h> 而不是 #import <XPKit/XPKit.h>
#import <XPKit/XPKit-Swift.h>OC 項目導入 Swift Framework 時自動產生的文件, 給我們展示可以用哪些接口
command + 左鍵 點到這個文件里去可以看到

image.png

這時候我們就會發現,XPManager 我們的類暴露出來了,但是方法只有一個sayHello()

所以敲黑板,劃重點

我們制作 swift framework 的時候,一定要注意可用性,因為難免會遇到讓OC調用的時候。
所以要在暴露在外給人家用的話,一定要寫上修飾詞 `@objc` 與 `public` 缺一不可

而我們的類,即 Class ,繼承了 NSObject 那么即使不寫 @objc , 也是OK的,但是屬性func一定要寫

可以做個測試,在framework工程中寫:

public class XPTest: NSObject{
    public func sayWorld() {
        print("XPKit-->: world")
    }
    @objc public func sayHello(){
        print("XPKit-->: hello")
    }
}

重新 build , 注: (每次重新build才會更新framework)
并刪除 OC-App 下的 framework 。重新拖到項目中并添加到Embedded Binaries
發現,XPKit-Swift.h下暴露的是這樣的:

@interface XPTest : NSObject
- (void)sayHello;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

所以如果是 Class 繼承了 NSObject 那么即使不寫 @objc , 也是OK的,但是屬性func一定要寫
這里就是簡單介紹,還有些細節你們可以慢慢測試

第五步:創建可以直接調試的工程

如果你都跟著步驟寫了以后,會發現一直 Build, 一直拖,一直添加,太繁瑣。所以咱們可以這樣創建一個 workspace 將兩個項目都包含進去

  1. 新建項目或者我們把原先的添加了的framework先丟掉,左上角 File -> save as workspace

  2. 關閉這個.xcodeproj 文件重新打開這個 .xcworkspace

  3. 將我們的 XPKit 項目 拖到 workspace 中,與 Demo-App 并行


    這個時候就會發現,我們就有兩個項目了可以分別 build

  4. Demo工程文件 -> General ->embedded binaries 中將 XPKit 下的 .framework 加進來

  5. 試一試,scheme 選擇 Demobuild 一下是success
    我們再來到我們的 XPManager.h ,添加幾行代碼(當測試)

public class XPHomeViewController: UIViewController{
    @objc public let size = CGSize(width: 40, height: 40)
    @objc public var point: CGPoint = CGPoint(x: 20, y: 20)
    public var content: String?
    @objc public var textColor: UIColor?
    
    public override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.red
    }
    @objc public func sayHello(){
        print("XPKit-->: hello")
    }
    
    public func sayWorld() {
        print("XPKit-->: world")
    }
}

當然還是先build一下我們的XPKit,否則Demo里的framework不更新吶,再到OC項目中import并點進去看看。
注:我這里試了幾次總是沒有自動補全,那就自己手寫 import地址吧

#import <XPKit/XPKit-Swift.h>

點進去發現

@interface XPHomeViewController : UIViewController
@property (nonatomic, readonly) CGSize size;
@property (nonatomic) CGPoint point;
@property (nonatomic, strong) UIColor * _Nullable textColor;
- (void)viewDidLoad;
- (void)sayHello;
- (nonnull instancetype)initWithNibName:(NSString * _Nullable)nibNameOrNil bundle:(NSBundle * _Nullable)nibBundleOrNil OBJC_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)aDecoder OBJC_DESIGNATED_INITIALIZER;
@end


SWIFT_CLASS("_TtC5XPKit9XPManager")
@interface XPManager : NSObject
- (void)sayHello;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end


SWIFT_CLASS("_TtC5XPKit6XPTest")
@interface XPTest : NSObject
- (void)sayHello;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

哇,完美,并且,可以直接在項目里用。那么邊寫邊測,還能設斷點,完美~跟平時寫app沒兩樣,其實還差最后一步 >_<.
當你調用并且運行,發現又報錯了

dyld: Library not loaded: @rpath/libswiftCore.dylib
  Referenced from: /Users/midoo/Library/Developer/Xcode/DerivedData/OCInvokDemo-fvmdsbbzuqqnjnbswwgpcidptord/Build/Products/Debug-iphonesimulator/XPKit.framework/XPKit
  Reason: image not found

并且這次我們已經加到 Embedded Binaries 中了,原因是,如果我們在OC項目中引用swift framework,還需要到 build setting 中設置如下:

image.png

到此為止才是真正完美 ~

第五步:創建通用包

之前說過iphone指令集, 真機與模擬器是不同的,所以編譯出來的包也是不通用的,現在我們要做通用的包提供別人使用。

  • 切換到XPKit, 選擇任意一個模擬器build一下,
  • 再選擇Generic iOS Device ,build 一下
  • show in finder,我們只看release包的,不要關閉finder
  • 我們需要用到終端來創建通用包,打開我們的終端
  • cd 到一個文件目錄下,為了存放我們制作的包
cd /Users/midoo/Desktop/測試文件 
  • 輸入lipo -create空格以后,分別到 release-iphoneosrelease-iphonesimulator 下的 XPKit.framework -> 選擇XPKit二進制文件,拖動到 終端下

  • 緊接在后面寫上 -output + 你的包名 ,回車
    注:我選擇的文件夾下面有 XPKit 文件名,重復了 - -。創建失敗...所以換了個文件夾
  • OK,這個時候還沒完,只是創建了一個通用的二進制文件,還得把其他東西給他加上,回到release-iphoneosrelease-iphonesimulator,選擇任意一個文件夾,整個復制出來
  • 并將制作好的二進制文件拖出來 替換掉


    image.png
  • 還沒完!!!再到另一個文件夾,將指令集文件copy到這個,我們這個新的文件夾下



    保證這些都包含在內,那么這個framework包才算制作完成


  • 到此為止我們得到了我們想要的通用包,按照相同的方法,拖到工程中,引入到Embedded Binaries,可以調試了

第六步:用Shell腳本創建通用包

創建通用包用到的次數不多,上面的方法夠用了,但是如果你還是覺得不方便、很繁瑣。那你可以跟我這樣做

  • 選擇XPKit 工程點擊左下角 +
  • 創建一個 Aggregate。去個名字,類似 CommonBuilder
    image.png
  • 選中 CommonBuilder -> Build Phases -> 添加New Run Script Phase
  • 在編輯器內輸入我們的腳本代碼,請全部復制,黏貼,記得修改第二步引號內的內容為你的framework name
# Merge Script

# 1
# Set bash script to exit immediately if any commands fail.
set -e

# 2
# Setup some constants for use later on.
FRAMEWORK_NAME="Your framework name" 

# 3
# If remnants from a previous build exist, delete them.
if [ -d "${SRCROOT}/build" ]; then
rm -rf "${SRCROOT}/build"
fi

# 4
# Build the framework for device and for simulator (using
# all needed architectures).
xcodebuild -target "${FRAMEWORK_NAME}" -configuration Release -arch arm64 -arch armv7 -arch armv7s only_active_arch=no defines_module=yes -sdk "iphoneos"
xcodebuild -target "${FRAMEWORK_NAME}" -configuration Release -arch x86_64 -arch i386 only_active_arch=no defines_module=yes -sdk "iphonesimulator"

# 5
# Remove .framework file if exists on Desktop from previous run.
if [ -d "${HOME}/Desktop/${FRAMEWORK_NAME}.framework" ]; then
rm -rf "${HOME}/Desktop/${FRAMEWORK_NAME}.framework"
fi

# 6
# Copy the device version of framework to Desktop.
cp -r "${SRCROOT}/build/Release-iphoneos/${FRAMEWORK_NAME}.framework" "${HOME}/Desktop/${FRAMEWORK_NAME}.framework"

# 7
# Replace the framework executable within the framework with
# a new version created by merging the device and simulator
# frameworks' executables with lipo.
lipo -create -output "${HOME}/Desktop/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${SRCROOT}/build/Release-iphoneos/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${SRCROOT}/build/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}"

# 8
# Copy the Swift module mappings for the simulator into the
# framework.  The device mappings already exist from step 6.
cp -r "${SRCROOT}/build/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule/" "${HOME}/Desktop/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule"

# 9
# Delete the most recent build.
if [ -d "${SRCROOT}/build" ]; then
rm -rf "${SRCROOT}/build"
fi

如圖:


  • scheme 選擇 CommonBuilder,任意模擬器,編譯,報錯了看看報了什么錯
Command /bin/sh failed with exit code 65

你們以后看到這些不用慌,網上看,信息都在上面

=== BUILD TARGET XPKit OF PROJECT XPKit WITH CONFIGURATION Release ===

Check dependencies
No architectures to compile for (ARCHS=x86_64 i386, VALID_ARCHS=arm64 armv7 armv7s).

** BUILD FAILED **

分析一下,這里都是我們提到過的指令集。 VALID_ARCHS=arm64 armv7 armv7s 這就是我們開始在 Build Setting ->Valid Architectures 中設置的內容,很明顯,意思是腳本里,要制作包含 x86_64i386的包,但是我們的Valid Architectures 中沒有。
那么解決問題就方便了,分別添加x86_64i386


編譯成功~
來到桌面我們發現XPKit.framework,已經靜悄悄的在桌面上了,這就是我們的通用包

總結一下

好了,我們總結一下,本篇簡單的介紹了一下

  1. 如何用Swift編寫,OC項目Swift項目 都能用的 dynamic framework
  2. 如何正確的調試我們的framework
  3. 如何制作通用的 framework 包
為了能讓OC項目也能調用,你還記得 @objc 和 public 嗎 ^_^

我們的 framework 如果需要導入其他第三方庫,該怎么做
本來也想寫篇文章,有點懶,大家先可以看看
Swift + framework 的制作(基于pod管理的workspace)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。