深入淺出@executable_path @loader_path @rpath

從報錯開始

當(dāng)你在APP中引入三方動態(tài)庫時,是不是經(jīng)常遇到下面這種錯誤:

dyld: Library not loaded: @rpath/TestKit.framework/TestKit
 Referenced from: <long_path_name>/TestApp.app/TestApp
 Reason: image not found

錯誤消息中的@rpath 是什么?

@rpath 代表運行路徑搜索路徑。
要了解它的含義以及我們?yōu)槭裁葱枰?,我們需要看看動態(tài)庫(在 macOS 和 iOS 世界中稱為 dylib)如何與其他 dylib 和可執(zhí)行文件鏈接。但在了解@rpath 之前,我們需要先搞明白@executable_path 和@loader_path 的含義。

什么是@executable_path

這里我們以C語言為例,當(dāng)然OC和Swift的原理是一樣的,用C只是因為在命令行里操作簡單一些。

? mkdir Demo
? cd Demo
? vim Cat.c

輸入以下內(nèi)容,保存并退出:

#include <stdio.h>

void catSound() {
  printf("MEOW!\n");
}

創(chuàng)建main文件:

? vim main.c

輸入以下代碼

void catSound();

int main(int argc, char** argv) {
  catSound();
  return 0;
}

現(xiàn)在,我們執(zhí)行編譯命令,將Cat.c編譯成動態(tài)庫,main.c編譯成mac的可執(zhí)行文件,一定要注意先后順序:

? clang -dynamiclib Cat.c -o libCat.dylib && clang -L. -lCat main.c -o main
? ls
Cat.c        libCat.dylib    main         main.c

現(xiàn)在,我們運行以下main,發(fā)現(xiàn)成功調(diào)用了libCat.dylib:

? ./main                                                                                                                                                                                                                                                         
MEOW!

使用otool命令看一下動態(tài)庫的鏈接有哪些:

? 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已經(jīng)鏈接了libCat.dylib,但這是一個相對路徑。
這意味著我們的主要可執(zhí)行文件希望在執(zhí)行它的同一目錄中找到 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,我們需要讓測試用例更加復(fù)雜一些。
讓我們在不同的目錄中創(chuàng)建另一個 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文件,我們改為調(diào)用animalSound,內(nèi)容如下:

void animalSound();

int main(int argc, char** argv) {
  animalSound();
  return 0;
}

然后鏈接一下Animal.dylib:

? clang -LAnimal -lAnimal main.c -o main
? ./main                                                                                                                                                                                                                                                        
MEOW!

像剛才一樣,我們知道可以從當(dāng)前目錄執(zhí)行 main,但不能從其他目錄執(zhí)行。
我們也知道解決這個問題的方法,所以讓我們繼續(xù)這樣做:

? 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

出人意料,剛才還可以的辦法,現(xiàn)在居然不管用了。但是仔細(xì)觀察報錯,發(fā)現(xiàn)是libAnimal找不到libCat,而不是main找不到libAnimal。
我們看一下libAnimal的動態(tài)庫依賴:

? 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 的相對路徑改為絕對路徑。
但是我們應(yīng)該把它改成什么?使用 @executable_path 會起作用 - 但僅適用于我們的主要可執(zhí)行文件。
Dylib 旨在在多個客戶端之間共享,所有客戶端都可以位于不同的路徑中。
這意味著 @executable_path 將根據(jù)正在運行的可執(zhí)行文件解析為不同的值,這一點從命名中不難看出,executable path直譯就是可執(zhí)行路徑。

讓我們再分析一下,看看我們擁有的依賴樹。
main 依賴于 libAnimal,而 libAnimal 依賴于 libCat。 libCat 不依賴任何東西。

libCat.dylib <--- Animal/libAnimal.dylib <--- main

不管是main加載libAnimal,還是libAnimal加載libCat,@executable_path 將始終解析為 main 的路徑。
因此dyld 提供了另一個變量 - @loader_path - 解析為客戶端執(zhí)行加載的路徑。
我們看一下當(dāng)前文件目錄的結(jié)構(gòu),這有助于我們分析動態(tài)庫的加載路徑:

.
├── Demo
   ├── Animal
   │   ├── Animal.c
   │   └── libAnimal.dylib
   ├── Cat.c
   ├── libCat.dylib
   ├── main
   └── main.c

我們列個表格,看一下:

依賴關(guān)系 @executable_path @loader_path
main -> libAnimal ./Demo/ ./Demo/
libAnimal -> libCat ./Demo/ ./Demo/Animal/

搞清楚了這個路徑關(guān)系,那問題就很好解決了,還是用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!

可以看到,現(xiàn)在從任何目錄運行 main 都會成功。

