iOS 静态库动态库看这里
问题
先列举一下本文要讲述的问题~解答后会通过演示证明
- 1、什么是动态库?
- 2、什么是动态库?
- 3、动态库和静态库的区别是什么?
- 4、动态库、静态库、
framework是什么关系? - 5、动态库和静态库链接到主程序以后放在什么位置?
- 6、什么是
dead strip? - 7、
-all_load、-noall_load、-ObjC、-force_load参数的区别? - 8、什么是
tbd文件? - 9、动态库和静态库的选择?
- 10、为什么项目里动态库不能超过六个?
- 11、怎么剥离动态库里不需要的架构?
1、什么是静态库?
静态库是静态链接库;是多个目标文件经过压缩打包后形成的文件包。以下都是静态库的类型
- Windows 的
.lib - Linux 的
.a - MacOS 独有的
.framework
2、什么是动态库?
- 动态库是动态链接库,是实现共享函数库的一种方式。
- 动态库在编译的时候不会被拷贝到目标程序中,目标程序只会存储下动态库的引用。
- 真正用到动态库内的函数时才会去查找 - 绑定 - 使用函数。
- 动态库的格式有:
.framework、.dylib、.tbd……
3、动态库和静态库的区别
- 静态库
- 在编译时加载
- 优点:代码装载和执行速度比动态库快。
- 缺点:浪费内存和磁盘空间,模块更新困难。
- 动态库
- 在运行时加载
- 优点:体积比静态库小很多,更加节省内存。
- 缺点:代码装载和执行速度比静态库慢。
- 备注:
- 体积小于
最小单位16k的静态库编译出来的动态库体积会等于16k。 - 换成动态库会导致⼀些速度变低,但是会通过延迟绑定(
Lazy Binding)技术优化。 - 延迟绑定:首次使用的时候查找并记录方法的内存地址,后续调用就可以省略查找流程。
- 体积小于
4、动态库、静态库、framework是什么关系?
- 库是已经编译完成的二进制文件。
- 代码需要提供给外部使用又不想代码被更改,就可以把代码封装成库,只暴露头文件以供调用。
- 希望提高编译速度,可以把部分代码封装成库,编译时只需要链接。
- 库都是需要链接的,链接库的方式有静态和动态,所以就产生了静态库和动态库。
framework其实是一种文件的打包方式,把头文件、二进制文件、资源文件封装在一起,方便管理和分发。所以动态库和静态库的文件格式都会有.framework。

-
Dynamic Framework动态库,系统提供的framework都是动态库。比如UIKit.framework,具有所有动态库的特性。 -
Static Framework静态库,开发者可以制作。可以理解为静态库 = 头文件 + 资源文件 + 二进制代码,具有静态库的属性。 -
Embedded Framework也是动态库的一种,用户可以制作。系统的Framework不需要拷贝到目标程序中,Embedded Framework最后需要拷贝到APP中。他具有部分动态特性,可以在Extension可执行文件和目标APP之间共享。 -
XCFramework是苹果官⽅推荐的、⽀持的文件格式。支持 xcode11 以上,可以提供多个不同平台的分发二进制文件,xcode会自动判断你需要编译的ipa包是什么架构,使用的时候就不用通过脚本剥离不需要的架构体系。
5、动态库和静态库链接到主程序以后放在什么位置?

6、什么是dead strip?
dead strip 可以在编译时把没有用到的代码屏蔽在外,以节约包体积。

