组件化场景下动态库与静态库依赖分析

405 阅读6分钟

背景

随着项目迭代和功能演进,大多数项目都会变的臃肿而复杂,组件化是一种将应用拆分为多个独立、可复用模块(组件)的架构思想;

  • 组件化的使用场景广泛,下面场景都可以使用组件化:
  1. 多人协作开发的大型/中型项目
  2. 多App共用基础能力
  3. 插件化/动态化需求
  4. 独立业务团队/外包协作
  5. 代码复用和开源
  • 组件化能够有些解决上述场景下的一些问题,主要包括:
  1. 提高可维护性和可扩展性
  2. 提升开发效率
  3. 优化项目管理
  4. 支持动态化和灵活发布
  • 组件化经常依赖一些常见的组件管理工具:
  1. CocoaPods
  2. Swift Package Manager
  3. Carthage等
  • 项目形态
  1. 三方库->组件->主项目是比较常见的组件化项目结构;
  2. .astatic frameworkdynamic framework是常见的库文件类型(二进制)

三方库->组件->主项目.png 通过这些工具我们可以轻松管理组件和三方依赖,但三方依赖和组件本身还是会产生一些不可预知的问题,可能会导致组件或者三方库在主项目中产生一些依赖错误,本文我们将重点分析下组件化中常见的一些场景问题和解决方法;

  • 我们先假定以下场景:
  1. TestFramework是我们开发的组件库;
  2. MBProgressHUDMJExtension是组件库依赖的三方库,通过pod编译为动态库或静态库
  3. 主项目依赖TestFramework,但不直接依赖MBProgressHUDMJExtension

基于以上信息,最终可能会有以下问题场景:

A. 主项目使用动态库TestFramework

A1. TestFramework通过Pod依赖三方库(dynamic framework)
现象

编译不报错,运行时报错 dyld[30339]: Library not loaded: @rpath/MBProgressHUD.framework/MBProgressHUD Referenced from: <9FB255F3-CED4-32E5-A9EF-10E473D3BAF6>

根本原因

通过pod依赖的三方动态库无法被动态库TestFramework合并,导致主项目使用时报错;

  • 编译/链接阶段,主项目只需要能找到 TestFramework.framework里声明的符号即可;
  • TestFramework.framework 的二进制里,引用了 MB/MJ的符号,但这些符号在TestFramework的链接阶段已经解决(因为 TestFramework已经链接了 MB/MJ)。
  • 主项目编译时不需要直接看到MB/MJ的实现,只要TestFramework 能链接通过,主项目就能编译通过。
解决办法

1、通过构建私有库并且在podspec中配置对MBProgressHUD的依赖,来保证主项目可以正常引用动态库MBProgressHUD; 2、直接把MBProgressHUDMBProgressHUD这两个动态库和TestFramework一起提供给主项目,主项目进行embed &sign in后即可

A2.TestFramework通过Pod依赖三方库(static framework)
现象

主项目可以正常编译运行

根本原因
  1. 动态库(.framework 或 .dylib)在构建时,必须生成一个完整的、可执行的二进制文件,因此,链接器必须立即解析并链接所有依赖的静态库,将它们的内容合并进动态库的二进制文件中。动态库构建完成后,就是一个自包含的、独立的二进制文件。
  2. 通过pod依赖的三方静态库(framework类型)被动态库TestFramework所吸收,导致主项目只需要引用动态库TestFramework本身即可;
A3. TestFramework通过Pod依赖三方库(static .a)
现象

主项目可以正常编译运行

根本原因
  1. 动态库(.framework 或 .dylib)在构建时,必须生成一个完整的、可执行的二进制文件,因此,链接器必须立即解析并链接所有依赖的静态库,将它们的内容合并进动态库的二进制文件中。动态库构建完成后,就是一个自包含的、独立的二进制文件。
  2. 通过pod依赖的三方静态库(framework类型)被动态库TestFramework所吸收,导致主项目只需要引用动态库TestFramework本身即可;

B. 主项目使用静态库TestFramework

B1. TestFramework 通过 Pod 依赖三方库(static.a)
现象

主项目编译不过,经典报错Undefined symbol: _OBJC_CLASS_$_MBProgressHUD

根本原因

静态框架(Static Framework)在构建时是可以将依赖的静态库代码合并进来的,具体合并逻辑:

1、如果是直接链接(在 Link Binary With Libraries 中直接添加.a文件):静态框架会将依赖代码合并进来,这是正常的静态链接行为,此时不需要额外操作,主项目依赖TestFramework可以正常编译运行

2、如果是通过 CocoaPods 间接依赖:CocoaPods 创建的只是一个"依赖声明",实际的代码合并被推迟到最终应用构建时,这是 CocoaPods 的设计选择,不是静态框架的限制

