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 的相关配置。

生成 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.

资源