1. 基础使用

Xcode 自带了创建 framework 的功能,但是使用起来并不是特别顺手。pod 附带的pod lib create命令能快速创建 framework 脚手架工程,方便我们使用。官方文档 using-pod-lib-create

注:本 Demo 中项目名称为 SJSwiftSDK, 请实际使用中根据自身需要进行相应调整。

1.1. 创建(pod lib lint)

执行pod lib create you_sdk_name,会创建相关工程。 pod 相关命令都可以通过添加 --verbose 后缀来显示详细信息。

注:最近使用时发现此命令可以需要科学上网。

# 执行脚本
pod lib create #your_sdk_name#

# 根据自身情况设置相应参数
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 ]
 > yes
Which testing frameworks will you use? [ Quick / None ]
 > None
Would you like to do view based testing? [ Yes / No ]
 > No

命令执行完毕后 Xcode 会自动打开到当前项目,项目结构比较简单,主要包含了 podspec 配置文件, example 项目,源代码。工程目录结构如下

├── Example
│   ├── Podfile
│   ├── Pods
│   │   ├── Headers
│   │   ├── #忽略部分文件#
│   ├── SJSwiftSDK # demo 项目,用于测试 SDK 功能
│   │   ├── AppDelegate.swift
│   │   └── ViewController.swift
├── README.md
├── SJSwiftSDK # SDK 路径,所有 SDK 内容存放在这里
│   ├── Assets # SDK 资源路径(podspec 可配置其他路径)
│   └── Classes # SDK 代码路径,podspec 可配置其他路径
│       └── ReplaceMe.swift
├── SJSwiftSDK.podspec # SDK 相关配置文件
└── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj

这里以创建一个 SJDemoViewController 为例,简单说一下创建过程。在SJSwiftSDK/Pods/Development Pods/SJSwiftSDK (Xcode 中看到的文件路径,实际的文件夹路径应该是 SJSwiftSDK/SJSwiftSDK/Classes/) 下创建文件(+N), 创建完成后目录结构如下

 pwd: SJSwiftSDK/SJSwiftSDK/Classes
 
 .
├── Example
│   ├── Podfile
│   ├── Podfile.lock
│   ├── SJSwiftSDK
│   │   ├── AppDelegate.swift
│   │   └── ViewController.swift
├── SJSwiftSDK
│   ├── Assets
│   └── Classes
│       ├── ReplaceMe.swift
│       └── SJDemoViewController.swift # 当前新建文件
├── SJSwiftSDK.podspec
└── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj

创建文件比较简单,但有一点儿需要注意,新建文件的路径要和 SJSWiftSDK.podspec 文件中的source_files配置保持一致。例如,podspec 中的相关配置是s.source_files = 'SJSwiftSDK/Classes/**/*',那么新建文件时,也应在 Classes 文件夹下,否则执行pod update命令后 Classes 文件夹以外的文件无法被加载进 SDK。可以在Finder中检查文件的创建路径是否正常。

1.2. 外部 SDK 依赖

使用 CocoasPod 创建动态库,依赖其他库的方式非常简单,大题分为两种方式依赖。

  1. dependency dependency 顾名思义为依赖关系,当自身的库需要有外部依赖时,可以直接使用dependency,pod 会自动帮我们下载对应的依赖文件,使用方式也比较简单, s.dependency 'pod_name' 'version',使用这种方式的好处时,不需要关系依赖的库有其他的依赖,pod 会处理所有的依赖关系。

    # podsepc 添加依赖如下
    s.dependency 'Alamofire'
    
  2. vendored_frameworks 这种方式是指定路径后,会在指定的路径下遍历所有的 framework 并加入依赖关系,若加入的 framework有其他的依赖,比如libc++,SystemConfiguration.framework等,需要我们手动加入这些依赖关系,资源文件也需要手动处理,关于资源文件的具体处理再下一节细讲。

    # podspec 添加 vendored_frameworks 如下
    # SDK 文件夹下所有的 framework 被添加
    s.vendored_frameworks = 'SDK/*.framework'
    
    #
    # 如有资源文件,需要手动添加(具体路劲依据实际情况)
    s.resources = ['resources/*.*']
    
    # 若有其他系统库依赖,需要添加
       # 依赖系统动态库
    s.frameworks = 'SystemConfiguration', 'WebKit'
      
    # 依赖系统静态库
    s.libraries    = 'z', 'sqlite3.0', 'stdc++', 'c++'
    

