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