事實上,如果你要添加一個新的依賴于 libAnimal 的可執(zhí)行文件 foo/main,你只需要在 foo/main 本身中設(shè)置 libAnimal.dylib 的安裝路徑。

libCat和libAnimal現(xiàn)在是真正意義上的“共享庫”,只要 libCat 或 libAnimal 之間的相對路徑保持不變,就不需要更改它們,但這是不可避免的。

有一點需要注意,對于可執(zhí)行文件,@loader_path 和@executable_path 的意思是一樣的。

安裝ID

在介紹@rpath之前,我們先認(rèn)識一個叫install id的概念,用otool查看兩個dylib的動態(tài)庫鏈接信息:

 ? 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。
當(dāng)另一個客戶端鏈接到這個 dylib 時,dylib 的安裝 ID 會被復(fù)制到客戶端中作為dylib的安裝路徑。
對于安裝ID,依然可以使用install_name_tool來進(jìn)行修改。

? install_name_tool -id @rpath/xxx/xxx.dylib xxx/xxx.dylib

終于到了@rpath

在一個大型項目中,在不同位置的多個客戶端相互依賴,管理 @loader_path 是一件是非復(fù)雜且麻煩的事情。
在這種情況下,我們可以使用@rpath。與上面介紹的兩個變量不同,@rpath 對 dyld 沒有任何特殊意義。由我們?yōu)槊總€客戶端的 @rpath 定義一個(或多個)值。@rpath 的出現(xiàn)極大的降低了管理動態(tài)庫加載路徑的復(fù)雜度。

讓我們修改測試用例以使用@rpath。
添加另一個與 main.c 代碼相同的可執(zhí)行文件 foo/main.c(直接用cp命令也可以),先不著急編譯,我們的目錄結(jié)構(gòu)如下所示:

.
├── Demo
   ├── Animal
   │   ├── Animal.c
   │   └── libAnimal.dylib
   ├── Cat.c
   ├── foo
   │   └── main.c
   ├── libCat.dylib
   ├── main
   └── main.c

第一步,在我們的目錄結(jié)構(gòu)中選擇一個路徑作為錨路徑。
讓我們選擇./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)

接下來,讓我們向可執(zhí)行文件添加一個 @rpath,其值等于 @loader_path/xxx,其中 xxx 是從可執(zhí)行文件到錨點的相對路徑。

對于 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 添加到編譯后的可執(zhí)行文件中,但是由于在編譯 main.c 之后 libAnimal 和 libCat 的安裝 ID 發(fā)生了變化,最好重新編譯并重新鏈接它,以便它獲取更新的 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可執(zhí)行文件將繼續(xù)運行 - 只要 Demo中的目錄結(jié)構(gòu)本身不改變。

請注意,您可以在鏈接時或稍后使用 install_name_tool 為可執(zhí)行文件定義多個 @rpath 值。
dyld 將嘗試所有值以檢查是否存在 dylib。

回到最初的報錯

了解所有這些后,我們現(xiàn)在可以更好地了解初始錯誤消息的含義以及如何修復(fù)它。
讓我們分析一下錯誤——

dyld: Library not loaded: @rpath/TestKit.framework/TestKit
 Referenced from: <long_path_name>/TestApp.app/TestApp
 Reason: image not found

可執(zhí)行文件是 <long_path_name>/TestApp.app/TestApp。
找不到的Dylib 是 TestKit。
可執(zhí)行文件在 @rpath/TestKit.framework/TestKit 中找不到 dylib。

對于 iOS 應(yīng)用程序,所有第三方框架都駐留在應(yīng)用程序目錄中的 Frameworks 目錄中。
所以 dylib 的實際路徑是 <long_path_name>/TestApp.app/Frameworks/TestKit.framework/TestKit。
錨目錄是 <long_path_name>/TestApp.app/Framewoks/。
因此,要找出此錯誤的原因,我們可以檢查三件事

  • <long_path_name>/TestApp.app/TestApp 的@rpath 值為@loader_path/Frameworks/。 @executable_path/Frameworks/ 也可以工作,因為兩者對可執(zhí)行文件的意義相同。如果您有源代碼,則可以在目標(biāo)的構(gòu)建設(shè)置 (LD_RUNPATH_SEARCH_PATHS) 中進(jìn)行檢查。如果沒有, 試一下otool -l。
  • TestKit dylib 的安裝 ID 為 @rpath/TestKitFramework.framework/TestKit。如果您有源代碼,請在構(gòu)建設(shè)置 (LD_DYLIB_INSTALL_NAME) 中進(jìn)行檢查。如果沒有, 再試一下otool -l。
  • TestKit.framework 實際上存在于 Frameworks 目錄中。為此,您需要將framework嵌入到應(yīng)用程序中。

當(dāng)您向應(yīng)用程序添加新framework時,Xcode 會為您處理所有這些設(shè)置。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容