Swift 静态库调研

  • 一、关于库的背景知识
    • 1、静态库和动态库
      • 静态库
      • 动态库
        • 系统动态库
        • Cocoa Touch Framework
      • 静态库 v.s. 动态框架
    • 2、制作框架的注意事项
      • 制作框架的一些知识
    • 使用框架的注意事项
  • 二、Swift 静态库实践
    • 1、资源管理
      • Swift 静态库中访问静态库图片资源的方法
      • Swift 静态库中访问主工程图片资源的方法
      • Swift 动态库中访问动态库图片资源的方法
      • Swift 动态库中访问主工程图片资源的方法
    • 2、符号丢失
    • 3、CocoaPods
    • 4、premain 消耗
  • 三、静态库打包脚本

一、关于库的背景知识

1、静态库和动态库

静态库

在 iOS 8 之前,iOS 只支持以 .a 静态库的方式来使用第三方的代码。

iOS 8 之后,静态库有 .a.framework 两种后缀。静态库是一系列从源码编译的目标文件的集合。是你的源码的实现所对应的二进制,在最后编译 app 的时候 将被链接到最终的可执行文件中,之后每次都随着app的可执行二进制文件一同加载,不能控制加载的方式和时机,所以称为静态库。

.a 静态库配合上公共的 .h 文件,我们可以获取到 .a 中暴露的方法或者成员等。

动态库

动态库可以分为系统动态库和自制动态库(Cocoa Touch Framework)。

系统动态库

我们每天使用的 iOS 系统的框架是以 .framework 结尾的,它们就是动态框架。

Framework 其实是一个 bundle,或者说是一个特殊的文件夹。系统的 framework 是存在于系统内部,而不会打包进 app 中。app 启动的时候会检查所需要的动态框架是否已经加载。像 UIKit 之类的常用系统框架一般已经在内存中,就不需要再次加载,这可以保证 app 启动速度。

相比静态库,framework 是自包含的,不需要关心头文件位置等,使用起来很方便。

Cocoa Touch Framework

Apple 从 iOS 8 开始允许开发者有条件地创建和使用动态框架,这种框架叫做 Cocoa Touch Framework。

虽然同样是动态框架,但是和系统 framework 不同,app 中的使用的 Cocoa Touch Framework 在打包和提交 app 时会被放到 app bundle 中,运行在沙盒里,而不是系统中。也就是说,不同的 app 就算使用了同样的 framework,但还是会有多份的框架被分别签名,打包和加载。

Cocoa Touch Framework 的推出主要是为了解决两个问题:首先是应对刚才提到的从 iOS 8 开始的扩展开发。

其次是因为 Swift,Swift 之前是不支持编译为静态库的。

现在,Swift runtime 不在系统中,而是打包在各个 app 里的。所以如果要使用 Swift 静态框架,由于 ABI 不兼容,所以我们将不得不在静态包中再包含一次 runtime,可能导致同一个 app 包中包括多个版本的运行时,暂时是不可取的。

静态库 v.s. 动态框架

  1. .a 静态库不能包含像 xib 文件、图片这样的资源文件,其他开发者必须将它们复制到 app 的 main bundle 中才能使用,维护和更新非常困难;而 framework 则可以将资源文件包含在自己的 bundle 中。
  • 静态库必须打包到二进制文件中,这在以前的 iOS 开发中不是很大的问题。但是随着 iOS 扩展(比如通知中心扩展或者 Action 扩展)开发的出现,你现在可能需要将同一个 .a 包含在 app 本体以及扩展的二进制文件中,这是不必要的重复。
  • 静态库只能随应用 binary 一起加载,而动态框架加载到内存后就不需要再次加载,二次启动速度加快。另外,使用时也可以控制加载时机。

2、制作框架的注意事项

  1. 设置 Deployment Target
  • Objective-C 库需要设置相关 .h 文件为 public
  • Swift 库的类名和函数名要注意访问控制关键字 public``open
  • 不同 Xcode 版本的 Swift 版本不同
  • Mach-O type 的设置

制作框架的一些知识

  • 静态库位置
    Debug运行真机编译会把静态库生成到 Debug-iphoneos目录下
    Debug运行模拟器编译会把静态库生成到 Debug-iphonesimulator目录下
    Release运行真机编译会把静态库生成到 Release-iphoneos目录下
    Release运行模拟器编译会把静态库生成到 Release-iphonesimulator目录下

  • Debug版本 VS Release版本
    2.1调试版
    调试版本会包含完整的符号信息,以方便调试
    调试版本不会对代码进行优化
    2.2发布版
    发布版本不会包含完整的符号信息
    发布版本的执行代码是进行过优化的
    发布版本的大小会比调试版本的略小
    在执行速度方面,调试版本会更快些,但不意味着会有显著的提升

  • iPhone手机的cpu架构
    模拟器
    iPhone4s,5 是 i386架构
    iPhone5s以后 是x86_64架构
    真机
    iPhone1代,3G,3GS 是 armv6 架构
    iPhone4,4s 是 armv7 架构
    iPhone5,5s,5c 是 armv7s 架构
    iPhone6,6s,6 plus,6s plus 是 arm64架构

  • 查看.a库所支持的架构类型
    lipo -info xxx.a

  • 库合并
    lipo -create 真机库 模拟器库 -output 新文件
    只合并Debug版本 或者 只合并Release版本即可。

  • 查看.framework库所支持的架构类型: fileFramework 即可

