缘起
最近在做Mac程序的打包,其中涉及到Mac程序引用了Hoops的第三方动态库。在之前的工程配置中,Project的Run Script是这么来处理动态库的:
FRAMEWORKS_DIR=${TARGET_BUILD_DIR}/${EXECUTABLE_NAME}.app/Contents/Frameworks/
mkdir -p ${FRAMEWORKS_DIR}
if [ -f ${TARGET_BUILD_DIR}/libhps_core.dylib -a -f ${TARGET_BUILD_DIR}/libhps_sprk.dylib -a -f ${TARGET_BUILD_DIR}/libhps_sprk_ops.dylib]};
then
LIB_DIR=${TARGET_BUILD_DIR}
else
LIB_DIR=${NL_GIT_EXTERNAL_DIR_MAC}/HoopsExchange/osx/2019/lib
fi
cp ${LIB_DIR}/libhps_core.dylib ${FRAMEWORKS_DIR}
cp ${LIB_DIR}/libhps_sprk.dylib ${FRAMEWORKS_DIR}
cp ${LIB_DIR}/libhps_sprk_ops.dylib ${FRAMEWORKS_DIR}
cp ${LIB_DIR}/libhps_sprk_exchange.dylib ${FRAMEWORKS_DIR}
cp ${LIB_DIR}/libhps_sprk_exchange_parasolid.dylib ${FRAMEWORKS_DIR}
cp ${LIB_DIR}/libA3DLIBS-12.2.20.dylib ${FRAMEWORKS_DIR}
cp ${LIB_DIR}/libA3DLIBS.dylib ${FRAMEWORKS_DIR}
cp ${LIB_DIR}/libA3DLIBSCpp.dylib ${FRAMEWORKS_DIR}
上面的脚本的意思是,在生成Mac程序后,将引用到的libhps动态库copy到程序包的framework目录,以便让hoops动态库和程序一起打包,从而在其他机器上也能够让dyld找到并加载程序所依赖的动态库。
而Apple规定的的Mac程序包所引用的动态库的存放位置应该是(包括动态库和静态库):
@executable_path/…/Frameworks
@executable_path 代表你Mac 程序可执行文件的位置
如果你的App名称为MyExe,则上面的路径则对应为:
MyExe.app/Contents/Frameworks
从这点看,之前工程脚本写的也没问题。但当真正build后,XCode就会报错:CodeSign Empty.
这是因为在XCode引入CodeSign机制后, 会默认对程序包里面的二进制文件检查其CodeSign,这种直接copy进去的动态库,当然是没有经过签名的,所以XCode会报错。
解决思路1
既然是动态库缺失CodeSign的问题,那就想办法解决他。第一种思路是,既然XCode只会检测导入到程序包里面文件的签名,那么我们就不要将libhps这些动态库copy到包里面。解决方法是:
- 把copy脚本删除
- 通过设置Xcode的Build Setting里面的Library Search Paths,指定hoops库的位置,让Xcode能够在程序包外找到所引用的动态库
重新build,运行,发现这时候程序正常启动,问题似乎得到了解决。
但是当我们把程序包copy到其他的机器上,再次运行,会发现dyld会报找不到动态库的错误。
出现这种错误是当然的,因为在Library Search Paths中我们只是指定了本机路径,在其他的机器上如果路径不存在的话,当然就找不到动态库了。
解决这个问题的方法也很简单,就是在其他机器的相同路径下,也放置动态库文件。
解决思路2
在思路1中,虽然可以通过在相同路径下放置动态库文件解决library not found的问题,但是如果App要上架APP store,就不太容易在客户机器上放置动态库文件了。
另一中解决思路仍是将用到的库文件copy到程序包中,但这次我们直接将动态库拖拽到工程中,并选择Embed & Sign:
通过这种方式,Xcode会自动完成Code Sign并将库文件copy到app的framework下。这下,库文件就可以随着程序包走了。
但这个时候试着运行程序,还是会在运行时抛出library not found的错误。这是为什么呢?动态库明明已经embed到我们的APP中,为什么dyld还是会找不到呢?
这里就需要了解一下Mach-O文件对动态库的加载过程。
Mach-O文件的动态库加载
我们知道,在Apple平台上的库文件和执行文件都是Mach-O格式的。在Mach-O文件中,有LOAD COMMAND字段,里面写有很多加载命令,来指导dyld如何将我们的执行文件加载到内存中运行。
其中,我们可以通过查看LOAD COMMAND下的LC_LOAD_DYLIB字段来查看程序需要加载的动态库。具体是可以通过在终端输入otool
命令,来查看程序需要加载的动态库。比如一个App名字叫做WaffleOMatic,他引用了一个动态库WaffleVarnish.framework/WaffleVarnish,就可以用otool命令来查看APP所引用的动态库:
otool -l WaffleOMatic.app/WaffleOMatic | grep -A 2 LC_LOAD_DYLIB
输出是:
cmd LC_LOAD_DYLIB
cmdsize 72
name @rpath/WaffleVarnish.framework/WaffleVarnish …
这里可以看到,我们确实可以看到所引用的动态库WaffleVarnish.framework/WaffleVarnish。但是他的name是@rpath/WaffleVarnish.framework/WaffleVarnish,这个name是怎么来的呢?
这个name被称作install name,它是在开发动态库的时候,在动态库工程的Dynamic Library Install Name选项中设置的。dyld就依据这个install name路径来找到并加载动态库的。在开发动态库的时候,可以将install name设置为一个具体的路径,来确保dyld能够在具体路径下找到这个动态库。而这里的WaffleVarnish的install name则被设置为了@rpath/WaffleVarnish.framework/WaffleVarnish。
这个@rpath
表示什么呢?其实,在开发动态库的时候,我们都应该将install name设置为@rpath开头,而不要使用具体的路径。因为@rpath是可以在引用动态库的工程中进行设置的,这就可以让所有引用你所开发的动态库的工程,都可以灵活的放置你的动态库而不需要局限在某个具体路径中。
比如WaffleOMatic工程引用了WaffleVarnish动态库,就可以在WaffleOMatic工程的Building Setting->Runpath Search Paths 中设置@rpath所替换的具体的路径。注意,这里@rpath可以设置多个值,dlyd就会依次寻找这些路径下是否存在对应的动态库。
回到我在实际工作中遇到的问题,当我把Hoops库通过Embed & Sign拖拽到工程中后,虽然在程序包中可以保证动态库的存在并被签名,但是当查看Hoops库的install name后会发现,他的值也是被设置为了@rpath的形式:
cmd LC_ID_DYLIB
cmdsize 64
name @rpath/libhps_sprk_exchange.dylib
这本意是为了让我们能够灵活地放置Hoops库文件,但是由于之前并不了解这个@rpath,我们并未在工程中设置@rpath的值,所以导致了Hoops动态库无法被找到。解决方法就是在Building Setting->Runpath Search Paths 中设置@rpath的值。这里我这设置为了
@executable_path/../Frameworks
@executable_path
是一个系统变量,表示可执行文件所在的path,@executable_path/../Frameworks
正好对应embed后动态库所在的path。通过设置后,再重新run程序,就可以正常启动了。
总结一下,动态库在Mac程序中的加载过程:
- 在打包程序时,所有引用的到的动态库的
install name
,都会被静态链接到程序的Mach-O文件中(其存储位置在LOAD COMMAND字段的LC_LOAD_DYLIB中)。 - 当程序被加载到内存,dyld会遍历Mach-O文件LC_LOAD_DYLIB字段下的值,根据每条记录的install name值来确定加载动态库的路径。
- 对于@rpath形式的install name,dlyd还会去Mach-O文件的LC_RPATH字段下读取我们在工程中所设置的Runpath Search Paths 的值,用这些具体的值来替换@rpath作为动态库的install name。
- dlyd依次根据install name的值来尝试寻找并加载动态库。
几个有用的命令
通过读取Mach-O相关的加载命令字段,我们可以了解程序对动态库的设置。下面记录些有用的命令:
- 查看程序所有引用的动态库:
otool -l WaffleOMatic.app/WaffleOMatic | grep -A 2 LC_LOAD_DYLIB
- 查看动态库的install name
otool -l WaffleOMatic.app/Frameworks/WaffleVarnish.framework/WaffleVarnish | grep -A 2 LC_ID_DYLIB
- 查看程序所配置的@rpath的值
otool -l WaffleOMatic.app/WaffleOMatic | grep -A 2 LC_RPATH
Symbolic link在动态库中的应用
在使用Hoops库的时候,会注意到在Hoops的lib文件夹下,存在一个软链接libA3DLIBS.dylib,指向libA3DLIBS.24.2.0.dylib这个动态库。
而在Hoops代码内部,也是引用libA3DLIBS.dylib这个动态库,而不是实际的libA3DLIBS.24.2.0.dylib。这是为什么呢?这是一个巧妙的设计,因为libA3DLIBS可能会有不同的版本,如在2019的Hoops库中,这个文件是libA3DLIBS-12.2.20.dylib。所以通过软链接libA3DLIBS.dylib,就可以屏蔽不同版本的动态库名称,在代码内部始终使用libA3DLIBS.dylib这个名称来使用动态库。
但这里就会有个问题, Symbolic link文件是不能够被embed到程序包中的,因此当我们程序在运行时,到了引用libA3DLIBS.dylib的地方,还是会报library not found的问题。这就需要在Build Phases->Run Script中创建一个Symbolic link:
FRAMEWORKS_DIR=${TARGET_BUILD_DIR}/${EXECUTABLE_NAME}.app/Contents/Frameworks/
mkdir -p ${FRAMEWORKS_DIR}
ln -s ${FRAMEWORKS_DIR}/libA3DLIBS.24.2.0.dylib ${FRAMEWORKS_DIR}/libA3DLIBS.dylib
如何修改一个已经存在的dylib的install name
因为dyld会根据动态库的install name来加载动态库,如果我们所引用的三方动态库的install name设置错了该怎么办呢?我们无法修改三方库的工程配置,但所幸的是可以使用命令install_name_tool
来修改install name。
@rpath的妙用
Swift System Libraries
Modern systems have the Swift system libraries built-in. If your app supports older systems, Xcode embeds a copy of these libraries within your app. It uses rpath magic to ensure that your app uses the built-in system libraries if they’re available, falling back to the embedded ones if they’re not.
To demonstrates how this works, change the deployment target for the WaffleOMatic app and the WaffleVarnish framework to iOS 12. Also add some trivial Swift code to the framework. The app now includes a copy of the Swift system libraries:
% ls -l WaffleOMatic.app/Frameworks
total 79032
drwxr-xr-x … WaffleVarnish.framework
-rwxr-xr-x … libswiftCore.dylib
…
Each library has an rpath-relative install name:
% otool -l WaffleOMatic.app/Frameworks/libswiftCore.dylib | grep -A 2 LC_ID_DYLIB
cmd LC_ID_DYLIB
cmdsize 52
name @rpath/libswiftCore.dylib …
The app also has a new LC_RPATH load command for /usr/lib/swift:
% otool -l WaffleOMatic.app/WaffleOMatic | grep -A 2 LC_RPATH
cmd LC_RPATH
cmdsize 32
path /usr/lib/swift …
--
cmd LC_RPATH
cmdsize 40
path @executable_path/Frameworks …
The placement of this load command is critical. By placing it first in the list, the dynamic linker will use the built-in Swift system libraries in preference to the embedded ones.
参考文档
Dynamic Library Identification
Dynamic Library Standard Setup for Apps