库就是程序代码的集合,将N个文件组织起来,是共享程序代码的一种方式。库从本质上来说是一种可执行代码的二进制格式,可以被载入内存中执行。库实现了程序某个功能模块的模块化,它便于我们共享(公用)、维护、独立和安全。
一般分为静态库和动态库。

  • 静态库:以.a.framework为文件后缀名。
  • 动态库:以.tbd(之前叫.dylib) 和 .framework 为文件后缀名。(系统直接提供给我们的framework都是动态库!)

库的分类

  • 开源库:源代码是公开的,可以看到每个实现文件(.m文件)的实现,例如GitHub上的常用的开源库:AFNetworking、SDWebImage等;
  • 闭源库:不公开源代码,是经过编译后的二进制文件,看不到具体的实现。闭源库又分为:静态库和动态库

静态库

静态库链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
静态库和动态库 - 图1

动态库

动态库链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。
静态库和动态库 - 图2

  • 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. 创建静态库工程

    静态库和动态库 - 图3

静态库和动态库 - 图4

然后直接编译,可以得到.a 为后缀的静态库。
静态库和动态库 - 图5
屏幕快照 2018-09-19 上午9.28.03

2. 编译静态库

设置 Edit Scheme -> Build Configuration-> 选为 Release
完成代码编写,并选中 Device 为真机编译,还是模拟器编译,不同的 Device 下编译出的静态库是不同的,真机编译可能在模拟器中使用会 crash,同样模拟器编译出来在真机运行中会发生问题。

所以,需要在真机及模拟器下各自编译,导出编译后的库。
静态库和动态库 - 图6

导出后,可以查看对应的库信息:

  1. $ lipo -info libBFStaticLib_device.a
  2. input file libBFStaticLib_device.a is not a fat file
  3. Non-fat file: libBFStaticLib_device.a is architecture: arm64
  4. $ lipo -info libBFStaticLib_simulator.a
  5. input file libBFStaticLib_simulator.a is not a fat file
  6. Non-fat file: libBFStaticLib_simulator.a is architecture: x86_64

3. 合并静态库

上一步生成的不同指令集下的静态库,需要合并才能支持多设备,如下:

  1. //lipo -create ***.a ***.a -output output_lib_path
  2. $ lipo -create libBFStaticLib_simulator.a libBFStaticLib_device.a -output ./libBFStaticLib
  3. $ lipo -info libBFStaticLib
  4. 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
静态库和动态库 - 图7

制作静态库 framework

一、创建工程及工程设置

image.png
创建一个项目名称为:BFStaticFramework 的静态库。
其工程设置中,如下

  • 在 Linking 里,将 Dead Code Stripping 设置为 NO,用于删除对象文件中不需要加载的符号,减小二进制文件大小
  • Link With Standard Libraries 设置为 NO,链接使用标准静态库。
  • Mach-O Type 改为 Static Librariy,选择 Mach-O 的类型。

静态库和动态库 - 图9

二、添加头文件

将需要暴露的头文件放在 Build Phases 对应的分组下:

  • Public:对外暴露的头文件。
  • Private:查看 Framework 下的头文件,依然是暴露出来的。
  • Project:该分组头文件对工程来说才是 “私有” 的
    所以,将你的头文件或者在 Public 下,或者在 Project 下。尽可能少的暴露头文件,将其他类放在 Project 中。

静态库和动态库 - 图10

三、导出 Framework

在这里,我们创建一个 Aggregate
静态库和动态库 - 图11

