Dynamic Library Usage Guidelines

动态加载器的兼容性函数提供了一个轻便的和有效的方法去在运行时加载代码。然而,不正确地使用一个函数会减低app的性能。这篇文章展示了如何正确地加载和在app中使用动态库。
动态库帮助去分散一个app的函数到不同中module中,这能让他们需要时才被加载。动态库能够在它们启动或者运行时才被加载。那些在启动时被加载的称之为dependent libraries。在运行时被加载的称之为dynamically loaded libraries。你通过linking那些动态库定义了你的app依赖的动态库。然而,使用dynamically loaded libraries的动态库比使用depedent libraries更有效。因为,你应该在准备使用symbols时才打开库并且在他们用完之后关闭它们。在一些例子上,这个系统当它决定了哪些库没有被使用时会卸载动态加载库。
这篇文章使用word image去指一个app文件或者动态库。app的二进制文件中包含了app的代码和从静态库中使用的代码。这个app在启动时或运行时加载的动态库是seperate images的。

Opening Dynamic Libraries

动态加载器会在一个image打开的时候加载那个image的独立库;即当一个app被加载或者当一个动态库被打开时。动态加载器懒绑定被dependent库输出的symbol的引用。懒绑定指那些symbol引用只在image实际使用symbols时才被绑定的引用。使用一个调试测试,你能够定义所有被一个库输出的symbol引用都在动态加载器打开库时被绑定。你能在产生动态库时使用-bind_at_load命令选项。

为了在你的images中使用一个不是dependent library的动态库,使用dlopen函数。这个函数告诉动态加载器去加载一个特性的动态库到当前进程的一个地址空间中。这个函数同样允许你去定义动态加载器什么时候去绑定一个库的引用到它的独立库中相关的输出symbol,和是否在当前进程中的全局范围或者本地范围放置库的被输出symbols。这个函数返回一个名为library handle的句柄。这个句柄代表在调用dlsym和dlclose时的dynamically loaded library。这个库的句柄在哪里查找symbol提供了dlsym一个有限的领域。这个客户端在当它完成使用dynamically loaded lirabry必须调用dlclose(如当打开库的module已经完成任务的时候)

一个动态库可能它自己也有一个dependent libraries,为了查出动态库依赖于哪些库,使用otool -L 命令。在使用一个库之前,你必须确保所有的独立库都在你的电脑中。否者的话,动态加载器在启动时或者使用dlopen打开你的库时不会加载你的app或者库。

一个进程能够打开数次打开同一个动态库而不用dlclose等去关闭它,dlopen函数返回和第一次返回相同的library句柄。但是它同样提高与这个句柄相关的引用计数。调用dlclose去减少library句柄的引用计数。因此,你每次调用dlopen之后必须平衡调用dlclose。当一个库句柄的引用计数为零时,动态加载器可能从app的地址空间中移除这个库。

The Library Search Process

dlopen的第一个参数是要打开动态库的名字,这个可能是一个文件名或者是部分或者全部的有效的路径名。例如,libCelsus.dylib,lib/libCelsus.dylib或者是/usr/local/libCelsus.dylib。
动态加载器查找通过一系列的环境变量定义的(如header search path,library search path)和线程当前工作目录的路径中去搜索libraries。这些变量在被定义的时候,必须包含一个被冒号分割开的多个路径,这些路径指明动态加载器在哪里查找这些库,table 1列出了这些变量:

当库的名字是一个文件名(就是没有包含任何目录名),动态加载器会搜索多个位置直到找到它位置(位置有下面的这些):

  1. $LD_LIBRARY_PATH
  2. $DYLD_LIBRARY_PATH
  3. 线程当前工作目录
  4. $DYLD_FALLBACK_LIBRARY_PATH

当库的名字里包含了至少一个目录名时,就是指名字是一个路径名(相对或者绝对路径),动态加载器会根据以下顺序查找这个库:

  1. 使用文件名的 $DYLD_LIBRARY_PATH
  2. 特定的路径
  3. 使用文件名的 $DYLD_FALLBACK_LIBRARY_PATH

例如,你在之前设置的在下面表中的环境变量

加入你的app使用dlopen打开文件名为libCelsus.dylib,动态加载器会尝试使用下面路径打开库

如果app使用路径名/libs/libCelsus.dylib调用dlopen,动态加载器尝试使用这些路径名去查找库

Specifying the Scope and Binding Behavior of Exported Symbol

dlopen第二个参数定义了两个属性:当前线程中库的exported symbols的范围和什么时候绑定app的引用到这些symbol中。
Symbol的范围直接影响了app的性能。因此,你为你的app设置在运行时打开的库的合适的范围是很重要的。
dynamically loaded library的被输出的symbol可以是当前进程两级范围中的一级:全局和本地scope。在这些scope中的主要区别是在全局范围中的symbols在进程中的所有image是可用的,包含其他的一些dynamically loaded libraries。local scope的symbols只能够被用在打开这个library的image中。See Using Symbols