解决办法

1、通过CocoaPods 间接依赖的.a可以通过runscript脚本实现合并.a的符号到framework中

FRAMEWORK_BINARY="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework/${PRODUCT_NAME}"
MBHUD_LIB="${BUILT_PRODUCTS_DIR}/MBProgressHUD/libMBProgressHUD.a"
MJEXT_LIB="${BUILT_PRODUCTS_DIR}/MJExtension/libMJExtension.a"
# 使用 libtool 直接合并静态库
libtool -static -o "${FRAMEWORK_BINARY}" "${FRAMEWORK_BINARY}" "${MBHUD_LIB}" "${MJEXT_LIB}"

2、修改TestFrameworklink binary with library,将pod生成产物MBProgressHUD.aMJExtension.a加入进去,可能会提示

File is already being linked. Linking “MBProgressHUD.framework” more than once is not supported. To use the same framework for multiple platforms, use an XCFramework.

选择add anyway即可

.a 文件就是一堆对象文件(.o)打包在一起的“压缩包”;Xcode/ld 链接器在链接时会递归展开所有 .a 文件,把里面的对象文件合并进最终产物;只要你在 Link Binary With Libraries 里加了 .a,它的内容就会被“吸收”进你的 framework 或可执行文件。

B2. TestFramework通过Pod依赖三方库(static framework)
现象

主项目编译不过,经典报错Undefined symbol: _OBJC_CLASS_$_MBProgressHUD

根本原因

静态框架(Static Framework)在构建时是可以将依赖的静态库代码合并进来的, 具体逻辑是通过 CocoaPods 间接依赖的frameworkCocoaPods 创建的只是一个"依赖声明",实际的代码合并被推迟到最终应用构建时,这是 CocoaPods 的设计选择,不是静态框架的限制

解决办法使用脚本进行合并
#!/bin/bash
# 合并静态框架依赖
FRAMEWORK_BINARY="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework/${PRODUCT_NAME}"
MBHUD_FRAMEWORK="${BUILT_PRODUCTS_DIR}/MBProgressHUD/MBProgressHUD.framework/MBProgressHUD"
MJEXT_FRAMEWORK="${BUILT_PRODUCTS_DIR}/MJExtension/MJExtension.framework/MJExtension"
# 使用 libtool 合并静态框架
libtool -static -o "${FRAMEWORK_BINARY}" "${FRAMEWORK_BINARY}" "${MBHUD_FRAMEWORK}" "${MJEXT_FRAMEWORK}"

这里不能使用修改TestFramework的link binary with library的方式,因为static framework 其实是一个目录结构,里面有 Headers、Modules、Info.plist,还有一个二进制文件(通常是静态库格式)。但 Xcode 在链接时不会递归处理 framework 里的依赖,它只会把你当前 target 的源文件和你直接加的 .a 文件合并。framework 只是目录结构的封装,Xcode 只会把 framework 作为一个整体链接,不会像处理 .a 那样递归合并里面的内容。我猜主要是Xcode 设计时主要是为动态库(dynamic framework)服务的,动态库的依赖是运行时解决的; .a 是静态链接的“原生食材”,Xcode 会把它“煮进锅里”;static framework 是“盒饭”,Xcode只会把盒饭放在桌上,不会拆开盒饭再煮一遍。

B3. TestFramework 通过 Pod 依赖三方库(dynamic framework)
现象

编译不报错,运行时报错 dyld[30339]: Library not loaded: @rpath/MBProgressHUD.framework/MBProgressHUD Referenced from: <9FB255F3-CED4-32E5-A9EF-10E473D3BAF6>

根本原因

这种混合情况比较复杂,静态库中依赖了动态库时依赖传递失效,静态框架不会自动带上动态框架依赖;

既失去了静态框架的自包含优势,又增加了动态框架的部署复杂性,让依赖管理变得困难;最好保持一致性:要么全静态,要么全动态。

并且无法使用脚本进行合并,libtool 没有“合并”动态库的功能,动态库只能在最终链接时由主项目动态加载,不能被“吸收”进静态库或 framework

编译能过是因为链接器能找到符号,运行时报错是因为动态库没被正确打包和加载。 只要依赖了动态库,主项目就必须负责把这些动态库带上,并配置好运行时路径。

解决办法

1、通过构建私有库并且在podspec中配置对MBProgressHUD的依赖,来保证主项目可以正常引用动态库MBProgressHUD; 2、直接把MBProgressHUDMBProgressHUD这两个动态库和TestFramework一起提供给主项目,主项目进行embed &sign in后即可

相关代码链接