- 库
- 库的分类
- 静态库
- 动态库
- 静态库与动态库的区别
.a
与.framework
区别- 制作静态库.a
- 制作静态库 framework
- 动态库
- 添加资源
- Framework
- Create the path to the real Headers die
- Create the required symlinks
- Copy the public headers into the framework
- 推荐方案:CocoaPods 库
- Uncomment the next line to define a global platform for your project
- 指明库podspec的路径,这里是BFDynamicFramework.podspec的路径,在此,BFDynamicFramework和BFTestFramework同级,所以path是../BFDynamicFramework
- Workaround for Cocoapods issue #7606
库
库就是程序代码的集合,将N个文件组织起来,是共享程序代码的一种方式。库从本质上来说是一种可执行代码的二进制格式,可以被载入内存中执行。库实现了程序某个功能模块的模块化,它便于我们共享(公用)、维护、独立和安全。
一般分为静态库和动态库。
- 静态库:以
.a
和.framework
为文件后缀名。 - 动态库:以
.tbd
(之前叫.dylib
) 和.framework
为文件后缀名。(系统直接提供给我们的framework
都是动态库!)
库的分类
- 开源库:源代码是公开的,可以看到每个实现文件(.m文件)的实现,例如GitHub上的常用的开源库:AFNetworking、SDWebImage等;
- 闭源库:不公开源代码,是经过编译后的二进制文件,看不到具体的实现。闭源库又分为:静态库和动态库
静态库
静态库链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
动态库
动态库链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。
- Framework 是 Cocoa/Cocoa Touch 程序中使用的一种资源打包方式,可以将代码文件、头文件、资源文件(nib/xib、图片、国际化文本)、说明文档等集中在一起,方便开发者使用。Framework 其实是资源打包的方式,和静态库动态库的本质是没有什么关系。
- 在 iOS 8 之前,iOS 平台不支持使用动态 Framework,开发者可以使用的 Framework 只有苹果系统提供的 UIKit.Framework,Foundation.Framework 等。开发者要进行模块化,只能是打包成静态库 .a 文件,同时附上头文件。但是这种方式打包不够方便,使用时也比较麻烦,没有 Framework 的便捷性。但是这时候的 Framework 只支持打包成静态库的 Framework。
- iOS 8/Xcode 6 推出之后,添加了动态库的支持,Xcode 6 支持动态 Framework 动态 Framework 和系统的 UIKit.Framework 还是有很大区别。系统的 Framework 不需要拷贝到目标程序中,是一个链接,而开发者打包的 Framework,还是要拷贝到 App 中,因此苹果又把这种 Framework 称为 Embedded Framework(可植入性 Framework)。
静态库与动态库的区别
| | 静态库 | 动态库 | | :—- | :—- | :—- | | 定义 | 链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。 | 链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。 | | 形式 |.a
、.framework
|.dylib
、.tbd
和.framework
| | 场景 | 避免少量改动经常导致大量的重复编译连接 | 使用动态库,可缩小最终可执行文件体积
使用动态库,多个应用程序共享内存中得同一份库文件,节省资源
使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的 |
.a
与.framework
区别
两者关系是:
.a + .h + sourceFile = .framework
.a | .framework | |
---|---|---|
文件 | 是一个纯二进制文件 | 除了有二进制文件之外还有资源文件 |
使用 | 文件不能直接使用,至少要有.h 文件配合 | 文件可以直接使用 |
- 无论是
.a
静态库还.framework
静态库,我们需要的都是 “二进制文件 +.h + 其它资源文件
” 的形式,不同的是,.a
本身就是二进制文件,需要我们自己配上.h
和其它文件才能使用,而.framework
本身已经包含了.h 和其它文件,可以直接使用。 - 图片资源的处理:两种静态库,一般都是把图片文件单独的放在一个
.bundle
文件中,一般.bundle
的名字和.a
或.framework
的名字相同。.bundle
文件新建一个文件夹,把它改名为.bundle
就可以了,右键,显示包内容可以向其中添加图片资源。 - 如果一个静态库很复杂,需要暴露的
.h
比较多的话,就可以在静态库的内部创建一个.h
文件(一般这个.h
文件的名字和静态库的名字相同),然后把所有需要暴露的.h 文件都集中放在这个.h
文件中,而原来都不需要再暴露了。制作静态库.a
一、简单静态库
简单静态库指的是,不包含其他静态库的静态库,常用于封装一些基础的工具类。1. 创建静态库工程
然后直接编译,可以得到.a
为后缀的静态库。
屏幕快照 2018-09-19 上午9.28.03
2. 编译静态库
设置 Edit Scheme
-> Build Configuration
-> 选为 Release
。
完成代码编写,并选中 Device 为真机编译,还是模拟器编译,不同的 Device 下编译出的静态库是不同的,真机编译可能在模拟器中使用会 crash,同样模拟器编译出来在真机运行中会发生问题。
导出后,可以查看对应的库信息:
$ lipo -info libBFStaticLib_device.a
input file libBFStaticLib_device.a is not a fat file
Non-fat file: libBFStaticLib_device.a is architecture: arm64
$ lipo -info libBFStaticLib_simulator.a
input file libBFStaticLib_simulator.a is not a fat file
Non-fat file: libBFStaticLib_simulator.a is architecture: x86_64
3. 合并静态库
上一步生成的不同指令集下的静态库,需要合并才能支持多设备,如下:
//lipo -create ***.a ***.a -output output_lib_path
$ lipo -create libBFStaticLib_simulator.a libBFStaticLib_device.a -output ./libBFStaticLib
$ lipo -info libBFStaticLib
Architectures in the fat file: libBFStaticLib are: x86_64 arm64
4. 相关设置
a. 添加 static library 的头文件
在 Targets->build settings->user header search paths
设置 static library 的头文件搜索路径
b. arc/mrc
可设置对应的文件是否开启 ARC:-fobjc-arc
/-fno-objc-arc
。
制作静态库 framework
一、创建工程及工程设置
创建一个项目名称为:BFStaticFramework 的静态库。
其工程设置中,如下
- 在 Linking 里,将
Dead Code Stripping
设置为 NO,用于删除对象文件中不需要加载的符号,减小二进制文件大小 Link With Standard Libraries
设置为 NO,链接使用标准静态库。- 将
Mach-O Type
改为 Static Librariy,选择 Mach-O 的类型。
二、添加头文件
将需要暴露的头文件放在 Build Phases
对应的分组下:
- Public:对外暴露的头文件。
- Private:查看
Framework
下的头文件,依然是暴露出来的。 - Project:该分组头文件对工程来说才是 “私有” 的
所以,将你的头文件或者在 Public 下,或者在 Project 下。尽可能少的暴露头文件,将其他类放在 Project 中。
三、导出 Framework
添加一个脚本:
屏幕快照 2018-10-17 下午4.07.14
为 OutputFramework 添加一个脚本
# Sets the target folders and the final framework product.
# 如果工程名称和Framework的Target名称不一样的话,要自定义FMKNAME
# 例如: FMK_NAME = "MyFramework"
FMK_NAME=${PROJECT_NAME}
# Install dir will be the final output to the framework.
# The following line create it in the root folder of the current project.
INSTALL_DIR=${SRCROOT}/Products/${FMK_NAME}.framework
# Working dir will be deleted after theframework creation.
WRK_DIR=build
DEVICE_DIR=${WRK_DIR}/Release-iphoneos/${FMK_NAME}.framework
SIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator/${FMK_NAME}.framework
# -configuration ${CONFIGURATION}
# Clean and Building both architectures.
# 在Xcode 10时,去掉下面每一句末尾的clean build,因为会每次都清空build文件夹,导致最后到处的framework为空
xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphoneos
xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphonesimulator
# Cleaning the oldest.
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi
mkdir -p "${INSTALL_DIR}"
cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.
lipo -create "${DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${INSTALL_DIR}/${FMK_NAME}"
rm -r "${WRK_DIR}"
open "${INSTALL_DIR}"
运行脚本,输出 Framework,记住,一定要选中真机,否则生成的架构只支持 x86_64 arm64
。
四、验证
$ cd framework路径
$ lipo -info BFStaticFramework
Architectures in the fat file: BFStaticFramework are: armv7 i386 x86_64 arm64
使用,直接拖入工程代码即可。
动态库
一、制作
制作动态库大体流程和制作静态库 framework 一致,只需要在第一步中工程配置中,将 Mach-O Type
改为 **Dynamic Librariy**
。
二、使用
将动态库拖入工程中,直接运行,会出现下面错误
dyld: Library not loaded: @rpath/BFDynamicFramework.framework/BFDynamicFramework Referenced from: /Users/wenghengcong/Library/Developer/CoreSimulator/Devices/
注意这一点与静态库有所不同!
如何解决?
- 方案一:需要将该库,添加到
Embedde Binaries
中。
- 方案二:尝试将动态库的 Status 的状态修改为
Optional
,改成 Optional 有效的前提是动态库中不包含分类,或者包含了分类但是未进行调用。否则,只能按方案一。
添加资源
一、创建 Bundle Target
在当前工程中,新建一个 Target。
屏幕快照 2018-10-18 下午6.14.40
二、Build Settings 设置
下面点击 Build Settings,进行一些设置:
Base SDK
: 选中,并按下 Delete,就会默认使用最新的 iOS SDK。Supported Platforms
:选中 iOSProduct Name
:替换为BFStaticFramework
Enable Bitcode
:设置为 NO。
如果不设置,可能会遇到错:ld: -bundle and -bitcode_bundle (Xcode setting ENABLE_BITCODE=YES) cannot be used together
COMBINE_HIDPI_IMAGES
:设置 NO
经测试在高版本的 XCode 中,设置为 YES 也不会导致,打包出的 framework 中的png
图片格式变为tiff
。
所以一定要修改。
三、编译出包
1. 直接出包
编译出包,得到 BFStaticFramework.bundle
。
为了确保编译 BFStaticFramework.framework
时,也编译 BFStaticFramework.bundle
,在之前输出 Framework 的 target 下,添加 target dependencies
,如下:
并且,为了是的 bundle 文件和 Framework 在同一目录下。
在 OutputFramework 对应的脚本中添加:
.....
# Copy the resources bundle
ditto "${BUILT_PRODUCTS_DIR}/${FMK_NAME}.bundle" \
"${SRCROOT}/Products/${FMK_NAME}.bundle"
open "${INSTALL_DIR}"
2. 导入 Framework
进一步,如果要将 bundle 文件打入 Framework 包中:
首先,将上一步中得到的 bundle 文件直接拖入 BFStaticFramework
工程中,并选中不要 Copy items if needed
。
四、使用
如果,需要将对应的 bundle 拖到对应的工程中去使用:
UIImage *image = [UIImage imageNamed:@"BFStaticFramework.bundle/qq"];
//or
UIImage *image = [UIImage imageNamed:@"qq" inBundle:[NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"BFStaticFramework" ofType: @"bundle"]] compatibleWithTraitCollection:nil];
或者新建分类:
//.h
#import <UIKit/UIKit.h>
@interface UIImage (Resource)
+ (UIImage *)resourceImageNamed:(NSString *)name;
@end
/.m
#import "UIImage+Resource.h"
@implementation UIImage (Resource)
+ (UIImage *)resourceImageNamed:(NSString *)name{
//先从默认目录里读
UIImage *imageFromMainBundle = [UIImage imageNamed:name];
if (imageFromMainBundle) {
return imageFromMainBundle;
}
//读不到再去Bundle里读
//此处Scale是判断图片是@2x还是@3x
NSInteger scale = (NSInteger)[[UIScreen mainScreen] scale];
for (NSInteger i = scale; i >= 1; i--) {
NSString *filepath = [self getImagePath:name scale:i];
UIImage *tempImage = [UIImage imageWithContentsOfFile:filepath];
if (tempImage) {
return tempImage;
}
}
return nil;
}
+ (NSString *)getImagePath:(NSString *)name scale:(NSInteger)scale{
NSURL *bundleUrl = [[NSBundle mainBundle] URLForResource:@"Resource" withExtension:@"bundle"];
NSBundle *customBundle = [NSBundle bundleWithURL:bundleUrl];
NSString *bundlePath = [customBundle bundlePath];
NSString *imgPath = [bundlePath stringByAppendingPathComponent:name];
NSString *pathExtension = [imgPath pathExtension];
//没有后缀加上PNG后缀
if (!pathExtension || pathExtension.length == 0) {
pathExtension = @"png";
}
//Scale是根据屏幕不同选择使用@2x还是@3x的图片
NSString *imageName = nil;
if (scale == 1) {
imageName = [NSString stringWithFormat:@"%@.%@", [[imgPath lastPathComponent] stringByDeletingPathExtension], pathExtension];
}
else {
imageName = [NSString stringWithFormat:@"%@@%ldx.%@", [[imgPath lastPathComponent] stringByDeletingPathExtension], (long)scale, pathExtension];
}
//返回删掉旧名称加上新名称的路径
return [[imgPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:imageName];
}
@end
Framework
上面我们讲述了如何制作,使用动态库和静态库的 Framework,下面我们针对 Framework 本身进行一些探讨。
配置
Key | Description |
---|---|
CFBundleName |
The framework display name |
CFBundleIdentifier |
The framework identifier (as a Java-style package name) |
CFBundleVersion |
The framework version |
CFBundleExecutable |
The framework shared library |
CFBundleSignature |
The framework signature |
CFBundlePackageType |
The framework package type (which is always 'FMWK' ) |
NSHumanReadableCopyright |
Copyright information for the framework |
CFBundleGetInfoString |
A descriptive string for the Finder |
结构
下面以 Alamofire 作为参考,目录如下::
$ tree -a -L 3
.
├── Alamofire -> Versions/Current/Alamofire
├── Headers -> Versions/Current/Headers
├── Modules -> Versions/Current/Modules
├── Resources -> Versions/Current/Resources
└── Versions
├── A
│ ├── Alamofire
│ ├── Headers
│ ├── Modules
│ └── Resources
└── Current -> A
而我们上面制作的,以 BFDynamicFramework 为例,结构如下:
.
├── .DS_Store
└── BFDynamicFramework.framework
├── BFDynamicFramework
├── Headers
│ ├── BFDeviceUtils.h
│ ├── BFDynamicFramework.h
│ └── UIView+Frame.h
├── Info.plist
└── Modules
└── module.modulemap
如何将我们默认的工程结构,修改成 Alamofire 类似的结构呢?
下面调试中的两种方法都可以做到。
- 子工程调试: 手动写脚本,改变 Framework 结构。
进入 target,选中 BFStaticFramework->Build Phases->Add “New Run Script Phases
”
```bash set -e export FRAMEWORK_LOCN=”${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework”
Create the path to the real Headers die
mkdir -p “${FRAMEWORK_LOCN}/Versions/A/Headers”
Create the required symlinks
/bin/ln -sfh A “${FRAMEWORK_LOCN}/Versions/Current” /bin/ln -sfh Versions/Current/Headers “${FRAMEWORK_LOCN}/Headers” /bin/ln -sfh “Versions/Current/${PRODUCT_NAME}” \ “${FRAMEWORK_LOCN}/${PRODUCT_NAME}”
Copy the public headers into the framework
/bin/cp -a “${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/“ \ “${FRAMEWORK_LOCN}/Versions/A/Headers”
之后,编译输出的结构就会更改:
```bash
$ tree -L 3
.
├── BFStaticFramework
├── Headers
│ ├── BFDeviceUtils.h
│ ├── BFStaticFramework.h
│ ├── Headers -> Versions/Current/Headers
│ └── UIView+Frame.h
├── Info.plist
└── Versions
├── A
│ └── Headers
└── Current -> A
Cocoapods:自动生成规范的目录结构。
调试
在前面,我们创建了 BFStaticFramework 静态库(也适用于动态库),现在我们创建一个调试工程 BFTestFramework。
打开 BFTestFramework,将 BFStaticFramework.xcodeproj 拖入 BFTestFramework,注意,拖入是,一定要确保 Xcode 没有打开 BFStaticFramework.xcodeproj,因为同一个工程,不能再两个窗口中打开。如下图:
现在,要将 BFStaticFramework 静态库链接到 BFTestFramework,点击 TARGETS
-BFTestFramework
,Build Phases 下,展开 Link Binary With Libraries 面板,添加 BFTestFramework。
之后,BFStaticFramework 任何的修改,都会同步到测试 demo 中。
推荐方案:CocoaPods 库
BFDynamicFramework
集成到 Cocoadpods
,必须要将 BFDynamicFramework 发布,在这里,我们采用本地路径,无须发布到 Cocoapods,podspec
文件以及如何发布库在CocoaPods私有库配置笔记有详细说明。
- 创建库
Podspec
文件
进入到 BFDynamicFramework
路径,创建 Podspec
文件
$ cd BFDynamicFramework
$ pod spec create BFDynamicFramework
该文件指定了库的基本信息:
Pod::Spec.new do |s|
s.name = "BFDynamicFramework"
s.version = "0.0.1"
s.summary = "build frameworks by cocoapods."
s.description = <<-DESC
show you how to build frameworks by cocoapods
DESC
s.homepage = "https://github.com/wenghengcong/BeeFunMac"
s.license = "MIT"
s.author = { "wenghengcong" => "wenghengcong@gamil.com" }
s.source = { :path => '.' }
# 注意代码路径
s.source_files = "Classes", "BFDynamicFramework/Classes/**/*.{h,m}"
# 注意资源路径
s.resources = "BFDynamicFramework/Assets/*.{png,jpg,xib,storyboard,xcassets}", "BFDynamicFramework/Assets/**/*.{png,jpg,xib,storyboard,xcassets}"
end
创建测试工程的 Podfile
进入测试的工程目录:cd BFTestFramework
$ pod init
$ tree -a -L 1
.
├── BFTestFramework
├── BFTestFramework.xcodeproj
└── Podfile
- PodFile 文件编辑如下:
```ruby
Uncomment the next line to define a global platform for your project
platform :ios, ‘9.0’
target ‘BFTestFramework’ do use_frameworks!
指明库podspec的路径,这里是BFDynamicFramework.podspec的路径,在此,BFDynamicFramework和BFTestFramework同级,所以path是../BFDynamicFramework
pod ‘BFDynamicFramework’, :path => ‘../BFDynamicFramework’
end
Workaround for Cocoapods issue #7606
post_install do |installer| installer.pods_project.build_configurations.each do |config| config.build_settings.delete(‘CODE_SIGNING_ALLOWED’) config.build_settings.delete(‘CODE_SIGNING_REQUIRED’) end end
- 安装集成 `pod install` 如下:
[](http://blog-1251606168.file.myqcloud.com/blog/2018-10-18-022412.png)
在 `Deveopment Pods` 下集成了我们需要的 `BFDynamicFramework`,之后,我们可以在测试工程中 `BFTestFramework` 直接修改库源码,并会同步到 `BFDynamicFramework` 库。
<a name="lmw3c"></a>
### 自动化
上面集成过程冗余,每次创建一个库,要测试还需要创建一个 Demo app 测试来,需要编辑两个文件:
- Podspec:库信息文件
- Podfile:Demo app 的引用文件
需要保证以上文件正确,然后才能正确发布、测试。<br />最糟心的是两个分开的工程,需要单独维护。<br />那么,如何在一个工程里维护库源码,而且集成一个 Demo app,最好自动化。<br />就是下面的命令,在 [CocoaPods私有库配置笔记](https://www.yuque.com/liboy/ios-study-notes-zgkfns/cocoapods-si-you-ku-pei-zhi-bi-ji?view=doc_embed)有更详细的说明:
```ruby
$ pod lib create BFTestFramework
以上路径中,Example–就是 Demo app 的可测试工程。对应源码在 Development Pods 中,所有以上,都在一个代码仓库中,可维护性极高。
打包
上面,我们通过将测试与库本身的开发都集中在 Cocoapods 中管理,同样的,我们需要打包,导出 Framework 也采用 Cocoapods 的工具。
// 安装`cocoapods-packager`
$ gem install cocoapods-packager
//打包
$ pod package BFDynamicFramework.podspec --force
其中 package
命令,打包成静态.a
文件:
$ pod package BFDynamicFramework.podspec --library --force
具体查看 BFDynamicFramework.framework
目录,和我们预想的一致:
$ tree -L 4
.
├── BFDynamicFramework -> Versions/Current/BFDynamicFramework
├── Headers -> Versions/Current/Headers
├── Resources -> Versions/Current/Resources
└── Versions
├── A
│ ├── BFDynamicFramework
│ ├── Headers
│ │ └── BFDeviceUtils.h
│ └── Resources
│ ├── G-symbol.png
│ ├── ItemView.xib
│ └── Media.xcassets
└── Current -> A
而且这种打包出来的格式,是 armv7 armv7s i386 x86_64 arm64
多架构的。
所以,这就是库开发的最佳方案之一。