自建 iOS 靜態庫並用 pod 管理

Ashen發表於2018-03-20

自建 iOS 靜態庫並用 pod 管理

2018-04-04 更新:新增 pod lib create 的方式建立 framework,推薦使用該方式。
Xcode: 9.3
Swift: 4.1
CocoaPod: 1.4.0
複製程式碼

序言

本文將主要討論如下幾個問題:

    1. OC 和 Swift 混編的形式建立支援多種架構的靜態庫(.framework);
    1. 靜態庫引用其他靜態庫(.framework 或 .a);
    1. 靜態庫新增資原始檔(xib,image,mp3 等);
    1. 如何獲取自建資源的 Bundle;
    1. pod 管理靜態庫;
    1. pod lib create 建立 framework。

建立靜態庫 framework

最近公司由於和其他公司建立了各種合作關係,“建立 SDK”工作被提上了日程,由於之前沒有自己做過,在生成 framework 時踩了一些坑,在這裡記錄下來以做總結,若碼友能用得上則不勝開心。 工作中難免會遇到這種情況,想把某個功能包裝起來給其他人用,但是出於某種原因又不想公開自己的實現方式,這時就需要靜態庫(framework 或 .a)了。(PS:.a 和 .framework 的區別可以看下這篇文章:iOS開發-.a與.framework區別?

建立靜態庫

XCode -> File -> New -> Project -> Cocoa Touch Framework
複製程式碼

建立

建立 SJTutorialSDK,並新建測試類名為 HelloWorld(Swift)、HelloOC(OC),目錄如下:

├── SJTutorialSDK
│   ├── HelloOC.h                -> 暴露的 OC 類
│   ├── HelloOC.m
│   ├── HelloWorld.swift         -> 暴露的 Swift 類
│   ├── Info.plist
│   └── SJTutorialSDK.h          -> 當前靜態庫標頭檔案
└── SJTutorialSDK.xcodeproj
    ├── project.pbxproj
    ├── project.xcworkspace
複製程式碼
    1. 對於 Swift,對外暴露的檔案需要用 public(或 open)修飾,並且要繼承自 NSObject(或其子類);
    1. 對於 OC,對外暴露的檔案需要在“Build Phases -> Headers -> Public”新增相應標頭檔案(例:HelloOC.h),並在 framework 的標頭檔案(例:SJTutorialSDK.h)中新增對該標頭檔案的引用(例:#import <SJTutorialSDK/HelloOC.h>);
    1. 如果當前 framework 引用了第三方 framework,需要在標頭檔案(例:SJTutorialSDK.h)中新增對第三方標頭檔案的引用(例:#import <SJDemoSDK/Animals.h>)。

建立完成後的標頭檔案如下:

//  SJTutorialSDK.h
//  SJTutorialSDK

#import <UIKit/UIKit.h>
FOUNDATION_EXPORT double SJTutorialSDKVersionNumber;
FOUNDATION_EXPORT const unsigned char SJTutorialSDKVersionString[];

// 第三方庫的標頭檔案
#import <SJDemoSDK/Animals.h>

// 對外暴露的標頭檔案(僅限於 OC,Swift 新增 public 後會自動匯入)
#import <SJTutorialSDK/HelloOC.h>
複製程式碼

Build Phases 配置如下:

header

Build Settings 中 Mach-o Type 設定為 Static Library。 至此,靜態庫.framework 建立完畢。

支援多種架構

我們開發過程中經常提到的 arm64,x86_64 具體是什麼東西呢?這裡可以參考這篇文章: iOS 中的 armv7,armv7s,arm64,i386,x86_64 都是什麼。 簡單點來說:模擬器 32/64 位處理器分別需要 i386/x86_64 架構,真機 32/64 位處理器分別需要 armv7(armv7s)/arm64 架構。 所以,如果你構建的靜態庫只需要支援 iPhone 5S 及以上(或 iPad mini2 及以上)的真機和模擬器,那麼你的靜態庫將只需支援 arm64 和 x86_64 架構。

  • 1.調整到 Release(釋出)模式:Edit Scheme -> Run -> Info -> Build Configuration -> Release;

    release mode

  • 2.分別使用真機、模擬器編譯,生成對應的 SJTutorialSDK.framework;

  • 3.合併 frameworks

# 檢視靜態庫支援的架構
lipo -info xxx

# 合併 xxx1 和 xxx2,最後的檔案支援兩者支援的所有架構
lipo -create xxx1 xxx2 -output xxx1
eg:
lipo -creat Release-iphoneos/SJTutorialSDK.framework/SJTutorialSDK Release-iphonesimulator/SJTutorialSDK.framework/SJTutorialSDK -output Release-iphoneos/SJTutorialSDK.framework/SJTutorialSDK
複製程式碼

生成 framework 時踩過的坑:-output 的檔案是 xxx,而不是 xxx.framework

  • 4.檢視最後的靜態庫支援架構:
Architectures in the fat file: /Users/shijian/Desktop/SJTutorialSDK.framework/SJTutorialSDK are: x86_64 arm64
# congratulations
複製程式碼

新增資原始檔

在 iOS 中,可以通過 Bundle 檔案管理資原始檔(圖片,語音,視訊,plist,xib,storyboard等),Bundle 檔案實際上就是個普通的資料夾,只是在名字中新增了 .Bundle 的字尾而已。 對圖片的命名最好新增上 @3x/@2x,這樣系統會自動放在對應的位置,不需要我們額外的操作。

    1. 新建資料夾 xxx,並新增相應資源,然後更名為 xxx.Bundle,也可以直接新建 xxx.Bundle,滑鼠右鍵 -> 顯示包內容 -> 新增相應檔案;
.
└── SJTutorialRes.Bundle
    ├── alipay@3x.png
    └── wechat@3x.png
複製程式碼
  • 2.對於獲取自建 bundle 的一些總結 如何獲取自建 bundle 是一個很難說明的問題,對於不同的寫法,獲取自建 bundle 的方式不一樣(主要體現在生成的 bundle 檔案在應用包中的位置可以千差萬別)。比如通過 CocoaPod 的 resources 的方式引入的會在根目錄下,如果是通過 CocoaPod 中的framework 下的,會在./Frameowrks/xxx.framework 下。那麼有沒有辦法來確定 bundle 的具體位置呢?當然有。 Bundle 的官方說明:A representation of the code and resources stored in a bundle directory on disk. 說白了我們可以把 Bundle 理解為硬碟上的路徑的一種特殊表示方式。舉個例子說明,iOS 編譯生成 app 的目錄結構如下:
.
├── Base.lproj
│   ├── LaunchScreen.nib
│   └── Main.storyboardc
│       ├── Info.plist
│       ├── UIViewController-vXZ-lx-hvc.nib
│       └── vXZ-lx-hvc-view-kh9-bI-dsS.nib
├── Frameworks
│   ├── HHMedicSDK.framework
│   │   ├── ControlView.nib
│   │   ├── HHMedicSDK
│   │   ├── HHPB.bundle
│   │   │   ├── HHCameraCancleCap@2x.png
│   │   │   ├── camear_del_btn_normal@2x.png
│   │   ├── HMSDK.bundle
│   │   │   ├── angle-mask.png
│   │   │   └── success@3x.png
│   │   ├── Info.plist
│   └── libswiftsimd.dylib
├── HHMedicSDK_Example
├── Info.plist
├── PkgInfo
├── _CodeSignature
│   └── CodeResources
└── libswiftRemoteMirror.dylib
複製程式碼

注:本文中的 bundle 指自建的資原始檔, Bundle 指代 swift 的類(對應 OC 中的 NSBundle)。 我們可以知道 Bundle.main 指代當前根目錄,那麼如何獲取 camear_del_btn_normal@2x.png 這張圖片呢?通過路徑我們可以知道其路徑為 ./Frameworks/HHMedicSDK.framework/HHPB.bundle/camear_del_btn_normal@2x.png。那麼一種簡單的獲取方式便明瞭了:Bundle.main.url(forResource: "Frameworks/HHMedicSDK.framework/HHPB.bundle/camear_del_btn_normal@2x", withExtension: "png"),當然也可以通過層層的 urlForResoure 來讀取。

轉過來再想想,獲取 bundle 的方式就簡單了。我們可以事先不用苦苦思考資源 bundle 的具體生成位置,先 debug 獲取 app,然後再開啟 app 檢視各個 Bundle 對應位置,最後就可以再回過來重寫鍵入獲取方式,大功告成。

  • 3.新建方法讀取 Bundle 中的圖片
class HHResManager {
    static func getImg(_ name: String) -> UIImage? {
        let url = Bundle.main.url(forResource: "Frameworks/HHMedicSDK", withExtension: "framework") ?? URL(fileURLWithPath: "")
        let bundle = Bundle(url: url)?.url(forResource: "HMSDK", withExtension: "bundle") ?? URL(fileURLWithPath: "")
        return UIImage(named: name, in: Bundle(url: bundle), compatibleWith: nil)
    }
    
    static func xibBundle() -> Bundle {
        let apath = Bundle.main.path(forResource: "Frameworks/HHMedicSDK", ofType: "framework")!
        return Bundle(path: apath)!
    }
    
    static func getAudio(_ name: String) -> URL? {
        let url = Bundle.main.url(forResource: "Frameworks/HHMedicSDK", withExtension: "framework") ?? URL(fileURLWithPath: "")
        guard let bundle = Bundle(url: url)?.url(forResource: "HMSDK", withExtension: "bundle") else { return nil }
        guard let path = Bundle(url: bundle)?.path(forResource: name, ofType: "mp3") else { return nil }
        return URL(string: path)
    }
}
複製程式碼

至此,當前建立的靜態庫 SJTutorialSDK 有如下特性:支援 OC 和 Swift 混編,引用了其他第三方庫,支援資原始檔讀取,支援多種架構。

pod 管理靜態庫

如果自己的靜態庫是私有的,可以跳過 trunk 環節,直接在自己的程式碼倉庫中建立好倉庫,然後配置相應的 podspec 檔案,pod 引用時指定對應的路徑即可。但大多數情況下還是要給其他人使用的,我們當然也可以不通過 trunk,直接在引用時指定地址來使用當前倉庫,但是這樣用起來總是不那麼直觀。

# 通過名稱和指定地址使用
pod 'SJTutorialSDK',:git => "git@code.XXX/SJTutorialSDK.git"
# 通過名稱
pod 'SJTutorialSDK'
複製程式碼

兩者對比起來,高下立判。所以建議還是通過 trunk 來 push 自己的程式碼。

配置 podspec 檔案

如何在 github 上新建倉庫,網上已經有很多的教程,這裡不再贅述,這裡主要談談配置 podspec 檔案。

podSpec 官方解釋: A Podspec, or Spec, describes a version of a Pod library. ,其實就是一個描述 pod 庫的資訊(版本,依賴,作者,描述,系統庫等)的檔案。

    1. 配置 SJTutorialSDK.podspec
Pod::Spec.new do |s|
  s.name         = "podSDK"
  s.version      = "0.0.1"
  s.summary      = "當前庫的總結。"

  s.description  = <<-DESC
                    描述檔案
                   DESC
  s.homepage     = "http://EXAMPLE/podSDK"
  s.license      = "MIT"
  s.author             = { "shmily" => "shmilyshijian@foxmail.com" }
  s.platform     = :ios, "9.0"
  s.source       = { :git => "https://github.com/515783034/podSDK.git", :tag => "#{s.version}" }
  s.resources = "podSDK/Resources/*.*"

  # 本庫提供的framework靜態庫
  s.vendored_frameworks = 'podSDK/Sources/*.framework'

  #################
  # 依賴的系統動態庫
  # s.frameworks = "SomeFramework", "AnotherFramework"
  # 依賴的系統靜態庫
  # s.libraries = "iconv", "z"

  # 本庫提供的 .a 靜態庫
  #s.vendored_libraries  = 'podSDK/Sources/*.a'

  # 本庫新增的第三方依賴庫
  #s.dependency "SJLineRefresh", "~> 1.4"
end
複製程式碼

關於 podspec 的內容,可以檢視我之前寫過的文章:建立公共/私有pod --podspec,這裡也不再過多贅述。 由於當前倉庫只是引用了幾個靜態庫,所以 sources 可以不配置,只需要配置 vendored_frameworks 即可。

    1. 驗證配置是否合理
pod lib lint
# 如果驗證中有警告,可以新增引數 --allow-warnings 可以忽略
複製程式碼
    1. trunk push
pod trunk push SJTutorialSDK.podspec
# 忽略警告的方式同上
複製程式碼
    1. 版本控制(tag)
git tag -m "desc" 1.0.0
git push --tag
# podspec 中修改對應的 s.version
複製程式碼

上傳成功,enjoy

 ?  Congrats

 ?  podSDK (0.0.1) successfully published
 ?  March 19th, 05:04
 ?  https://cocoapods.org/pods/podSDK
 ?  Tell your friends!
複製程式碼

使用 pod lib create 建立第三方庫

上面講到我們通過 Cocoa Touch Framework 建立 framework,然後再上傳到 CocoaPod 供別人使用,但是在使用這種方式的過程中遇到了幾個問題。 使用 Xcode 建立 framework,為了測試我們當然可以通過在當前工程的基礎上再 File -> New -> Target -> Single View App 對 framework 進行測試,但是在實際使用中遇到了幾個問題,通過拖拽 framework 的方式與 CocoaPod 管理的方式有些出入,會導致部分功能有差異,比如上面提到的 Bundle 問題,在二者表現上完全不一樣,再比如在某些 Build Settings 的設定上也會有一些差別,總之這不是個友好的方式。 既然使用了 CocoaPod 管理 framework,那麼索性也用 CocoaPod 來"生成" Framework 吧,都是同一套東西,用起來也不會有各種各樣匪夷所思的問題。

建立 framework

官方 pod lib create 命令使用詳解:Using Pod Lib Create 使用起來非常簡單,只需要 通過 pod lib create xxxx 然後選擇相關配置即可。

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 ]  (是否包含 demo)
 >
yes
Which testing frameworks will you use? [ Quick / None ]    (測試框架)
 > None

Would you like to do view based testing? [ Yes / No ]
 > No

Running pod install on your new library.
複製程式碼

目錄大致如下(其中篇幅原因部分不相關目錄已移除):

.
├── Example
│   ├── Podfile
│   ├── Podfile.lock
│   ├── Pods
│   │   ├── Headers
│   │   ├── Local Podspecs
│   │   ├── Manifest.lock
│   │   ├── Pods.xcodeproj
│   │   │   ├── project.pbxproj
│   │   └── Target Support Files
│   ├── SJPlcSDK
│   │   ├── AppDelegate.swift
│   │   ├── Base.lproj
│   │   │   ├── LaunchScreen.xib
│   │   │   └── Main.storyboard
│   │   ├── Info.plist
│   │   └── ViewController.swift
│   ├── SJPlcSDK.xcodeproj
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace
│   ├── SJPlcSDK.xcworkspace
├── LICENSE
├── README.md
├── SJPlcSDK                   (原始碼和資源等)
│   ├── Assets
│   └── Classes
│       └── ReplaceMe.swift
├── SJPlcSDK.podspec           (倉庫配置檔案)
└── _Pods.xcodeproj
複製程式碼

新增程式碼後,配置 podspec 檔案(上文已給出,不贅述),可開啟 ./Example/SJPlcSDK.xcworkspace 進行測試。

配置並生成

可在 ./Pods -> targets -> SJPlcSDK -> Build Settings 下進行 framework 的相關配置。

自建 iOS 靜態庫並用 pod 管理
生成 framework 也比較簡單,debug 成功後在 ./Pods/Products 資料夾下的 SJPlcSDK.framework 即為所需,切換到模擬器再次 debug,最後通過 lipo -create -output 命令合併即可(詳細操作請看上文)。

釋出 framework

有了 framework 和相關資原始檔後,釋出和上面相同。提交程式碼,打 tag,pod trunk push。

總結

靜態庫的建立並沒有太複雜的操作,主要步驟如下:

    1. 建立 Cocoa Touch Framework,新增對應檔案;
    1. 暴露出需要對外公開的標頭檔案;
    1. 新增資原始檔 Bundle,並提供相應的獲取方法;
    1. 在真機和模擬器下分別編譯,然後合併 framework;
    1. pod trunk push.

資源

建立靜態庫demo: SJTutorialSDK

靜態庫使用 demo: podSDK

iOS開發-.a與.framework區別?

iOS 中的 armv7,armv7s,arm64,i386,x86_64 都是什麼

建立公共/私有pod --podspec

相關文章