iOS 静态库动态库看这里

4,407 阅读12分钟

问题

先列举一下本文要讲述的问题~解答后会通过演示证明

  • 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

iShot2022-01-03 03.37.36.png

  • Dynamic Framework动态库,系统提供的 framework 都是动态库。比如 UIKit.framework,具有所有动态库的特性。

  • Static Framework静态库,开发者可以制作。可以理解为静态库 = 头文件 + 资源文件 + 二进制代码,具有静态库的属性。

  • Embedded Framework也是动态库的一种,用户可以制作。系统的Framework不需要拷贝到目标程序中,Embedded Framework最后需要拷贝到APP中。他具有部分动态特性,可以在 Extension可执行文件目标APP 之间共享。

  • XCFramework是苹果官⽅推荐的、⽀持的文件格式。支持 xcode11 以上,可以提供多个不同平台的分发二进制文件,xcode会自动判断你需要编译的ipa包是什么架构,使用的时候就不用通过脚本剥离不需要的架构体系。

5、动态库和静态库链接到主程序以后放在什么位置?

iShot2022-01-03 03.41.43.png

6、什么是dead strip

dead strip 可以在编译时把没有用到的代码屏蔽在外,以节约包体积。 iShot2022-01-05 04.49.29.png

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,一个静态库,一个动态库 iShot2021-12-31 13.13.53.png iShot2022-01-04 21.31.06.png

在主工程APP内创建Workspace iShot2021-12-31 13.47.00.png iShot2021-12-31 13.47.16.png

关闭主APP工程,打开workspace添加两个库工程 iShot2021-12-31 13.55.34.png iShot2021-12-31 13.57.43.png iShot2021-12-31 14.51.21.png iShot2021-12-31 14.52.10.png

添加完成后的工程文件目录如下 iShot2021-12-31 13.58.57.png

2、库工程的设置

关闭workspace,打开库工程(两个设置都一样)

Project -> BuildSettings -> Deployment Postprocessing 设置为YES

这个设置相当于 Deployment 一整列的开关,所以一定要打开 iShot2022-01-01 17.51.37.png

并不是所有的符号都是必须的,比如 Debug Map,所以 Xcode 提供了Strip Linked Product去除不需要的符号信息,可以通过设置 Strip Style 参数控制效果。

稍后会演示这个参数如何使用。

去除符号信息后只能使用 dSYM 进行符号化,所以需要将 Debug Information Format 修改为 DWARF with dSYM file

ps:没有 DWARF 调试信息Xcode 靠什么来生成dSYM

还是通过 DWARFXcode 的编译步骤:

  • 生成带有 DWARF 调试信息的可执行文件
  • 提取可执行文件中的信息打包成 dSYM
  • 使用 strip 命令去除符号化信息

iShot2022-01-04 22.06.48.png

Project -> BuildSettings -> Strip Style 设置为 Debugging Symbols(默认)

选择不同的 Strip StyleAPP 构建最后一步的 Strip操作 会带上对应参数。

选择 debugging symbols 在函数调用栈中,可以看到类名和方法名。 iShot2021-12-31 13.36.38.png

添加可以暴露的头文件 iShot2021-12-31 13.40.47.png

3、生成XCConfig配置文件

iShot2022-01-04 23.01.46.png

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

指定使用配置文件 iShot2022-01-05 01.22.29.png

4、初步运行工程

编译成功后,可以看到产物里面会出现动态库.framework及静态库.a文件 iShot2021-12-31 14.55.50.png iShot2021-12-31 14.56.19.png

5、配置运行脚本

接下来,给xcode添加一个脚本,使得编译的时候能把符号表输出到终端。

在主程序的根目录下创建一个名为xcode_run_cmd.sh的脚本文件 iShot2022-01-05 00.52.24.png

代码直接贴出来方便大家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"

iShot2022-01-05 00.57.21.png

6、创建运行脚本的配置文件

打开终端,输入tty,看到一个路径,记下来且不要关闭终端 iShot2022-01-05 01.03.44.png

创建一个名为runConfig.xcconfigXCConfig文件,并且添加如下代码 iShot2022-01-05 01.01.30.png

记得上面第一次创建的配置文件吗,因为XCode只支持一份配置文件,所以不需要改动配置路径

直接在第一份配置里引用runConfig.xcconfig即可

#include "runConfig.xcconfig"

运行工程,接下来查看终端,已经输出的我们的指令~ iShot2022-01-05 01.24.30.png

