Mac工程动态库配置和加载探究

2024-09-05 05:04

本文主要是介绍Mac工程动态库配置和加载探究,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

缘起

最近在做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
ficp  ${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到包里面。解决方法是:

  1. 把copy脚本删除
  2. 通过设置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 72name @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程序中的加载过程:

  1. 在打包程序时,所有引用的到的动态库的install name,都会被静态链接到程序的Mach-O文件中(其存储位置在LOAD COMMAND字段的LC_LOAD_DYLIB中)。
  2. 当程序被加载到内存,dyld会遍历Mach-O文件LC_LOAD_DYLIB字段下的值,根据每条记录的install name值来确定加载动态库的路径。
  3. 对于@rpath形式的install name,dlyd还会去Mach-O文件的LC_RPATH字段下读取我们在工程中所设置的Runpath Search Paths 的值,用这些具体的值来替换@rpath作为动态库的install name。
  4. 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_DYLIBcmd LC_ID_DYLIBcmdsize 52name @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_RPATHcmd LC_RPATHcmdsize 32path /usr/lib/swift …
--cmd LC_RPATHcmdsize 40path @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

这篇关于Mac工程动态库配置和加载探究的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1138019

相关文章

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

C#如何动态创建Label,及动态label事件

《C#如何动态创建Label,及动态label事件》:本文主要介绍C#如何动态创建Label,及动态label事件,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#如何动态创建Label,及动态label事件第一点:switch中的生成我们的label事件接着,

SpringCloud动态配置注解@RefreshScope与@Component的深度解析

《SpringCloud动态配置注解@RefreshScope与@Component的深度解析》在现代微服务架构中,动态配置管理是一个关键需求,本文将为大家介绍SpringCloud中相关的注解@Re... 目录引言1. @RefreshScope 的作用与原理1.1 什么是 @RefreshScope1.

MyBatis 动态 SQL 优化之标签的实战与技巧(常见用法)

《MyBatis动态SQL优化之标签的实战与技巧(常见用法)》本文通过详细的示例和实际应用场景,介绍了如何有效利用这些标签来优化MyBatis配置,提升开发效率,确保SQL的高效执行和安全性,感... 目录动态SQL详解一、动态SQL的核心概念1.1 什么是动态SQL?1.2 动态SQL的优点1.3 动态S

Spring Boot 配置文件之类型、加载顺序与最佳实践记录

《SpringBoot配置文件之类型、加载顺序与最佳实践记录》SpringBoot的配置文件是灵活且强大的工具,通过合理的配置管理,可以让应用开发和部署更加高效,无论是简单的属性配置,还是复杂... 目录Spring Boot 配置文件详解一、Spring Boot 配置文件类型1.1 applicatio

SpringBoot日志配置SLF4J和Logback的方法实现

《SpringBoot日志配置SLF4J和Logback的方法实现》日志记录是不可或缺的一部分,本文主要介绍了SpringBoot日志配置SLF4J和Logback的方法实现,文中通过示例代码介绍的非... 目录一、前言二、案例一:初识日志三、案例二:使用Lombok输出日志四、案例三:配置Logback一

springboot security之前后端分离配置方式

《springbootsecurity之前后端分离配置方式》:本文主要介绍springbootsecurity之前后端分离配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的... 目录前言自定义配置认证失败自定义处理登录相关接口匿名访问前置文章总结前言spring boot secu

一文详解SpringBoot响应压缩功能的配置与优化

《一文详解SpringBoot响应压缩功能的配置与优化》SpringBoot的响应压缩功能基于智能协商机制,需同时满足很多条件,本文主要为大家详细介绍了SpringBoot响应压缩功能的配置与优化,需... 目录一、核心工作机制1.1 自动协商触发条件1.2 压缩处理流程二、配置方案详解2.1 基础YAML

springboot简单集成Security配置的教程

《springboot简单集成Security配置的教程》:本文主要介绍springboot简单集成Security配置的教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录集成Security安全框架引入依赖编写配置类WebSecurityConfig(自定义资源权限规则

SpringBoot中封装Cors自动配置方式

《SpringBoot中封装Cors自动配置方式》:本文主要介绍SpringBoot中封装Cors自动配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录SpringBoot封装Cors自动配置背景实现步骤1. 创建 GlobalCorsProperties