当动态加载器查找symbols时,它会在搜索范围内与每个symbol的字符串进行比较。减少symbol的数量动态加载器需要去查找目标的symbol会提升你app的性能。打开所有的dynamically loaded libraries到local scope而不是全局scope会提升查找symbol的性能。
你永远不需要在进程的全局范围内打开一个动态库才能让所有在app中的modules能使用它的symbols。替代的方式是,每个使用到这个库的module应该在它的locol scope中打开它。在结束时,module应该关闭这个库。如果你想要library中被export的symbol能够在进程所有的images中使用,考虑把库做成app的dependent 库。
使用去定义symbol scope的参数也同样定义了dynamically loaded library中undefined external symbols是什么时候被resolve的(或者是与他们库中的自己独立库的library绑定)。Dynamically loaded library中undefined external symbols能够马上或之后被resolve。如果一个客户端的app使用了在dlopen一个动态库时马上绑定,动态加载器会在客户端控制权返回之前绑定所有的undefined external symbols。如Listing 1 展示了动态加载器的输出信息当DY_PRINT_BINDING环境变量被设置和一个客户端的app加载一个名为libPerson.dylib的dynamic library。
Listing 1 Bindings resolved during call to dlopen using immediate binding

1
2
3
4
5
6
7
8
9
10
11
dyld: lazy bind: client:0x107575050 = libdyld.dylib:_dlopen, *0x107575050 = 0x7FFF88740922
dyld: bind: libPerson.dylib:0x1075A9000 = libdyld.dylib:dyld_stub_binder, *0x1075A9000 = 0x7FFF887406A0
dyld: bind: libPerson.dylib:0x1075A9220 = libobjc.A.dylib:__objc_empty_cache, *0x1075A9220 = 0x7FFF7890EC10
dyld: bind: libPerson.dylib:0x1075A9248 = libobjc.A.dylib:__objc_empty_cache, *0x1075A9248 = 0x7FFF7890EC10
dyld: bind: libPerson.dylib:0x1075A9228 = libobjc.A.dylib:__objc_empty_vtable, *0x1075A9228 = 0x7FFF7890CF60
dyld: bind: libPerson.dylib:0x1075A9250 = libobjc.A.dylib:__objc_empty_vtable, *0x1075A9250 = 0x7FFF7890CF60
dyld: bind: libPerson.dylib:0x1075A9218 = CoreFoundation:_OBJC_CLASS_$_NSObject, *0x1075A9218 = 0x7FFF77C40BA8
dyld: bind: libPerson.dylib:0x1075A9238 = CoreFoundation:_OBJC_METACLASS_$_NSObject, *0x1075A9238 = 0x7FFF77C40B80
dyld: bind: libPerson.dylib:0x1075A9240 = CoreFoundation:_OBJC_METACLASS_$_NSObject, *0x1075A9240 = 0x7FFF77C40B80
dyld: bind: libPerson.dylib:0x1075A9260 = CoreFoundation:___CFConstantStringClassReference, *0x1075A9260 = 0x7FFF77C72760
dyld: bind: libPerson.dylib:0x1075A9280 = CoreFoundation:___CFConstantStringClassReference, *0x1075A9280 = 0x7FFF77C72760

第一个输出信息指那些客户端app的_dlopen的undefined symbol被绑定了。剩余的信息是动态加载器在动态库中展示的绑定信息,作为返回调用路径控制之前加载进程的一部分。当使用懒绑定时,动态加载器只解决客户端到dlopen函数的引用,返回控制到调用路径更加迅速。
一旦一个库使用了dlopen打开,为它定义的范围不能被随后调用dlopen打开相同库的方法改变。例如,如果一个进程打开了一个没有被加载进local scope的库,并且随后在global scope中打开了相同的库,打开的library依然是local状态。即是指之后的调用病没有时库export的symbol变成global scope有效。在相同进程中甚至在这个库在被重新打开之前已经被关闭一样也是这样。

Immediate binding会减慢加载动态库的过程,尤其当这些库包含多个undefined external symbols时。然后,马上绑定有利于开发和测试动态库,因为当动态加载器不能resolve所有dynamically loaded library中undefined external symbol时,app会以错误终结。当你开发一个应用时,你就应该使用懒加载,因为undefined external symbol只有在需要时才被绑定。通过这种方式加载动态库能帮你的app对用户更有响应性。
在dependent libraries中的external undefined symbols在他们第一次使用时被绑定,除非客户端的编译命令包含-bind_at_load选项。See ld man page for detail.