7、输出符号信息

runConfig.xcconfig里面添加查看符号表的指令

括号内名称就是debugConfig.xcconfig里配置的

// nm 查看符号表
// p:不分类 a:查看所有machO符号,理解成all
// ${地址}
CMD = nm -pa ${MYSTATICLIB_PATH}

iShot2022-01-05 01.28.52.png

在静态库内添加一个测试方法 iShot2022-01-05 01.35.08.png

再次运行代码可以在终端里看到,测试方法的符号 iShot2022-01-05 01.36.00.png

为了测试的方便和清晰,动态库和静态库都各添加上两个类

  • 一个公开类PublibObj
  • 一个私有类PrivateObj 都添加上Test方法 iShot2022-01-05 02.05.41.png

此时输出动态库的符号表,可以看到不同类的方法是放在一起的 iShot2022-01-05 02.09.20.png

再输出静态库的符号表,看到符号表是按照.o文件分类输出的

这也可以印证静态库就是一个个.o文件的合集 iShot2022-01-05 02.11.20.png

最后输出主工程的符号表 iShot2022-01-05 02.39.00.png

8、使用动态库和静态库

主工程内创建一个文件夹并添加两个公开类的引用,调用两个测试方法 iShot2022-01-05 04.06.02.png

再次查看终端,搜索静态库的方法,能查到其符号 iShot2022-01-05 04.08.30.png

再去查找动态库方法时,却发现找不到他的符号 iShot2022-01-05 04.08.59.png

其实上述现象正好印证了动态库和静态库的加载时期并不一样,脚本是在编译时运行的

  • 静态库的方法也是在编译期加载,所以这里能获取到静态库的符号。

  • 动态库的方法则是在运行时加载,所以脚本运行时获取不到动态库的符号。

9、查看静态库的代码

通过objdump指令可以查看具体的代码情况

// objdump 查看
// -macho 查看macho格式的信息
// -d 打印代码块的内容
// ${地址}
CMD = objdump -macho -d ${APP_PATH}

iShot2022-01-05 04.52.37.png

可以看到动态库的代码只有可怜的两行,这里其实是ViewDidLoad在调用 iShot2022-01-05 04.51.50.png

但是动态库的代码除了被调用以外,连实现都在这里输出了 iShot2022-01-05 04.52.07.png

而且能楚的看到,静态库的代码和主程序的其他代码放在一块了~

这是因为静态库在编译时会复制一份代码到全局符号表~

10、静态库的链接方式

前面已经讲述了下列几个参数只对链接静态库生效

  • -all_load:加载全部代码
  • -noall_load:默认参数,屏蔽未用代码
  • -ObjC:加载全部OC相关代码,包括分类
  • -force_load: 可以加载指定静态库的全部代码

下面来稍稍验证一下~

ViewContrller的代码屏蔽 iShot2022-01-05 05.09.23.png

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

Target -> Building Setting -> Other Linker Flag 设置为 -all_load 再次运行 iShot2022-01-05 05.39.02.png

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

接下来把ViewContrller的代码屏蔽放开 iShot2022-01-05 05.21.50.png

Target -> Building Setting -> Other Linker Flag 设置为 -noall_load 再次运行 iShot2022-01-05 05.20.53.png

此时报错无法运行。当然了,所有符号都不加载程序肯定没法跑 iShot2022-01-05 05.21.35.png

-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

终端调用一下 iShot2022-01-06 02.41.30.png

创建成功~ iShot2022-01-06 02.52.08.png

怎么使用?

  • 1、跟普通的动态库一样~XCFramework复制到工程里面
  • 2、Target -> General -> Frameworks,Libraries,and Embeddded Content 添加一下 iShot2022-01-06 03.14.50.png
  • 3、然后import头文件调用就行

总结瞎掰

总结其实也没啥能总了...

就是最近沸点吐槽掘金水文闹的沸沸扬扬...

这里纯瞎说一下

这篇东西3天熬夜断断续续搞出来的,查资料、xcode调试、截图、码字要老命了...

质量嘛,自我感觉还行,纯手打真手把手教程,小白跟着也能走完整个过程

但是也有可能被认为是水文,毕竟没有很深奥的东西

其实吧

像我记性不好的人,喜欢记录一下折腾过的东西和踩过的坑

以后用到的时候翻一下自己的主页就找到,也不用去搜,方便省时间

万一有相同问题的朋友能搜到看到,那帮到人就更开心~

技术社区嘛,技术的东西多包容~