深入浅出@executable_path @loader_path @rpath

从报错开始

当你在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

可执行文件是 /TestApp.app/TestApp。
找不到的Dylib 是 TestKit。
可执行文件在 @rpath/TestKit.framework/TestKit 中找不到 dylib。

对于 iOS 应用程序,所有第三方框架都驻留在应用程序目录中的 Frameworks 目录中。
所以 dylib 的实际路径是 /TestApp.app/Frameworks/TestKit.framework/TestKit。
锚目录是 /TestApp.app/Framewoks/。
因此,要找出此错误的原因,我们可以检查三件事

  • /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 会为您处理所有这些设置。

你可能感兴趣的:(深入浅出@executable_path @loader_path @rpath)