添加一个脚本:
静态库和动态库 - 图12
屏幕快照 2018-10-17 下午4.07.14
为 OutputFramework 添加一个脚本

  1. # Sets the target folders and the final framework product.
  2. # 如果工程名称和Framework的Target名称不一样的话,要自定义FMKNAME
  3. # 例如: FMK_NAME = "MyFramework"
  4. FMK_NAME=${PROJECT_NAME}
  5. # Install dir will be the final output to the framework.
  6. # The following line create it in the root folder of the current project.
  7. INSTALL_DIR=${SRCROOT}/Products/${FMK_NAME}.framework
  8. # Working dir will be deleted after theframework creation.
  9. WRK_DIR=build
  10. DEVICE_DIR=${WRK_DIR}/Release-iphoneos/${FMK_NAME}.framework
  11. SIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator/${FMK_NAME}.framework
  12. # -configuration ${CONFIGURATION}
  13. # Clean and Building both architectures.
  14. # 在Xcode 10时,去掉下面每一句末尾的clean build,因为会每次都清空build文件夹,导致最后到处的framework为空
  15. xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphoneos
  16. xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphonesimulator
  17. # Cleaning the oldest.
  18. if [ -d "${INSTALL_DIR}" ]
  19. then
  20. rm -rf "${INSTALL_DIR}"
  21. fi
  22. mkdir -p "${INSTALL_DIR}"
  23. cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
  24. # Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.
  25. lipo -create "${DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${INSTALL_DIR}/${FMK_NAME}"
  26. rm -r "${WRK_DIR}"
  27. open "${INSTALL_DIR}"

运行脚本,输出 Framework,记住,一定要选中真机,否则生成的架构只支持 x86_64 arm64

四、验证

  1. $ cd framework路径
  2. $ lipo -info BFStaticFramework
  3. 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 中。

静态库和动态库 - 图13

  • 方案二:尝试将动态库的 Status 的状态修改为 Optional,改成 Optional 有效的前提是动态库中不包含分类,或者包含了分类但是未进行调用。否则,只能按方案一。

静态库和动态库 - 图14

添加资源

我们继续使用 BFStaticFramework

一、创建 Bundle Target

在当前工程中,新建一个 Target。
静态库和动态库 - 图15
屏幕快照 2018-10-18 下午6.14.40

二、Build Settings 设置

选中对应的 Target:IconImage
静态库和动态库 - 图16

下面点击 Build Settings,进行一些设置:

  • Base SDK: 选中,并按下 Delete,就会默认使用最新的 iOS SDK。
  • Supported Platforms:选中 iOS
  • Product 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
    所以一定要修改。
    静态库和动态库 - 图17

三、编译出包

1. 直接出包

编译出包,得到 BFStaticFramework.bundle
为了确保编译 BFStaticFramework.framework 时,也编译 BFStaticFramework.bundle,在之前输出 Framework 的 target 下,添加 target dependencies,如下:
静态库和动态库 - 图18

并且,为了是的 bundle 文件和 Framework 在同一目录下。
在 OutputFramework 对应的脚本中添加:

  1. .....
  2. # Copy the resources bundle
  3. ditto "${BUILT_PRODUCTS_DIR}/${FMK_NAME}.bundle" \
  4. "${SRCROOT}/Products/${FMK_NAME}.bundle"
  5. open "${INSTALL_DIR}"

如下:
静态库和动态库 - 图19

2. 导入 Framework

进一步,如果要将 bundle 文件打入 Framework 包中:
首先,将上一步中得到的 bundle 文件直接拖入 BFStaticFramework 工程中,并选中不要 Copy items if needed
静态库和动态库 - 图20

得到如下的结构:
静态库和动态库 - 图21

四、使用

如果,需要将对应的 bundle 拖到对应的工程中去使用:

  1. UIImage *image = [UIImage imageNamed:@"BFStaticFramework.bundle/qq"];
  2. //or
  3. UIImage *image = [UIImage imageNamed:@"qq" inBundle:[NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"BFStaticFramework" ofType: @"bundle"]] compatibleWithTraitCollection:nil];