7、-all_load、-noall_load、-ObjC、-force_load 参数的区别?
这几个参数只对链接静态库生效
-all_load:加载全部代码-noall_load:默认参数,屏蔽未用代码-ObjC:加载全部OC相关代码,包括分类-force_load: 可以加载指定静态库的全部代码
8、什么是tbd文件?
tbd全称是txt-based stub libraries,本质上是一个YMAL描述文本文件。- 用于记录动态库信息,包括 导出符号、动态库框架信息、动态库依赖信息。
- 真机情况下动态库都在手机内
xcode开发时相关的库存在MacOS,不用存储Xcode内。使用tbd格式的伪framework可以大大减少xcode的大小。
9、动态库和静态库的选择?
- 相同代码打包成动态库比静态库体积更小
- 静态库是
.o文件的合集,每个.o都包含全局符号,多个.o会重复包含全局符号,库体积更大。 - 动态库是编译链接产物,所有符号都放在一起,全局符号只存1次,库体积更小。
- 静态库是
- 使用静态库的工程生成
ipa包体积更小- 动态库是编译链接的最终产物,无法优化,需要拷贝到
frameworks文件夹中,会增加ipa包体积。 - 工程编译默认将静态库代码合并到
APP主程序符号表,.framework格式静态库不含资源文件的时候可以选择Do not embed,这样静态库文件不会嵌入包,可以缩小安装包体积 - 静态库还可以通过设置
-noall_load、-ObjC、-force_load屏蔽不需要的代码。
- 动态库是编译链接的最终产物,无法优化,需要拷贝到
10、为什么项目里动态库不能超过六个?
因为动态库是APP运行时动态加载的,数量多了会影响启动速度。
11、怎么剥离动态库里不需要的架构?
- 1、编译库文件
- 2、
lipo指令剥离不需要的架构 - 3、重新拖到项目
演示
接下来手把手通过Xcode和模拟脚本模拟打包过程并验证上述的问题。
1、创建项目
首先在同一目录创建一个主工程APP,一个静态库,一个动态库

在主工程APP内创建Workspace

关闭主APP工程,打开workspace添加两个库工程

添加完成后的工程文件目录如下

2、库工程的设置
关闭workspace,打开库工程(两个设置都一样)
Project -> BuildSettings -> Deployment Postprocessing 设置为YES
这个设置相当于 Deployment 一整列的开关,所以一定要打开

并不是所有的符号都是必须的,比如 Debug Map,所以 Xcode 提供了Strip Linked Product去除不需要的符号信息,可以通过设置 Strip Style 参数控制效果。
稍后会演示这个参数如何使用。
去除符号信息后只能使用 dSYM 进行符号化,所以需要将 Debug Information Format 修改为 DWARF with dSYM file。
ps:没有 DWARF 调试信息,Xcode 靠什么来生成dSYM?
还是通过 DWARF,Xcode 的编译步骤:
- 生成带有
DWARF调试信息的可执行文件 - 提取可执行文件中的信息打包成
dSYM - 使用 strip 命令去除符号化信息

Project -> BuildSettings -> Strip Style 设置为 Debugging Symbols(默认)
选择不同的 Strip Style,APP 构建最后一步的 Strip操作 会带上对应参数。
选择 debugging symbols 在函数调用栈中,可以看到类名和方法名。

添加可以暴露的头文件

3、生成XCConfig配置文件

先创建一个配置文件debugConfig.cxconfig,里面指定库以及主程序路径

指定使用配置文件

4、初步运行工程
编译成功后,可以看到产物里面会出现动态库.framework及静态库.a文件

5、配置运行脚本
接下来,给xcode添加一个脚本,使得编译的时候能把符号表输出到终端。
在主程序的根目录下创建一个名为xcode_run_cmd.sh的脚本文件

代码直接贴出来方便大家cv操作。
#!/bin/sh
RunCommand() {
#判断全局字符串VERBOSE_SCRIPT_LOGGING是否为空。-n string判断字符串是否非空
#[[是 bash 程序语言的关键字。用于判断
if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
#作为一个字符串输出所有参数。使用时加引号"$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数
if [[ -n "$TTY" ]]; then
echo "♦ $@" 1>$TTY
else
echo "♦ $*"
fi
echo "------------------------------------------------------------------------------" 1>$TTY
fi
#与$*相同。但是使用时加引号,并在引号中返回每个参数。"$@" 会将各个参数分开,以"$1" "$2" … "$n" 的形式输出所有参数
if [[ -n "$TTY" ]]; then
eval "$@" &>$TTY
else
"/bin/bash $@"
fi
#显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。
return $?
}
EchoError() {
#在shell脚本中,默认情况下,总是有三个文件处于打开状态,标准输入(键盘输入)、标准输出(输出到屏幕)、标准错误(也是输出到屏幕),它们分别对应的文件描述符是0,1,2
# > 默认为标准输出重定向,与 1> 相同
# 2>&1 意思是把 标准错误输出 重定向到 标准输出.
# &>file 意思是把标准输出 和 标准错误输出 都重定向到文件file中
# 1>&2 将标准输出重定向到标准错误输出。实际上就是打印所有参数已标准错误格式
if [[ -n "$TTY" ]]; then
echo "$@" 1>&2>$TTY
else
echo "$@" 1>&2
fi
}
RunCMDToTTY() {
if [[ ! -e "$TTY" ]]; then
EchoError "=========================================="
EchoError "ERROR: Not Config tty to output."
exit -1
fi
# CMD = 运行到命令
# CMD_FLAG = 运行到命令参数
# TTY = 终端
if [[ -n "$CMD" ]]; then
RunCommand $CMD
else
EchoError "=========================================="
EchoError "ERROR:Failed to run CMD. THE CMD must not null"
fi
}
RunCMDToTTY
主程序里配置脚本路径 Target -> Build Phases -> Run TTY Script 添加
/bin/bash -c "${SRCROOT}/xcode_run_cmd.sh"

