從報錯開始
當(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è)置。