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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

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