6、创建运行脚本的配置文件
打开终端,输入tty,看到一个路径,记下来且不要关闭终端

创建一个名为runConfig.xcconfig的XCConfig文件,并且添加如下代码

记得上面第一次创建的配置文件吗,因为XCode只支持一份配置文件,所以不需要改动配置路径
直接在第一份配置里引用runConfig.xcconfig即可
#include "runConfig.xcconfig"
运行工程,接下来查看终端,已经输出的我们的指令~

7、输出符号信息
在runConfig.xcconfig里面添加查看符号表的指令
括号内名称就是debugConfig.xcconfig里配置的
// nm 查看符号表
// p:不分类 a:查看所有machO符号,理解成all
// ${地址}
CMD = nm -pa ${MYSTATICLIB_PATH}

在静态库内添加一个测试方法

再次运行代码可以在终端里看到,测试方法的符号

为了测试的方便和清晰,动态库和静态库都各添加上两个类
- 一个公开类
PublibObj - 一个私有类
PrivateObj都添加上Test方法
此时输出动态库的符号表,可以看到不同类的方法是放在一起的

再输出静态库的符号表,看到符号表是按照.o文件分类输出的
这也可以印证静态库就是一个个.o文件的合集

最后输出主工程的符号表

8、使用动态库和静态库
主工程内创建一个文件夹并添加两个公开类的引用,调用两个测试方法

再次查看终端,搜索静态库的方法,能查到其符号

再去查找动态库方法时,却发现找不到他的符号

其实上述现象正好印证了动态库和静态库的加载时期并不一样,脚本是在编译时运行的
-
静态库的方法也是在编译期加载,所以这里能获取到静态库的符号。
-
动态库的方法则是在运行时加载,所以脚本运行时获取不到动态库的符号。
9、查看静态库的代码
通过objdump指令可以查看具体的代码情况
// objdump 查看
// -macho 查看macho格式的信息
// -d 打印代码块的内容
// ${地址}
CMD = objdump -macho -d ${APP_PATH}

可以看到动态库的代码只有可怜的两行,这里其实是ViewDidLoad在调用

但是动态库的代码除了被调用以外,连实现都在这里输出了

而且能楚的看到,静态库的代码和主程序的其他代码放在一块了~
这是因为静态库在编译时会复制一份代码到全局符号表~
10、静态库的链接方式
前面已经讲述了下列几个参数只对链接静态库生效
-all_load:加载全部代码-noall_load:默认参数,屏蔽未用代码-ObjC:加载全部OC相关代码,包括分类-force_load: 可以加载指定静态库的全部代码
下面来稍稍验证一下~
把ViewContrller的代码屏蔽

运行后在终端是无法找到任何与静态库相关的内容,因为主工程是默认开启dead strip

Target -> Building Setting -> Other Linker Flag 设置为 -all_load 再次运行

重新搜索到staticPublicTest的实现,这就证明了 -all_load可以加载全部代码

接下来把ViewContrller的代码屏蔽放开

Target -> Building Setting -> Other Linker Flag 设置为 -noall_load 再次运行

此时报错无法运行。当然了,所有符号都不加载程序肯定没法跑