1.3. Bundle 资源加载与使用

SDK 除了代码之外,难免会有一些资源文件(图片,音频,视频,plist等)需要被加载,资源文件如何处理的呢?

对于资源文件的处理,这里分两种情况:静态库和动态库(原因:静态库和动态库的编译方式不同,导致资源存储路径有差异)。

  • 静态库,原本的资源文件(eg: resource.bunle)放在 framework 下,但是生成 App 后,由于静态库被吸附,导致 bundle 资源文件丢失,无法查找到对应文件,需要手动加入依赖文件。即在podsepc中的s.resources中,指定该静态库资源的位置。
  • 动态库,资源文件也放在 framework 下,被一同拷贝到 mainBunle 的 Frameworks 文件夹下,资源可以直接使用。

例如下图,OCDynamicSDK.framework 和 SJSwiftSDK.framework 都是动态库,各自需要的 bundle 资源在各自的 framework 下,app 编译成功后,路径为mainBunle/Frameworks/OCDynamicSDK.framework/dynamic.bundle ;SJOCSDK.framework 为静态库,编译后此文件消失(被吸附到 app 中),framework 下若有 bundle文件,会被丢弃,所以需要脱离当前 framework 手动指定 bundle,即下图中的resources/OCSDKBundle.bundle,路径为mainBundle/OCSDKBundle.bundle。此处的 mainBunle 表示 [NSBundle mainBundle]获取的路径,即 app 的根路径。 查看 app 内容:XCode 项目列表-> Products/xxx.app -> 右键 Show in Finder -> 显示包内容,即可看到 app 所有内容及文件结构。

.
├── OutputSDK.podspec
├── SDK
│   ├── OCDynamicSDK.framework # 动态库,直接使用 bundle 资源
│   │   ├── OCDynamicSDK
│   │   ├── dynamic.bundle # 该库的 bundle 资源,运行时相对位置固定
│   │   │   └── shmily2@3x.png
│   ├── SJOCSDK.framework # 静态库, 需要手动添加 bundle 资源
│   │   └── SJOCSDK
│   └── SJSwiftSDK.framework # 动态库,直接使用资源
│       ├── Headers
│       ├── SJResource.bundle
│       │   └── ashen_23@3x.png
│       ├── SJSwiftSDK
│       └── filter.bundle
│           └── ashen@3x.png
├── resources # 静态库中的资源需要在此指定
│   └── OCSDKBundle.bundle
│       └── shmily@3x.png
└── upgrade.sh

接下来我们讲讲如何创建,首先我们创建资源文件夹,pod lib create xx命令为我们默认创建了 Assets文件夹,我们可以使用此文件夹存放相关资源(当然也可以根据自身爱好新建诸如 Resources等),需要在SJSWiftSDK.podspec中进行配置s.resources = 'SJSwiftSDK/#your_resource_folder#/*.*'。一般来说,为了方便管理,我们会创建bundle(本质也是文件夹添加了个后缀,新建文件夹后,重命名为 xxx.bundle 即可)进行图片的管理,想即时生效需要执行 pod update命令。目录如下

pwd: SJSwiftSDK/SJSwiftSDK

.
├── Assets # 资源文件夹
│   └── SJResource.bundle # 图片 bundle
│       └── ashen_23@3x.png
└── Classes # 源代码文件夹
    ├── ReplaceMe.swift
    └── SJDemoViewController.swift

那么代码中如何加载当前资源呢? 查看Products/SJSwiftSDK_Example.app我们可以看到 framework 及 bundle 的存储路径,通过 Bundle.main()即可获取当前路径,再拼接相应路径可以。当然也可以通过获取 SDK 中某个 class 的 bundle(eg: Bundle(for: SJAssets.self)),然后再获取相应资源文件。资源文件在 App 中的路径如下。