使用框架的注意事项

  1. 使用动态库时,需要注意将动态库加入到 Embedded Binaries
  • 打包出来的 Swift 动态库语言版本和使用环境的 Swift 语言版本: Xcode 8.3.3 打出的 Swift 动态库版本自带的运行时环境是 Swift 3,在 Xcode 9.2 中不能运行(Xcode 9 支持 Swift 3.2 和 4)

二、Swift 静态库实践

1、资源管理

Swift 静态库中访问静态库图片资源的方法

无法访问

Swift 静态库中访问主工程图片资源的方法

使用 UIImage 的 public init?(named name: String) 直接访问

Swift 动态库中访问动态库图片资源的方法

1、动态库中访问动态库文件目录中的图片

var a: UIImage? = nil
let dynamicLibraryBundle = Bundle(for: dynamicLibraryClass.self)
if let path = dynamicLibraryBundle.path(forResource: "icon.png", ofType: nil) {
    a = UIImage(contentsOfFile: path)
}

2、动态库中访问动态库的bundle中的图片

var a: UIImage? = nil
let dynamicLibraryBundle = Bundle(for: dynamicLibraryClass.self)
if let path = dynamicLibraryBundle.path(forResource: "icon.png", ofType: nil, inDirectory:"dynamicResource.bundle") {
    a = UIImage(contentsOfFile: path)
}

Swift 动态库中访问主工程图片资源的方法

和静态库访问主工程图片资源的方法相同,使用 UIImage 的 public init?(named name: String) 直接访问

2、符号丢失

测试过程中未发现符号丢失情况。

3、CocoaPods

测试使用 CocoaPods 版本:1.4.0.rc.1

CocoaPods 在1.4.0的Beta版本中开始支持 Swift 静态库打包。方法是在制作 pod 库时,在 .podspec 文件中写入

s.static_framework = true

不过现在基本所有的第三方库都没有支持这一新特性,所以为了使用这一特性,一个可行的方式是

自己提供项目依赖的 pod 对应的静态库 podspec

我在无人超市项目中进行了实践。测试步骤是:

1、 将无人超市的项目代码适配 Swift 4,进而在 podfile 和项目中引用最新版本的相关第三方库;
2、 将相关第三方库 fork 一份出来到自己的 GitHub 上,修改每个项目的 podspec,添加static_frameworksource 属性,例如

Pod::Spec.new do |s|
  s.name = 'SnapKit'
  s.version = '4.0.0'
  s.license = 'MIT'
  s.summary = 'Harness the power of auto layout with a simplified, chainable, and compile time safe syntax.'
  s.homepage = 'https://github.com/SnapKit/SnapKit'
  s.authors = { 'Robert Payne' => 'robertpayne@me.com' }
  s.social_media_url = 'http://twitter.com/robertjpayne'
  s.source = { :git => 'https://github.com/rxg9527/SnapKit.git', :tag => '4.0.0' }

  s.ios.deployment_target = '8.0'
  s.osx.deployment_target = '10.11'
  s.tvos.deployment_target = '9.0'

  s.source_files = 'Source/*.swift'

  s.requires_arc = true
  s.static_framework = true
end

3、修改无人超市的 podfile 如下:

platform :ios, '8.0'
use_frameworks!


target 'AutoStore' do
    
    pod 'Alamofire', :git => 'https://github.com/rxg9527/Alamofire'
    pod 'Kingfisher', :git => 'https://github.com/rxg9527/Kingfisher'
    pod 'SnapKit', :git => 'https://github.com/rxg9527/SnapKit'
    pod 'SwiftyJSON', :git => 'https://github.com/rxg9527/SwiftyJSON'
    pod 'IQKeyboardManagerSwift', '~> 5.0.0'

    pod 'WechatOpenSDK', '~> 1.7.7'
    pod 'JPush', '3.0.6'
    
    pod 'MJRefresh', :git => 'https://github.com/rxg9527/MJRefresh'
    pod 'MBProgressHUD', :git => 'https://github.com/rxg9527/MBProgressHUD'
    pod 'FLEX', :configurations => ['Debug'], :git => 'https://github.com/rxg9527/Flex'
    
end

pod install 后可以看到绝大部分库已经打包为静态库的形式。

4、premain 消耗

Warm launch: App 和数据已经在内存中
Cold launch: App 不在内核缓冲存储器中

冷启动(Cold launch)耗时才是我们需要测量的重要数据,为了准确测量冷启动耗时,测量前需要重启设备。在 main() 方法执行前测量是很难的,好在 dyld 提供了内建的测量方法:在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为 1

控制台会输出类似如下的内容:

