从报错开始
当你在APP中引入三方动态库时,是不是经常遇到下面这种错误:
dyld: Library not loaded: @rpath/TestKit.framework/TestKit
Referenced from: /TestApp.app/TestApp
Reason: image not found
错误消息中的@rpath 是什么?
@rpath 代表运行路径搜索路径。
要了解它的含义以及我们为什么需要它,我们需要看看动态库(在 macOS 和 iOS 世界中称为 dylib)如何与其他 dylib 和可执行文件链接。但在了解@rpath 之前,我们需要先搞明白@executable_path 和@loader_path 的含义。
什么是@executable_path
这里我们以C语言为例,当然OC和Swift的原理是一样的,用C只是因为在命令行里操作简单一些。
❯ mkdir Demo
❯ cd Demo
❯ vim Cat.c
输入以下内容,保存并退出:
#include
void catSound() {
printf("MEOW!\n");
}
创建main文件:
❯ vim main.c
输入以下代码
void catSound();
int main(int argc, char** argv) {
catSound();
return 0;
}
现在,我们执行编译命令,将Cat.c编译成动态库,main.c编译成mac的可执行文件,一定要注意先后顺序:
❯ clang -dynamiclib Cat.c -o libCat.dylib && clang -L. -lCat main.c -o main
❯ ls
Cat.c libCat.dylib main main.c
现在,我们运行以下main,发现成功调用了libCat.dylib:
❯ ./main
MEOW!
使用otool命令看一下动态库的链接有哪些:
❯ otool -L main
main:
libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
可以看到,main已经链接了libCat.dylib,但这是一个相对路径。
这意味着我们的主要可执行文件希望在执行它的同一目录中找到 libCat.dylib。
所以如果我们尝试从其他目录以相对路径运行 main,我们会得到一个大家非常熟悉错误:
❯ cd ..
❯ ./Demo/main
dyld: Library not loaded: libCat.dylib
Referenced from: /Users/admin/Desktop/Code/inject/./Demo/main
Reason: image not found
[1] 11442 abort ./Demo/main
但是,我们可以使用install_name_tool这个工具,将libCat.dylib的路径改为绝对路径,这样就可以解决上面的问题:
❯ install_name_tool -change libCat.dylib @executable_path/libCat.dylib main
❯ otool -L main
main:
@executable_path/libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
❯ cd ..
❯ ./Demo/main
MEOW!
什么是@loader_path
要理解@loader_path,我们需要让测试用例更加复杂一些。
让我们在不同的目录中创建另一个 dylib,并使这个 dylib 依赖于我们的 Cat dylib。
❯ mkdir Animal
❯ cd Animal
❯ vim Animal.c
输入以下代码:
void catSound();
void animalSound() {
catSound();
}
退到上层目录(因为libCat.dylib在上层目录),然后编译一下吧:
❯ cd ..
❯ clang -dynamiclib -L. -lCat Animal/Animal.c -o Animal/libAnimal.dylib
❯ ls
Animal libCat.dylib
修改一下main.c文件,我们改为调用animalSound,内容如下:
void animalSound();
int main(int argc, char** argv) {
animalSound();
return 0;
}
然后链接一下Animal.dylib:
❯ clang -LAnimal -lAnimal main.c -o main
❯ ./main
MEOW!
像刚才一样,我们知道可以从当前目录执行 main,但不能从其他目录执行。
我们也知道解决这个问题的方法,所以让我们继续这样做:
❯ install_name_tool -change Animal/libAnimal.dylib @executable_path/Animal/libAnimal.dylib main
❯ ./main
MEOW!
看上去没有问题,我们返回上层目录,再尝试一下:
❯ cd ..
❯ ./Demo/main
dyld: Library not loaded: libCat.dylib
Referenced from: /Users/admin/Desktop/Code/inject/Demo/Animal/libAnimal.dylib
Reason: image not found
[1] 12240 abort ./Demo/main
出人意料,刚才还可以的办法,现在居然不管用了。但是仔细观察报错,发现是libAnimal找不到libCat,而不是main找不到libAnimal。
我们看一下libAnimal的动态库依赖:
❯ otool -L Animal/libAnimal.dylib
Animal/libAnimal.dylib:
Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
我们需要在这里将libCat.dylib 的相对路径改为绝对路径。
但是我们应该把它改成什么?使用 @executable_path 会起作用 - 但仅适用于我们的主要可执行文件。
Dylib 旨在在多个客户端之间共享,所有客户端都可以位于不同的路径中。
这意味着 @executable_path 将根据正在运行的可执行文件解析为不同的值,这一点从命名中不难看出,executable path直译就是可执行路径。
让我们再分析一下,看看我们拥有的依赖树。
main 依赖于 libAnimal,而 libAnimal 依赖于 libCat。 libCat 不依赖任何东西。
libCat.dylib <--- Animal/libAnimal.dylib <--- main
不管是main加载libAnimal,还是libAnimal加载libCat,@executable_path 将始终解析为 main 的路径。
因此dyld 提供了另一个变量 - @loader_path - 解析为客户端执行加载的路径。
我们看一下当前文件目录的结构,这有助于我们分析动态库的加载路径:
.
├── Demo
├── Animal
│ ├── Animal.c
│ └── libAnimal.dylib
├── Cat.c
├── libCat.dylib
├── main
└── main.c
我们列个表格,看一下:
依赖关系 | @executable_path | @loader_path |
---|---|---|
main -> libAnimal | ./Demo/ | ./Demo/ |
libAnimal -> libCat | ./Demo/ | ./Demo/Animal/ |
搞清楚了这个路径关系,那问题就很好解决了,还是用install_name_tool,但是这次是修改libAnimal的依赖:
❯ install_name_tool -change libCat.dylib @loader_path/../libCat.dylib Animal/libAnimal.dylib
❯ otool -L Animal/libAnimal.dylib
Animal/libAnimal.dylib:
Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
@loader_path/../libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
让我们试一下吧:
❯ ./main
MEOW!
❯ cd ..
❯ ./Demo/main
MEOW!
可以看到,现在从任何目录运行 main 都会成功。
事实上,如果你要添加一个新的依赖于 libAnimal 的可执行文件 foo/main,你只需要在 foo/main 本身中设置 libAnimal.dylib 的安装路径。
libCat和libAnimal现在是真正意义上的“共享库”,只要 libCat 或 libAnimal 之间的相对路径保持不变,就不需要更改它们,但这是不可避免的。
有一点需要注意,对于可执行文件,@loader_path 和@executable_path 的意思是一样的。
安装ID
在介绍@rpath之前,我们先认识一个叫install id的概念,用otool查看两个dylib的动态库链接信息:
❯ otool -L Animal/libAnimal.dylib
Animal/libAnimal.dylib:
Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
@loader_path/../libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
❯ otool -L libCat.dylib
libCat.dylib:
libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
对于 dylib来说,第一个条目不是安装路径,而是安装 ID。
当另一个客户端链接到这个 dylib 时,dylib 的安装 ID 会被复制到客户端中作为dylib的安装路径。
对于安装ID,依然可以使用install_name_tool来进行修改。
❯ install_name_tool -id @rpath/xxx/xxx.dylib xxx/xxx.dylib
终于到了@rpath
在一个大型项目中,在不同位置的多个客户端相互依赖,管理 @loader_path 是一件是非复杂且麻烦的事情。
在这种情况下,我们可以使用@rpath。与上面介绍的两个变量不同,@rpath 对 dyld 没有任何特殊意义。由我们为每个客户端的 @rpath 定义一个(或多个)值。@rpath 的出现极大的降低了管理动态库加载路径的复杂度。
让我们修改测试用例以使用@rpath。
添加另一个与 main.c 代码相同的可执行文件 foo/main.c(直接用cp命令也可以),先不着急编译,我们的目录结构如下所示:
.
├── Demo
├── Animal
│ ├── Animal.c
│ └── libAnimal.dylib
├── Cat.c
├── foo
│ └── main.c
├── libCat.dylib
├── main
└── main.c
第一步,在我们的目录结构中选择一个路径作为锚路径。
让我们选择./Demo/ 作为我们的锚点。
接下来,让我们将 dylib 安装 ID 更改为 @rpath/xxx,其中xxx是从锚点到 dylib 的相对路径。
对于 libCat.dylib,路径为 @rpath/libCat.dylib:
❯ install_name_tool -id @rpath/libCat.dylib libCat.dylib
❯ otool -L libCat.dylib
libCat.dylib:
@rpath/libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
对于Animal/libAnimal.dylib,路径为@rpath/Animal/libAnimal.dylib
❯ install_name_tool -id @rpath/Animal/libAnimal.dylib Animal/libAnimal.dylib
❯ otool -L Animal/libAnimal.dylib
Animal/libAnimal.dylib:
@rpath/Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
@loader_path/../libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
接下来,让我们向可执行文件添加一个 @rpath,其值等于 @loader_path/xxx,其中 xxx 是从可执行文件到锚点的相对路径。
对于 foo/main.c ,这个路径为 @loader_path/../
❯ clang -LAnimal -lAnimal -rpath "@loader_path/../" foo/main.c -o foo/main
❯ otool -L foo/main
foo/main:
@rpath/Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
❯ ./foo/main
MEOW!
而对于 main.c,这个路径是 @loader_path。
我们可以使用 install_name_tool 将@rpath 添加到编译后的可执行文件中,但是由于在编译 main.c 之后 libAnimal 和 libCat 的安装 ID 发生了变化,最好重新编译并重新链接它,以便它获取更新的 ID :
❯ clang -LAnimal -lAnimal -rpath "@loader_path" main.c -o main
❯ otool -L main
main:
@rpath/Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
❯ ./main
MEOW!
完成这些步骤之后,我们可以将Demo目录移动到任何地方,甚至是不同的mac电脑上,这两个main可执行文件将继续运行 - 只要 Demo中的目录结构本身不改变。
请注意,您可以在链接时或稍后使用 install_name_tool 为可执行文件定义多个 @rpath 值。
dyld 将尝试所有值以检查是否存在 dylib。
回到最初的报错
了解所有这些后,我们现在可以更好地了解初始错误消息的含义以及如何修复它。
让我们分析一下错误——
dyld: Library not loaded: @rpath/TestKit.framework/TestKit
Referenced from: /TestApp.app/TestApp
Reason: image not found
可执行文件是
找不到的Dylib 是 TestKit。
可执行文件在 @rpath/TestKit.framework/TestKit 中找不到 dylib。
对于 iOS 应用程序,所有第三方框架都驻留在应用程序目录中的 Frameworks 目录中。
所以 dylib 的实际路径是
锚目录是
因此,要找出此错误的原因,我们可以检查三件事
/TestApp.app/TestApp 的@rpath 值为@loader_path/Frameworks/。 @executable_path/Frameworks/ 也可以工作,因为两者对可执行文件的意义相同。如果您有源代码,则可以在目标的构建设置 (LD_RUNPATH_SEARCH_PATHS) 中进行检查。如果没有, 试一下otool -l。 - TestKit dylib 的安装 ID 为 @rpath/TestKitFramework.framework/TestKit。如果您有源代码,请在构建设置 (LD_DYLIB_INSTALL_NAME) 中进行检查。如果没有, 再试一下otool -l。
- TestKit.framework 实际上存在于 Frameworks 目录中。为此,您需要将framework嵌入到应用程序中。
当您向应用程序添加新framework时,Xcode 会为您处理所有这些设置。