pwd: Build/Products/Debug-iphonesimulator/SJSwiftSDK_Example.app

.
├── Base.lproj
│   └── Main.storyboardc
├── Frameworks # framework 存放路径
│   ├── SJSwiftSDK.framework 
│   │   ├── SJResource.bundle # 资源路径
│   │   │   └── ashen_23@3x.png
│   │   ├── SJSwiftSDK
├── SJSwiftSDK_Example
└── _CodeSignature
    └── CodeResources

这里给出加载图片资源的简单 demo。

/// 加载图片资源
/// - Parameters:
///   - bundleName: bundle名(eg: SJResource)
///   - imgName: 图片名(eg: ashen_23, ashen23@3x)
public static func loadAsset(_ bundleName: String, imgName: String) -> UIImage? {
    let sdkBundle = Bundle(for: SJAssets.self)
    // 若只有一个 bundle,bundleName 可以为"",系统会自动创建
    guard let bundlePath = sdkBundle.path(forResource: bundleName, ofType: "bundle") else {
        print("当前 bundle(\(bundleName)) 不存在")
        return nil
    }
    let resBundle = Bundle(path: bundlePath)
    return UIImage(named: imgName, in: resBundle, compatibleWith: nil)
}

1.4. 发布 SDK

在发布之前,需要先检查下 SDK 是否存在问题,pod lib lint命令可以很方便的进行检查。 在首次执行此命令前,需要配置仓库地址( 使用 github 需要配置此参数,私有仓库不需要)以及 swift 版本号。

对于私有仓库,只需将打包出的 framework 拷贝到私有仓库,提供其他人下载即可。一般来说生成 SDK 需要闭源,理论上不应该直接将该 SDK 源代码直接放在 github 的 public 仓库,所幸现在 github 私有仓库已免费,建议保存在私有仓库下。

若仓库地址存放在 github 上,需要修改 podspecs.source,调整到自己的仓库地址,为了能直接提交到 github,还需要调整相应的.git文件夹(clone github 仓库内容,替换掉 SDK 下默认生成的 .git 文件夹即可);通过修改 s.swift_versions 可以调整 swift 版本号。

 # 仓库地址
s.source           = { :git => 'https://github.com/515783034/SJSwiftSDK.git', :tag => s.version.to_s }

 # Swift 版本号
s.swift_versions   = '5'

执行 pod lib lint命令,若出现SJSwiftSDK passed validation., 恭喜你已经成功了99%。此时已说明 SDK 代码部分已创建完毕,现在需要做的是打包生成 framework,并提交供用户下载。

  • 打包生成 framework 调整 Xcode的设置,Edit Schemes -> Run -> Info -> Build Configuration设置为 Release。

    • Xcode 打包 使用CMD + B快捷键打包,在 Products/SJSwiftSDK_Example.app上右击Show in Finder,在当前路径下有和SDK 同名的文件夹(demo 中是 SJSwiftSDK) ,此文件夹下的 SJSwiftSDK.framework 即为最终生成的 framework。

    • xcodebuild 脚本打包

      执行xcodebuild打包命令,设置相应参数后即可。

      设置 workspace,scheme,及编译缓存路径
      xcodebuild -quiet -workspace SJSwiftSDK/Example/SJSwiftSDK.xcworkspace -scheme SJSwiftSDK-Example -derivedDataPath SJSwiftSDK/build/
      
  • 发布 framework 新建仓库或者找到已存在仓库,将上一步获取的 framework 拷贝到仓库中。然后提交当前修改,并打 tag。 执行trunk命令可以发布 framework

    # 提交代码
    git add .
    git commit -m "xxx"
      
    # 打 tag
    git tag -m "" xxx
    gir push --tags
      
    # 发布代码到仓库, 若当前文件夹仅有一个 podspec,后面参数可以省略
    pod trunk push SJSwiftSDK.podspec
      
    # 若确认 framework 无问题,可以跳过验证
    pod trunk push --skip-import-validation
    