-ObjC这个不好演示,就稍微讲一讲吧~
他的作用是将静态库中任何Objective-C代码都链接到APP中。
任何也就包括了Category的方法,这就导致使用-ObjC可能会链接很多静态库中未被使用的Objective-C代码,极大的增加APP的代码体积。
至于-force_load和前面-all_load的用法基本一致,只需要在参数后面添加静态库的地址即可,这样就加载指定静态库的全部代码~
10、构建 XCFramework 并使用
在动态库工程的根目录创建一个脚本pack_xcframe.sh,复制下列代码
FREAMEWORK_NAME='MyDynamicLib'修改为你的库名就能用了
#!/bin/sh -e
# Framework/工程 的名字
FREAMEWORK_NAME='MyDynamicLib'
# 所有产物的目标路径
OUTPUT_DIR="./Build/${FREAMEWORK_NAME}"
# Device Archive 生成的 .xcarchive 存放路径。在工程的根目录下生成 Build 文件夹。
ARCHIVE_PATH_IOS_DEVICE="./${OUTPUT_DIR}/${FREAMEWORK_NAME}_device.xcarchive"
# Simulator Archive 生成的 .xcarchive 存放路径。
ARCHIVE_PATH_IOS_SIMULATOR="./${OUTPUT_DIR}/${FREAMEWORK_NAME}_simulator.xcarchive"
# 制作完 framework 后,是否在 Finder 中打开
REVEAL_XCFRAMEWORK_IN_FINDER=true
# 生成单个平台的 .xcarchive. 接收4个参数, scheme, destination, archivePath,指令集.
function archiveOnePlatform {
echo "▸ Starts archiving the scheme: ${1} for destination: ${2};\n▸ Archive path: ${3}"
xcodebuild archive \
-scheme "${1}" \
-destination "${2}" \
-archivePath "${3}" \
VALID_ARCHS="${4}" \
SKIP_INSTALL=NO\
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
}
function archiveAllPlatforms {
# Platform Destination
# iOS generic/platform=iOS
# iOS Simulator generic/platform=iOS Simulator
# iPadOS generic/platform=iPadOS
# iPadOS Simulator generic/platform=iPadOS Simulator
# macOS generic/platform=macOS
# tvOS generic/platform=tvOS
# watchOS generic/platform=watchOS
# watchOS Simulator generic/platform=watchOS Simulator
# carPlayOS generic/platform=carPlayOS
# carPlayOS Simulator generic/platform=carPlayOS Simulator
SCHEME=${1}
archiveOnePlatform $SCHEME "generic/platform=iOS Simulator" ${ARCHIVE_PATH_IOS_SIMULATOR} "x86_64"
archiveOnePlatform $SCHEME "generic/platform=iOS" ${ARCHIVE_PATH_IOS_DEVICE} "armv7 arm64"
}
function makeXCFramework {
mkdir -p "${OUTPUT_DIR}"
FRAMEWORK_RELATIVE_PATH="Products/Library/Frameworks"
sudo xcodebuild -create-xcframework \
-framework "${ARCHIVE_PATH_IOS_DEVICE}/${FRAMEWORK_RELATIVE_PATH}/${FREAMEWORK_NAME}.framework" \
-framework "${ARCHIVE_PATH_IOS_SIMULATOR}/${FRAMEWORK_RELATIVE_PATH}/${FREAMEWORK_NAME}.framework" \
-output "${OUTPUT_DIR}/${FREAMEWORK_NAME}.xcframework"
}
echo "##################### 启动脚本 #####################"
echo "##################### 重置目标路径 ${OUTPUT_DIR} #####################"
sudo rm -rf $OUTPUT_DIR
echo "##################### 正在归档 ${FREAMEWORK_NAME} #####################"
archiveAllPlatforms $FREAMEWORK_NAME
echo "##################### 正在制作 framework: ${FREAMEWORK_NAME}.xcframework #####################"
makeXCFramework
if [ ${REVEAL_XCFRAMEWORK_IN_FINDER} = true ]; then
sudo open "${OUTPUT_DIR}/"
fi
终端调用一下

创建成功~

怎么使用?
- 1、跟普通的动态库一样~
XCFramework复制到工程里面 - 2、
Target -> General -> Frameworks,Libraries,and Embeddded Content添加一下
- 3、然后
import头文件调用就行
总结瞎掰
总结其实也没啥能总了...
这里纯瞎说一下
这篇东西3天熬夜断断续续搞出来的,查资料、xcode调试、截图、码字要老命了...
质量嘛,自我感觉还行,纯手打真手把手教程,小白跟着也能走完整个过程
但是也有可能被认为是水文,毕竟没有很深奥的东西
其实吧
像我记性不好的人,喜欢记录一下折腾过的东西和踩过的坑
以后用到的时候翻一下自己的主页就找到,也不用去搜,方便省时间
万一有相同问题的朋友能搜到看到,那帮到人就更开心~
技术社区嘛,技术的东西多包容~
转载自:https://juejin.cn/post/7049803824214573086