或者新建分类:

  1. //.h
  2. #import <UIKit/UIKit.h>
  3. @interface UIImage (Resource)
  4. + (UIImage *)resourceImageNamed:(NSString *)name;
  5. @end
  6. /.m
  7. #import "UIImage+Resource.h"
  8. @implementation UIImage (Resource)
  9. + (UIImage *)resourceImageNamed:(NSString *)name{
  10. //先从默认目录里读
  11. UIImage *imageFromMainBundle = [UIImage imageNamed:name];
  12. if (imageFromMainBundle) {
  13. return imageFromMainBundle;
  14. }
  15. //读不到再去Bundle里读
  16. //此处Scale是判断图片是@2x还是@3x
  17. NSInteger scale = (NSInteger)[[UIScreen mainScreen] scale];
  18. for (NSInteger i = scale; i >= 1; i--) {
  19. NSString *filepath = [self getImagePath:name scale:i];
  20. UIImage *tempImage = [UIImage imageWithContentsOfFile:filepath];
  21. if (tempImage) {
  22. return tempImage;
  23. }
  24. }
  25. return nil;
  26. }
  27. + (NSString *)getImagePath:(NSString *)name scale:(NSInteger)scale{
  28. NSURL *bundleUrl = [[NSBundle mainBundle] URLForResource:@"Resource" withExtension:@"bundle"];
  29. NSBundle *customBundle = [NSBundle bundleWithURL:bundleUrl];
  30. NSString *bundlePath = [customBundle bundlePath];
  31. NSString *imgPath = [bundlePath stringByAppendingPathComponent:name];
  32. NSString *pathExtension = [imgPath pathExtension];
  33. //没有后缀加上PNG后缀
  34. if (!pathExtension || pathExtension.length == 0) {
  35. pathExtension = @"png";
  36. }
  37. //Scale是根据屏幕不同选择使用@2x还是@3x的图片
  38. NSString *imageName = nil;
  39. if (scale == 1) {
  40. imageName = [NSString stringWithFormat:@"%@.%@", [[imgPath lastPathComponent] stringByDeletingPathExtension], pathExtension];
  41. }
  42. else {
  43. imageName = [NSString stringWithFormat:@"%@@%ldx.%@", [[imgPath lastPathComponent] stringByDeletingPathExtension], (long)scale, pathExtension];
  44. }
  45. //返回删掉旧名称加上新名称的路径
  46. return [[imgPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:imageName];
  47. }
  48. @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 作为参考,目录如下::

  1. $ tree -a -L 3
  2. .
  3. ├── Alamofire -> Versions/Current/Alamofire
  4. ├── Headers -> Versions/Current/Headers
  5. ├── Modules -> Versions/Current/Modules
  6. ├── Resources -> Versions/Current/Resources
  7. └── Versions
  8. ├── A
  9. ├── Alamofire
  10. ├── Headers
  11. ├── Modules
  12. └── Resources
  13. └── Current -> A

而我们上面制作的,以 BFDynamicFramework 为例,结构如下:

  1. .
  2. ├── .DS_Store
  3. └── BFDynamicFramework.framework
  4. ├── BFDynamicFramework
  5. ├── Headers
  6. ├── BFDeviceUtils.h
  7. ├── BFDynamicFramework.h
  8. └── UIView+Frame.h
  9. ├── Info.plist
  10. └── Modules
  11. └── 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”

  1. 之后,编译输出的结构就会更改:
  2. ```bash
  3. $ tree -L 3
  4. .
  5. ├── BFStaticFramework
  6. ├── Headers
  7. │ ├── BFDeviceUtils.h
  8. │ ├── BFStaticFramework.h
  9. │ ├── Headers -> Versions/Current/Headers
  10. │ └── UIView+Frame.h
  11. ├── Info.plist
  12. └── Versions
  13. ├── A
  14. │ └── Headers
  15. └── Current -> A

Cocoapods:自动生成规范的目录结构。

调试

在前面,我们创建了 BFStaticFramework 静态库(也适用于动态库),现在我们创建一个调试工程 BFTestFramework。
打开 BFTestFramework,将 BFStaticFramework.xcodeproj 拖入 BFTestFramework,注意,拖入是,一定要确保 Xcode 没有打开 BFStaticFramework.xcodeproj,因为同一个工程,不能再两个窗口中打开。如下图:
image.png
现在,要将 BFStaticFramework 静态库链接到 BFTestFramework,点击 TARGETS-BFTestFramework,Build Phases 下,展开 Link Binary With Libraries 面板,添加 BFTestFramework。
静态库和动态库 - 图23
之后,BFStaticFramework 任何的修改,都会同步到测试 demo 中。

推荐方案:CocoaPods 库

BFDynamicFramework 集成到 Cocoadpods,必须要将 BFDynamicFramework 发布,在这里,我们采用本地路径,无须发布到 Cocoapods,podspec 文件以及如何发布库在CocoaPods私有库配置笔记有详细说明。

  • 创建库 Podspec 文件

进入到 BFDynamicFramework 路径,创建 Podspec 文件

  1. $ cd BFDynamicFramework
  2. $ pod spec create BFDynamicFramework

下面是具体的路径;
静态库和动态库 - 图24

该文件指定了库的基本信息:

  1. Pod::Spec.new do |s|
  2. s.name = "BFDynamicFramework"
  3. s.version = "0.0.1"
  4. s.summary = "build frameworks by cocoapods."
  5. s.description = <<-DESC
  6. show you how to build frameworks by cocoapods
  7. DESC
  8. s.homepage = "https://github.com/wenghengcong/BeeFunMac"
  9. s.license = "MIT"
  10. s.author = { "wenghengcong" => "wenghengcong@gamil.com" }
  11. s.source = { :path => '.' }
  12. # 注意代码路径
  13. s.source_files = "Classes", "BFDynamicFramework/Classes/**/*.{h,m}"
  14. # 注意资源路径
  15. s.resources = "BFDynamicFramework/Assets/*.{png,jpg,xib,storyboard,xcassets}", "BFDynamicFramework/Assets/**/*.{png,jpg,xib,storyboard,xcassets}"
  16. end

至此,本地库已经支持集成。

创建测试工程的 Podfile

进入测试的工程目录:cd BFTestFramework

静态库和动态库 - 图25
创建 Podfile 文件,该文件指明需要集成哪些库。

  1. $ pod init
  2. $ tree -a -L 1
  3. .
  4. ├── BFTestFramework
  5. ├── BFTestFramework.xcodeproj
  6. └── 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

  1. - 安装集成 `pod install` 如下:
  2. [![](https://cdn.nlark.com/yuque/0/2020/png/126194/1592468618629-b3dad7ec-102d-42b8-b33a-4692b25935f8.png#align=left&display=inline&height=329&margin=%5Bobject%20Object%5D&originHeight=497&originWidth=800&size=0&status=done&style=none&width=530)](http://blog-1251606168.file.myqcloud.com/blog/2018-10-18-022412.png)
  3. `Deveopment Pods` 下集成了我们需要的 `BFDynamicFramework`,之后,我们可以在测试工程中 `BFTestFramework` 直接修改库源码,并会同步到 `BFDynamicFramework` 库。
  4. <a name="lmw3c"></a>
  5. ### 自动化
  6. 上面集成过程冗余,每次创建一个库,要测试还需要创建一个 Demo app 测试来,需要编辑两个文件:
  7. - Podspec:库信息文件
  8. - PodfileDemo app 的引用文件
  9. 需要保证以上文件正确,然后才能正确发布、测试。<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)有更详细的说明:
  10. ```ruby
  11. $ pod lib create BFTestFramework

静态库和动态库 - 图26

以上路径中,Example–就是 Demo app 的可测试工程。对应源码在 Development Pods 中,所有以上,都在一个代码仓库中,可维护性极高。

打包

上面,我们通过将测试与库本身的开发都集中在 Cocoapods 中管理,同样的,我们需要打包,导出 Framework 也采用 Cocoapods 的工具。

  1. // 安装`cocoapods-packager`
  2. $ gem install cocoapods-packager
  3. //打包
  4. $ pod package BFDynamicFramework.podspec --force

其中 package 命令,打包成静态.a 文件:

  1. $ pod package BFDynamicFramework.podspec --library --force

--force 是否覆盖原目录。
最后目录结果如下:
静态库和动态库 - 图27

具体查看 BFDynamicFramework.framework 目录,和我们预想的一致:

  1. $ tree -L 4
  2. .
  3. ├── BFDynamicFramework -> Versions/Current/BFDynamicFramework
  4. ├── Headers -> Versions/Current/Headers
  5. ├── Resources -> Versions/Current/Resources
  6. └── Versions
  7. ├── A
  8. ├── BFDynamicFramework
  9. ├── Headers
  10. └── BFDeviceUtils.h
  11. └── Resources
  12. ├── G-symbol.png
  13. ├── ItemView.xib
  14. └── Media.xcassets
  15. └── Current -> A

而且这种打包出来的格式,是 armv7 armv7s i386 x86_64 arm64 多架构的。
所以,这就是库开发的最佳方案之一。