关于iOS动态库和静态库,网上还没找到一篇能说清楚的文章, About iOS dynamic and static libraries, explained in one click.

Author:站长

一切问题的根源

在制作SDK的过程中, 需要依赖其他第三方库,这一点之前都是通过cocoapods进行的。

但是某个客户要求提供静态库,查看了一下网上的资料发现Xcode build settings 里面有个Mach-O Type 这个之前一直设置的是dynamic library ,即动态库, 只要将它改成 static library就可以了,但是这又出现了一个新问题:

客户在使用SDK的时候编译过程中会出现一大堆报错信息:

Undefined symbols for architecture arm64:
"_OBJC_CLASS_$_SDWebImageManager", referenced from:
      objc-class-ref in NaverCafeSDK(NCSDKManager+Private.o)
  "_OBJC_METACLASS_$_SDImageCache", referenced from:
      _OBJC_METACLASS_$_SDImageCacheForData in NaverCafeSDK(SDImageCacheForData.o)
     (maybe you meant: _OBJC_METACLASS_$_SDImageCacheForData, _OBJC_METACLASS_$_SDImageCacheAdapter )
  "_OBJC_CLASS_$_GADRewardedAd", referenced from:
      objc-class-ref in uluLTV(ULADRewardedAd.o)
  "_OBJC_CLASS_$_ATAPI", referenced from:
      objc-class-ref in uluLTV(ULADRewardedAd.o)

从经验来讲的话这通常都是linker找不到库文件导致的,这是为什么呢?

抛开重复引用的问题,如何将库文件依赖的第三方库打包成一个单一的framework提供给用户呢?

这就需要提到iOS 隐式分享代码(显式直接提供源代码就可以啦,哈哈)的两个方式 :静态库和动态库

静态库和动态库

一般可分享的代码 包含两部分 : 可执行文件和资源文件

这里静态和动态指的是关于”可执行文件“的部分的格式(mach-o type)

下面是静态库和动态库的说明:

静态库 : static library 在生成最终程序的可执行文件时必须拷贝到可执行文件的产物中 其实是一个archive 里面包含了各个架构下的汇编代码,通过file命令可以看到:

****代表文件的路径

*****: Mach-O universal binary with 4 architectures: [arm64:current ar archive] [arm_v7] [i386] [x86_64]
**** (for architecture arm64):    current ar archive
**** (for architecture armv7):    current ar archive
**** (for architecture i386):    current ar archive
**** (for architecture x86_64):    current ar archive

动态库: dynamic library 不拷贝代码,而是在最终可执行文件中加入LC COMMANDS 供dyld在运行时识别并装载到内存中 ,对于除系统库以外的第三方库只能从app的bundle中查找,所以必须拷贝到bundle里,这一步叫做embed ,而发布到App Store上的App中的动态库必须是和主App通过同一个account签名的。

静态链接和动态链接

上面提到了 ,静态库在生成可执行程序过程中是将库文件拷贝到可执行程序段中的,这个就叫做静态链接。 动态链接则是留下供dyld识别的LINKER_OPTION 在运行时(启动时)装载的。

那么当静态或者动态库依赖其他库的时候呢?

首先有个结论就是静态库是无法依赖动态库的 ,因为所有程序必须以静态方式拷贝到可执行文件的程序段。

那么动态库是可以依赖动态库和静态库?

  1. 动态库依赖动态库的情况
    如果一个动态库A依赖另一个动态库B, 那么在生成A的过程中,是通过动态链接的方式进行的,意即不拷贝程序段,而是加入LD_COMMANDS供dyld在运行时装载 ,那么这个动态库A依赖的所有动态库需要以下条件才能在程序中正确调用:
    • 生成库A的过程中不进行链接,而是推迟到生成最终产物的时候链接,这就需要保证连接器ld 的命令行参数中包括可找到B.framework的路径
    • 运行时dyld装载库A的时候也需要动态装载库B,那么就需要保证dyld在程序的bundle中可以找到该动态库,意即这个B.framework也是要拷贝到程序的bundle中,i.e. embed
  2. 动态库依赖静态库的情况
    如果一个动态库A依赖一个静态库B,对于库B来说只能通过静态链接的方式装入程序段,所以在生成A时,所有库A中调用的库B中的代码都需要静态拷贝到库A的程序段 。

在最终App bundle生成时库A是通过动态链接的,而所有库B的代码已经在库A中存在,所以App在链接过程不需要任何库B的信息。

回到问题上来

那么了解了动态库和静态库,动态链接和静态链接的区别,怎么解决提供单个静态库给用户,省却引入其他依赖库的麻烦呢?

在原先使用动态库的情况下,所有依赖项已经被打包到了动态库的程序段中,所以直接使用是没有问题的 linker不会报undefined symbol

在需要提供静态库的情况下:

xcode打包静态库过程中只是将可执行文件打包成mach-o文件,并没有将所有引用的第三方库拷贝入程序段,那么这时候就轮到 ar出场了。

ar的作用是将多个archive文件合并成一个,并剔除重复的符号。

那么最终解决方案就是,通过xcode打包出静态framework之后 将所有依赖的第三方库生成的.a文件 通过ar指令合并成一个最终产物。

libtool -static xxx/xx.framework/xxx **/*.a -o xxx/xx.framework/xxx

其中xxx/xx.framework/xxx 是xcodebuild产生的静态库, **/*.a 这个在Build/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 文件夹中,存在所有依赖项的中间产物*.a文件。

最后将生成的framework和所有SDK用到的资源文件打包成.bundle 和其他第三方库中的.bundle 一并提交给用户,就大功告成啦。

One More Thing

那么还有一个问题 。 cocoapods 的podfile中有一个

use_frameworks! :linkage => :static 选项,
关于”:linkage” 可以参考 静态或动态链接 Firebase 依赖项
在之前SDK开发过程中,这个是设置为 use_frameworks!

那么也就是依赖的pods 默认是以dynamic framework提供的,

通过上面的讨论我们知道动态库依赖动态库的情况下在链接过程中是不拷贝依赖程序的可执行代码的,

那么最终产物在仅依赖一个库A.framework的情况下,linker是如何找到所有依赖库的framework位置的呢? 这个暂时还没有研究明白,需要了解xcodebuild 在打包动态库过程中具体执行了哪些操作。

参考

[1] Static and Dynamic Libraries
[2] Overview of Dynamic Libraries