由于每次打包都需要 commit,tag,trunk 等操作,一直重复敲命令行总是显得不够极客,我这里简单写了一个脚本,可以一键执行上诉命令,在podspec 同级文件下创建脚本文件,设置执行权限即可。

# 脚本内容如下

tag=$1
cm_info='"'$1'"'

# 提交 git
git_commit() {
    echo '\033[31m start commit git \033[0m'
    git status
    git add .
    git commit -m $cm_info
    git pull
    git push
}

# 打 tag
git_tag() {
    echo '\033[31m start add tag \033[0m'
    git tag -a $tag -m $cm_info
    git push --tags
}

if [ $# -eq 0 ]
then
    echo '\033[31m please input tag(eg: 1.2.4) \033[0m'

else
    git_commit
    git_tag
    
    # 推送当前版本
    # --verbose 查看详细信息
    # --skip-import-validation 跳过验证
    echo '\033[31m start upload version \033[0m'
    pod trunk push
    if test $? -eq 0
    then
        echo '\033[31m 🎉🎉🎉 upload success \033[0m'
    else
        echo 'trunk error'
    fi
fi

下面介绍了如何创建与使用脚本。

# 创建脚本文件
touch upgrade.sh

# 编辑文件(拷贝上诉内容)
vi upgrade.sh

# 设置执行权限
chmod 777 upgrade.sh

# 执行脚本(tag:1.2.4)
./upgrade.sh 1.2.4

2. 多架构支持

一般来说,framework 需要支持真机(armv7s, arm64, arm64e)和模拟器(i386, x86_64)架构,这样用户使用时才可以在真机和模拟器下都能编译通过,这里有点需要注意,为了让整个应用都支持真机和模拟器,需要将所有的 framework 都同时支持真机和模拟器,但凡有一个 framework 不支持,编译时都会报错的,那么如何创建出支持多种架构的 framework 呢?

2.1. 构建多种架构

我们这里以 OCDynamicSDK.framework 为例,简单介绍下如何生成多种架构支持的 framework。查看当前的架构是在 Pods 下的 target(当前是 OCDynamicSDK),不是主工程的 target,主工程 target 只是一个 demo,设置参数对 framework 并没有任何影响。

在 target OCDynamicSDK 下的 Build Settings 下,有 Architectures,Build Active Architecture Only,Valid Architectures 三种设置,对 SDK 的架构有影响,这里简单说一下三者关系。

  • Valid Architectures 有效的指令集,用来给对应的target设置有效指令集范围。
  • Architectures 目标指令集,只有设置成有效的所包含的)才会起作用。默认为 Standard architectures(armv7,arm64)
  • Build Active Architecture Only 是否仅创建可用的架构,debug 一般设置为 true 提高编译速度,release 设置为 false。

当我们在编译时,target 选择 Generic iOS Device,编译出来为真机下的架构,target 选择模拟器后,则编译出来为模拟器架构,我们可以通过这种方式分别生成支持多种架构的 framework,然后通过 lipo命令合并这些架构,即可完成多架构 framework。

我们可以通过xcodebuild命令来构建 framework,查看具体用法可以通过xcodebuild -help

# 对 xxx.xcworkspace 下的 xxx target进行打包,编译缓存路径为 ./build
xcodebuild -workspace xxx.xcworkspace -scheme xxx -derivedDataPath ./build/

# 执行模拟器编译, 具体支持的模拟器版本可通过 xcodebuild -showsdks 查看
# xcodebuild 添加参数 -sdk iphonesimulator13.2
xcodebuild -workspace xxx.xcworkspace -scheme xxx -derivedDataPath ./build/ -sdk iphonesimulator13.2

2.2. 合并架构

lipo 支持查看、移除、合并架构,通过lipo命令可以方便生成同时支持真机和模拟器的 framework。

# lipo 命令的对象是 framework 下的二进制文件,而不是 framework,这里需要特被注意
# 查看当前支持架构
lipo -info xxxx.framework/xxxx

# 合并多架构(xxx1 和 xxx2 合并到 xxx1下)
lipo -create xxx1.framework/xxx1 xxx2.framework/xxx2 -output xxx1.framework/xxx1