Total pre-main time: 353.93 milliseconds (100.0%)
         dylib loading time: 279.41 milliseconds (78.9%)
        rebase/binding time:  30.70 milliseconds (8.6%)
            ObjC setup time:  17.78 milliseconds (5.0%)
           initializer time:  25.97 milliseconds (7.3%)
           slowest intializers :
             libSystem.B.dylib :   3.12 milliseconds (0.8%)
                     AutoStore :  13.73 milliseconds (3.8%)

以无人超市项目为例,不同情况下的 premain 消耗测试结果如下

iPhone X 重启 1 2 3 4 5 平均
静态库(ms) 402.17 444.70 419.20 402.15 438.70 421.384
动态库(ms) 491.24 509.79 661.97 499.90 483.13 529.206
应用卸载 1 2 3 4 5 平均
静态库(ms) 372.46 431.79 399.18 384.19 374.01 392.326
动态库(ms) 448.42 450.85 444.77 455.19 485.74 456.994

三、静态库打包脚本

1、 菜单File --> New --> Target --> Cross-platform --> Aggregate
2、 选中刚创建的 Target, 然后 Build Phases --> 点击左上角+箭头 --> New Run Script Phase,在编辑栏区域填入脚本:

# AllCPUArchitecture
# Sets the target folders and the final framework product.

# 如果工程名称和Framework的Target名称不一样的话,要自定义framework名称
# FRAMEWORK_NAME=${PROJECT_NAME}
FRAMEWORK_NAME=SwiftStaticLibrary

# 最终输出 framework 的目录
# 在当前 Project 的 SRCROOT 目录下创建 build 文件夹,存放 framework
INSTALL_DIR=${SRCROOT}/build/${FRAMEWORK_NAME}.framework

# WRK_DIR: 工作目录,在脚本执行完成后删除
WRK_DIR=build

# DEVICE_DIR: Release 配置下编译真机架构下的 framework 的默认生成目录
# SIMULATOR_DIR:  Release 配置下编译模拟器架构下的 framework 的默认生成目录
DEVICE_DIR=${WRK_DIR}/Release-iphoneos/${FRAMEWORK_NAME}.framework
SIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator/${FRAMEWORK_NAME}.framework

# -configuration ${CONFIGURATION}
# Clean and Building both architectures.
xcodebuild \
  MACH_O_TYPE="staticlib" \
  -configuration "Release" \
  -target "${FRAMEWORK_NAME}" \
  -sdk iphoneos \
  clean build

xcodebuild \
  MACH_O_TYPE="staticlib" \
  -configuration "Release" \
  -target "${FRAMEWORK_NAME}" \
  -sdk iphonesimulator \
  clean build

# 清除原有 INSTALL_DIR 目录

if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi

mkdir -p "${INSTALL_DIR}"
cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"

# 对于 Swift framework, 为了建立通用 CPU 架构的 framework,真机和模拟器的 framework 中的 Swiftmodule 文件夹必须合并
if [ -d "${SIMULATOR_DIR}/Modules/${FRAMEWORK_NAME}.swiftmodule/" ]
then
cp -R "${SIMULATOR_DIR}/Modules/${FRAMEWORK_NAME}.swiftmodule/." "${INSTALL_DIR}/Modules/${FRAMEWORK_NAME}.swiftmodule"
fi
                                                                      
# 使用 lipo 工具 merge binary files into one Universal final product.

lipo \
  -create "${DEVICE_DIR}/${FRAMEWORK_NAME}" "${SIMULATOR_DIR}/${FRAMEWORK_NAME}" \
  -output "${INSTALL_DIR}/${FRAMEWORK_NAME}"

#rm -r "${WRK_DIR}"
open "${INSTALL_DIR}"

3、选中该 Scheme,⌘ + B 编译生成需要的通用 framework

脚本的作用:
生成 Objective-C 或者 Swift 的通用 CPU 架构的静态库 framework,但是要注意:

  1. 设置 Deployment Target
  • Objective-C 库需要设置相关 .h 文件为 public
  • Swift 库的类名和函数名要注意访问控制关键字 public open

推荐阅读更多精彩内容

  • 静态库与动态库的区别 首先来看什么是库,库(Library)说白了就是一段编译好的二进制代码,加上头文件就可以供别...
    吃瓜群众呀阅读 10,592评论 3 42
  • 是视图和模型保持同步, 无论哪方改变,另一方都将同步 前面学的都是单向绑定, 事件绑定是模板到控制器 DOM是控制...
    Monee121阅读 108评论 0 0
  • 我在很久以前,就知道会有一个"空间",允许我建造一个理想中的乐园. 在那里聚集善良的,真诚的家人,可以用一种不同于...
    生态缔造者阅读 930评论 0 14
  • “每逢佳节倍思亲”在我家一年当中只有两个节日是比较重要的,那就是中秋和春节。只有没有啥事,都必须赶回过节,难得的一...
    言凡凡阅读 343评论 0 1
  • 每个人都有大脑空闲时,这个空闲阶段的思维想法都有变化吗,是专注的想刚才做的事情,还是已经飘忽不着边际、错乱的想法中...
    玻璃丝阅读 91评论 0 0