macOS 上 bundle (.so) 和 dylib 的区别

2019年11月15日 · 4 years ago

今天和同事讨论到一个问题:

bundle 和动态库一样吗?

同事说 bundle 只是包含了其他资源而已,其实就是动态库。

我看 Mach-O 文件类型里 MH_BUNDLEMH_DYLIB 是分开的,所以觉得 .bundle 里面的 Mach-O 文件和 dylib 的 Mach-O 文件应该会有些不一样。不过我也不知道有什么不一样,所以学习了一下,以此文记之。

定义一下动态库为 dylib Mach-O 文件, bundle 指的是 .bundle 文件夹里面的 Mach-O 文件,一般类 Unix 系统叫做 .so 库,不过苹果官方建议叫做 .bundle

P.S. 这里苹果官方不厚道,它推荐用 .bundle 作为 MH_BUNDLE 类型文件的后缀名但不强制,然后自己还把 .bundle 后缀名用作一个类似 .app 的资源与可执行文件打包。所以很容易就会混淆两个概念。实际上我看到的 MH_BUNDLE 类型的 Mach-O 基本上都没有后缀名,有 .bundle 后缀名的基本上都是资源与可执行文件的打包。

先说结论: 通常语境下 bundledylib 没有区别。要较真的话也只有在 OS X 10.5 以前才有比较大的区别,所以同事说 bundle 和动态库没有区别是对的。

P.S. ELF 系统(Executable and Linking Format,Unix-like 系统基本都是)上这两者完全相等,只有 Mac 的 dyld 对他们做了点区别对待。

一、File Type 不同

Mach-O 文件的 header 里有一个 type 字段表示当前文件的类型,如果把 .bundle 文件夹解开,里面的 Mach-O 文件的类型是 MH_BUNDLE,而 dylib 则是 MH_DYLIB

➜ otool -hv AppKit
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64  X86_64        ALL  0x00       DYLIB    60       8344   NOUNDEFS DYLDLINK TWOLEVEL APP_EXTENSION_SAFE

➜ AppKit.framework otool -hv /System/Library/Audio/Plug-Ins/HAL/AirPlay.driver/Contents/MacOS/AirPlay
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64  X86_64        ALL  0x00      BUNDLE    21       2544   NOUNDEFS DYLDLINK TWOLEVEL

二、加载、链接时机不同

在 macOS 上,动态加载通过 dyld 进行。bundledylib 两种文件都可以使用 dlopen 加载。两者的区别要在 dyld源码里面找。

dylddlopen() 实现主要关注是这几个地方:

  • dlopen()
  • load()
  • loadPhase0()
  • loadPhase1()
  • loadPhase2()
  • loadPhase3()
  • loadPhase4()
  • loadPhase5()
  • loadPhase6()
  • checkandAddImage()
    • 如果是 dylib 就从 sAllImages 找到一样路径的 image 先删掉
    • dylibbundle 能使用的 API 不一样,所以这里还得判断 context.mustBeBundleisBundle()是否匹配
    // some API's restrict what they can load
    if ( context.mustBeBundle && !image->isBundle() )
        throw "not a bundle";
    if ( context.mustBeDylib && !image->isDylib() )
        throw "not a dylib";
    
    • 如果是 bundle 就不会加到 global list,因为 bundle 可以只加载但不链接。

所以结论是 bundle 可以只加载不链接,而 dylib 加载后就链接了。

三、NSObjectFileImage 只有 bundle 能用

dyld 提供了 NSObjectFileImage 接口,这些接口只有 bundle 能用,只加载不链接就通过这个接口来实现。

NSObjectFileImageReturnCode NSCreateObjectFileImageFromFile(const char* pathName, NSObjectFileImage *objectFileImage)

里面会调用 load() 方法加载 bundle,这类接口的 context.mustBeBundletrue,底下判断的时候遇到非 bundle 就会报错。

load() 之后再使用以下方法链接:

NSModule NSLinkModule(NSObjectFileImage objectFileImage, const char* moduleName, uint32_t options)

四、结论

NSObjectFileImage 相关的接口从 OS X 10.5 开始已经被废弃了。

在 Mac OS X 10.5 (2007 年) 以前,bundle 可以被 unload 但是 dylib 不可以,10.5 开始 dylib 也可以被 unload 了。dlclose() 的实现很简单,调用时减一下引用计数,为 0 就从走垃圾回收接口 garbageCollectImages() 删掉。

经过以上调查,现如今的 bundledylib 在使用上几乎可以完全对等。要说区别那就只有编译 dylib 为 shared library 的时候需要加上版本号,而 bundle 只会给自己的 App 用就没有必要了。

libbz2.1.0.5.tbd
libbz2.1.0.tbd
libbz2.tbd

至于 Mach-O Header file type 的区别,只是给 dyldNSObjectFileImage 接口判断而已,这些接口废弃了那自然就没有区别了。

参考资料