2.3. 合并时问题修复

对于 OC 来说,上一步合并完毕后,整个 framework 已经制作完毕,但是对于 Swift,还有额外的一步重要操作:将真机和模拟器 Modules 文件夹下面的文件合并。

合并 framework 和合并 Modules 文件,都需要手动操作,繁琐且无技术含量,这里我写了个脚本可以方便处理这些流程,有需要的可以修改后自行使用。


# 定义模拟器名称(用于生成模拟器架构)
simulator='iphonesimulator13.2'
# SDK 名称(workspace 和 scheme 若不同名,需要手动修改代码)
SDK_name='hhVDoctorSDK'
# 设置缓存路径后,base_path 指向打包路径
base_path='build/Build/Products/'

# 以上参数根据自身情况修改

# 打包
build_SDK() {
    # build 真机包 -quiet
    xcodebuild -quiet -workspace Example/$1.xcworkspace -scheme $1-Example -derivedDataPath build/
    if test $? -eq 0
    then
        echo '\033[33m build' $1 ' arm64 success \033[0m'
        # build 模拟器包(-SDK)
        xcodebuild -quiet -workspace Example/$1.xcworkspace -scheme $1_simulator -derivedDataPath build/ -sdk $simulator
        if test $? -eq 0
        then
            echo '\033[33m build' $1 ' simulator success \033[0m'
            # 合并仓库
            lipo_make $1
            return $?
        else
            echo '\033[30m build' $1 ' simulator fail \033[0m'
            return 0
        fi
    else
        echo '\033[30m build' $1 ' arm64 fail \033[0m'
        return 0
    fi
}

# 合并
lipo_make() {
    # 合并 SDK
    lipo -create $base_path$arm64_path$1/$1.framework/$1 $base_path$x86_64_path$1/$1.framework/$1 -output $base_path$arm64_path$1/$1.framework/$1

    if test $? -eq 0
    then
        # 合并其他文件
        for file in $x86_app_module $x86_app_doc $x86_doc $x86_module
        do
            cp -r $base_path$x86_64_path$1/$1.framework/Modules/$1.swiftmodule/$file $base_path$arm64_path$1/$1.framework/Modules/$1.swiftmodule/
        done
        echo '\033[32m copy files success \033[0m'
        return 0
    else
        echo '\033[30m lipo create fail \033[0m'
        return 1
    fi
}

# 执行构建 SDK
build_SDK $SDK_name

3. 供外部调用

Swift 写的 framework 供外部使用时会有一些问题需要处理,对于 Swift 代码引用 Swift 写的 SDK,这种方式比较简单,对外暴露的元素添加修饰符 public 或 open 即可,但是对于 OC 调用 Swift 的 SDK 来说,问题也稍显麻烦点。

3.1. 关键字和头文件

首先,Swift 的 结构体(struct)类型在 OC 中没有对应,所以无法在 OC 中使用。

  • Swift 对外的 class 需要最终继承自 NSObject
  • Swift 对外的 class 需要添加 public 或 open 修饰符
  • Swift 对外的 class 的方法需要 @objc 和 public/open 修饰

对于 OC 的用户来说,可以通过导入 name-Swift.hname-umbrella.h 来分别导入 Swift 和 OC 的头文件,但是使用起来总觉得别扭,我们可以效仿其他 SDK 的设计,添加和 SDK 同名的头文件,将上面提到的两个文件加入后即可。

3.2. 引用(Embeded framework)

对外如果是采用 CocoasPod 引用的话,这里就没有问题了,pod 已经帮我们处理好了,但是若是手动拖入 framework 的话,由于我们使用的是动态库(Dynamic Library),直接拖入编译没问题,运行时会报错,因为我们自己的动态库是 Embedded Framework,在 Framework,Libraries,and Embedded Content中,需要设置为 Embedded & Sign

3.3. 部分机型闪退问题(swift libraries)

对于使用 OC 的用户来说,由于使用了咱们的 Swift 版的 framework,可能在某些机型上运行会闪退,需要他们的 Build Settings 里面设置 Always Embed Swift Standard Libraries 为 true。