什么是动态库?
与静态库相反,动态库在编译时并不会被拷⻉到⽬标程序中,⽬标程序中只会存储指向动态库的引⽤。等到程序运⾏时,动态库才会被真正加载进来。格式有:
.framework
、.dylib
、.tbd
缺点:会导致⼀些性能损失。但是可以优化,⽐如延迟绑定(
Lazy Binding
)技术
链接动态库
生成目标文件
目录中包含一个
test.m
文件和AFNetworking
三方库打开
test.m
文件,写入以下代码:#import <Foundation/Foundation.h> #import <AFNetworking.h> int main(){ AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; NSLog(@"testApp----%@", manager); return 0; }
AFNetworking
为动态库,打开AFNetworking
目录,里面包含了头文件和dylib
文件
使用
clang
命令,将.m
文件编译成.o
文件clang -x objective-c \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -I ./AFNetworking \ -c test.m -o test.o
此时目录中生成了
.o
目标文件
生成可执行文件
使用
clang
命令,将.o
文件链接成可执行文件clang -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -L ./AFNetworking \ -lAFNetworking \ test.o -o test
此时目录中生成了
test
可执行文件
- 库文件名称的查找规则:先找
lib+<library_name>
的动态库,找不到,再去找lib+<library_name>
的静态库,还找不到,就报错
运行可执行文件
- 使用
lldb
命令,在终端中进入lldb
环境- 使用
file test
命令,将test
可执行文件包装成一个target
- 使用
r
命令,开始运行dyld: Library not loaded: >@rpath/AFNetworking.framework/Versions/A/AFNetworking Referenced from: /Users/zang/Zang/Spark/Test2/test Reason: image not found
- 运行失败,提示错误信息:
image not found
在日常开发中,当我们的项目使用了动态库,在运行时经常见到
image not found
错误。它产生的原因是什么?又该如何解决?在文章后面将详细说明...
链接动态库的原理
搭建项目
项目中包含
test.m
文件和一个dylib
子目录,dylib
目录下包含TestExample.h
文件和TestExample.m
文件
打开
TestExample.h
文件,写入以下代码:#import <Foundation/Foundation.h> @interface TestExample : NSObject - (void)lg_test:(_Nullable id)e; @end
打开
TestExample.m
文件,写入以下代码:#import "TestExample.h" @implementation TestExample - (void)lg_test:(_Nullable id)e { NSLog(@"TestExample----"); } @end
打开
test.m
文件,写入以下代码:#import <Foundation/Foundation.h> #import "TestExample.h" int main(){ NSLog(@"testApp----"); TestExample *manager = [TestExample new]; [manager lg_test: nil]; return 0; }
生成可执行文件
创建
build.sh
文件,和test.m
文件平级
打开build.sh
文件,按以下步骤写入代码:【步骤一】:使用
clang
命令,将test.m
文件编译成.o
文件echo "-------------编译test.m to test.o------------------" clang -x objective-c \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -I./dylib \ -c test.m -o test.o
【步骤二】:进入
dylib
目录echo "-------------进入到dylib目录------------------" pushd ./dylib
【步骤三】:使用
clang
命令,将TestExample.m
文件编译成.o
文件echo "-------------编译TestExample.m to TestExample.o------------------" clang -x objective-c \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -c TestExample.m -o TestExample.o
【步骤四】:使用
clang
命令,生成libTestExample.dylib
文件echo "-------------TestExample.o to libTestExample.dylib------------------" clang -dynamiclib \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ TestExample.o -o libTestExample.dylib
-dynamiclib
:表示编译生成一个动态库【步骤五】:退出
dylib
目录echo "-------------退出dylib目录------------------" popd
【步骤六】:使用
clang
命令,将test.o
文件链接成可执行文件echo "-------------将test.o链接成可执行文件------------------" clang -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -L./dylib \ -lTestExample \ test.o -o test
- 此时
build.sh
的脚本代码全部完成打开终端,使用
chmod
命令,为build.sh
文件增加可执行权限chmod +x ./build.sh
使用
./build.sh
命令,执行Shell
脚本-------------编译test.m to test.o------------------ -------------进入到dylib目录------------------ ~/Zang/Spark/Test/dylib ~/Zang/Spark/Test -------------编译TestExample.m to TestExample.o------------------ -------------TestExample.o to libTestExample.dylib------------------ -------------退出dylib目录------------------ ~/Zang/Spark/Test -------------将test.o链接成可执行文件------------------
执行成功,目录下自动生成
test
可执行文件
运行可执行文件
- 使用
lldb
命令,在终端中进入lldb
环境- 使用
file test
命令,将test
可执行文件包装成一个target
- 使用
r
命令,开始运行dyld: Library not loaded: libTestExample.dylib Referenced from: /Users/zang/Zang/Spark/Test/test Reason: image not found
运行失败,提示错误信息:
image not found
将静态库链接为动态库
动态库是编译链接的最终产物,而静态库是.o
文件的合集,所有静态库可以链接成为一个动态库
沿用上述案例,打开
build.sh
文件,将步骤四
改为以下代码:echo "-------------TestExample.o to libTestExample.a------------------" libtool -static -arch_only x86_64 TestExample.o -o libTestExample.a echo "-------------TestExample.a to libTestExample.dylib------------------" ld -dylib -arch x86_64 \ -macosx_version_min 11.1 \ -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -lsystem -framework Foundation \ libTestExample.a -o libTestExample.dylib
libtool
:使用Xcode
提供的libtool
命令,将.o
文件生成.a
文件ld
:链接器命令,将.a
文件链接变为一个动态库
-dylib
:指定链接成一个动态库
-arch
:指定架构
-macosx_version_min
:支持最小的macOS
版本
-syslibroot
:使用SDK
的路径
-lsystem
:链接指定库,这里指定了两个必须使用的系统库,system
和Foundation
使用
./build.sh
命令,执行Shell
脚本-------------将test.o链接成可执行文件------------------ Undefined symbols for architecture x86_64: "_OBJC_CLASS_$_TestExample", referenced from: objc-class-ref in test.o ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation)
- 执行到最后一步,生成可执行文件时,提示错误信息:未定义的符号
_OBJC_CLASS_$_TestExample
错误原因:在
test.m
中,使用了动态库中的TestExample
类的lg_test
方法,此时动态库应该提供导出符号,以供外部使用。但是在.a
文件链接成为动态库时,由于-noall_load
为默认值,故此将符合剥离条件的代码全部剥离
解决此问题,可以在
.a
文件链接为动态库时,指定-all_load
参数echo "-------------TestExample.a to libTestExample.dylib------------------" ld -dylib -arch x86_64 \ -macosx_version_min 11.1 \ -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -lsystem -framework Foundation \ -all_load \ libTestExample.a -o libTestExample.dylib
使用
./build.sh
命令,再次执行Shell
脚本-------------编译test.m to test.o------------------ -------------进入到dylib目录------------------ ~/Zang/Spark/Test1/dylib ~/Zang/Spark/Test1 -------------编译TestExample.m to TestExample.o------------------ -------------TestExample.o to libTestExample.a------------------ -------------TestExample.a to libTestExample.dylib------------------ -------------退出dylib目录------------------ ~/Zang/Spark/Test1 -------------将test.o链接成可执行文件------------------
执行成功,目录下自动生成
test
可执行文件
运行
test
可执行文件
- 使用
lldb
命令,在终端中进入lldb
环境- 使用
file test
命令,将test
可执行文件包装成一个target
- 使用
r
命令,开始运行dyld: Library not loaded: libTestExample.dylib Referenced from: /Users/zang/Zang/Spark/Test1/test Reason: image not found
运行失败,提示错误信息:
image not found
手动创建Framework
链接动态库
- 在项目根目录下,创建
Frameworks
目录- 在
Frameworks
目录下,创建TestExample.framework
目录- 在
TestExample.framework
目录下,创建Headers
目录在
Headers
目录下,创建TestExample.h
文件,写入以下代码:#import <Foundation/Foundation.h> @interface TestExample : NSObject - (void)lg_test:(_Nullable id)e; @end
在
TestExample.framework
目录下,创建TestExample.m
文件,写入以下代码:#import "TestExample.h" @implementation TestExample - (void)lg_test:(_Nullable id)e { NSLog(@"TestExample----"); } @end
在
TestExample.framework
目录下,创建build.sh
文件,写入以下代码:echo "-------------编译TestExample.m to TestExample.o------------------" clang -x objective-c \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -I./Headers \ -c TestExample.m -o TestExample.o echo "-------------TestExample.o to TestExample------------------" clang -dynamiclib \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ TestExample.o -o TestExample
使用
./build.sh
命令,执行Shell
脚本-------------编译TestExample.m to TestExample.o------------------ -------------TestExample.o to TestExample------------------
执行成功,目录下自动生成
TestExample
动态库
生成可执行文件
来到项目根目录,创建
test.m
文件,和Frameworks
目录平级,写入以下代码:#import <Foundation/Foundation.h> #import "TestExample.h" int main(){ NSLog(@"testApp----"); TestExample *manager = [TestExample new]; [manager lg_test: nil]; return 0; }
创建
build.sh
文件,和test.m
文件平级,写入以下代码:echo "-------------编译test.m to test.o------------------" clang -x objective-c \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -I./Frameworks/TestExample.framework/Headers \ -c test.m -o test.o echo "-------------将test.o链接成可执行文件------------------" clang -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -F./Frameworks \ -framework TestExample \ test.o -o test
使用
./build.sh
命令,执行Shell
脚本-------------编译test.m to test.o------------------ -------------将test.o链接成可执行文件------------------
执行成功,目录下自动生成
test
可执行文件
运行可执行文件
- 使用
lldb
命令,在终端中进入lldb
环境- 使用
file test
命令,将test
可执行文件包装成一个target
- 使用
r
命令,开始运行dyld: Library not loaded: TestExample Referenced from: /Users/zang/Zang/Spark/Test3/test Reason: image not found
- 运行失败,提示错误信息:
image not found
image not found
上述几个案例中,当最后运行可执行文件时,都会提示image not found
错误
产生问题的原因
分析问题是如何产生的,就要从
dyld
加载一个动态库开始说起:
- 当
dyld
加载一个Mach-O
时,例如上述案例中的test
可执行文件。在Mach-O
中会有一个名称为LC_LOAD_DYLIB
的Load Command
,它里面存储了动态库的路径- 动态库是运行时加载的,它的加载方式就是
dyld
通过路径找到对应的动态库- 如果路径有误,导致运行时
dyld
无法找到动态库,就会提示image not found
错误
找到上述案例中的
test
可执行文件使用
otool -l test | grep 'DYLIB' -A 2
命令,查看Mach-O
中动态库的路径cmd LC_LOAD_DYLIB cmdsize 40 name TestExample (offset 24) -- cmd LC_LOAD_DYLIB cmdsize 96 name /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (offset 24) -- cmd LC_LOAD_DYLIB cmdsize 56 name /usr/lib/libobjc.A.dylib (offset 24) -- cmd LC_LOAD_DYLIB cmdsize 56 name /usr/lib/libSystem.B.dylib (offset 24) -- cmd LC_LOAD_DYLIB cmdsize 104 name /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (offset 24)
Mach-O
中,一共使用了五个动态库- 例如系统提供的
libobjc.A.dylib
动态库,它的路径是/usr/lib/libobjc.A.dylib
,按照此路径可以找到动态库,因此加载正常- 自定义的
TestExample
动态库,它的路径只有一个TestExample
名称,相当于和Mach-O
平级,在运行时无法找到动态库,因此提示image not found
错误
解决问题的办法
问题本质:
Mach-O
中LC_LOAD_DYLIB
存储的动态库路径不正确解决办法:当链接成为一个动态库时,要在动态库中指定正确的所在路径
在动态库中,有一个名称为
LC_ID_DYLIB
的Load Command
,里面存储了自身所在路径使用
otool -l TestExample | grep 'ID_DYLIB' -A 2
,查看TestExample
动态库中存储的所在路径cmd LC_ID_DYLIB cmdsize 40 name TestExample (offset 24)
- 在动态库中存储的自身所在路径,只有一个
TestExample
名称。说明在链接成为一个动态库时,这个路径就已经出现错误了
使用
man install_name_tool
查看install_name_tool
命令
install_name_tool
命令:改变动态库的install name
,相当于所在路径使用
-id
参数,改变动态库所在路径
使用
install_name_tool
命令,将动态库所在路径修改为绝对路径install_name_tool -id /Users/zang/Zang/Spark/Test3/Frameworks/TestExample.framework/TestExample TestExample
使用
otool -l TestExample | grep 'ID_DYLIB' -A 2
,查看TestExample
动态库中存储的所在路径cmd LC_ID_DYLIB cmdsize 104 name /Users/zang/Zang/Spark/Test3/Frameworks/TestExample.framework/TestExample (offset 24)
- 修改后的绝对路径已经生效
来到项目根目录,使用
./build.sh
命令,重新链接test
可执行文件-------------编译test.m to test.o------------------ -------------将test.o链接成可执行文件------------------
使用
otool -l test | grep 'DYLIB' -A 2
命令,查看Mach-O
中动态库的路径cmd LC_LOAD_DYLIB cmdsize 104 name /Users/zang/Zang/Spark/Test3/Frameworks/TestExample.framework/TestExample (offset 24)
TestExample
动态库修改后的路径,在Mach-O
中生效
运行
test
可执行文件
- 使用
lldb
命令,在终端中进入lldb
环境- 使用
file test
命令,将test
可执行文件包装成一个target
- 使用
r
命令,开始运行Process 23430 launched: '/Users/zang/Zang/Spark/Test3/test' (x86_64) 2021-03-04 18:52:09.602958+0800 test[23430:8086654] testApp---- 2021-03-04 18:52:09.603205+0800 test[23430:8086654] TestExample---- Process 23430 exited with status = 0 (0x00000000)
- 执行成功,
image not found
错误彻底解决
@rpath
上述案例中,使用绝对路径,虽然程序执行成功,但无法通用。这时需要动态库和可执⾏程序双方约定一个规则,由可执⾏程序(案例中的test
可执行文件)提供一个变量,动态库基于这个变量指定自身的相对路径
@rpath
(Runpath search Paths
):dyld
搜索路径运⾏时
@rpath
指示dyld
按顺序搜索路径列表,以找到动态库
@rpath
:可以保存⼀个或多个路径的变量,谁链接我谁来提供
链接成为动态库时,指定相对路径
使用
ld
命令的-install_name
参数,在链接成为动态库时,指定所在路径
来到
TestExample.framework
目录,打开build.sh
文件,改为以下代码:echo "-------------编译TestExample.m to TestExample.o------------------" clang -x objective-c \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -I./Headers \ -c TestExample.m -o TestExample.o echo "-------------TestExample.o to TestExample------------------" clang -dynamiclib \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -Xlinker -install_name -Xlinker @rpath/TestExample.framework/TestExample \ TestExample.o -o TestExample
- 将
-install_name
参数设置为@rpath/TestExample.framework/TestExample
@rpath
由test
可执行文件提供,TestExample
动态库指定@rpath
之后的相对路径使用
./build.sh
命令,执行Shell
脚本-------------编译TestExample.m to TestExample.o------------------ -------------TestExample.o to TestExample------------------
使用
otool -l TestExample | grep 'ID_DYLIB' -A 2
,查看TestExample
动态库中存储的所在路径cmd LC_ID_DYLIB cmdsize 72 name @rpath/TestExample.framework/TestExample (offset 24)
- 动态库的所在路径指定成功
生成可执行文件时,指定
@rpath
参数使用
install_name_tool
命令的-add_rpath
参数,对Mach-O
添加@rpath
参数
使用
install_name_tool
命令,对test
可执行文件添加@rpath
参数install_name_tool -add_rpath /Users/zang/Zang/Spark/Test3/Frameworks test
使用
otool -l test | grep 'rpath' -A 5 -i
命令,查看@rpath
参数是否生效。如果添加成功,Mach-O
中有一个名称为LC_RPATH
的Load Command
,存储了@rpath
设置的路径cmd LC_RPATH cmdsize 56 path /Users/zang/Zang/Spark/Test3/Frameworks (offset 12)
- 命令最后的
-i
参数,用于忽略大小写
运行
test
可执行文件
- 使用
lldb
命令,在终端中进入lldb
环境- 使用
file test
命令,将test
可执行文件包装成一个target
- 使用
r
命令,开始运行Process 26436 launched: '/Users/zang/Zang/Spark/Test3/test' (x86_64) 2021-03-05 10:56:26.721260+0800 test[26436:8191289] testApp---- 2021-03-05 10:56:26.721506+0800 test[26436:8191289] TestExample---- Process 26436 exited with status = 0 (0x00000000)
- 执行成功,双方约定的
@rpath
参数设置成功
@executable_path
此时Mach-O
中,@rpath
设置为绝对路径,这显然不合理
系统为此提供了参数
@executable_path
:表示可执⾏程序所在的⽬录,解析为可执⾏⽂件的绝对路径
将
Mach-O
中的@rpath
路径,修改为相对路径使用
install_name_tool
命令的-rpath
参数,将Mach-O
中@rpath
老路径修改为新路径
使用
install_name_tool
命令,修改Mach-O
中@rpath
路径install_name_tool -rpath /Users/zang/Zang/Spark/Test3/Frameworks @executable_path/Frameworks test
使用
otool -l test | grep 'RPATH' -A 5
命令,查看Mach-O
中@rpath
路径修改结果cmd LC_RPATH cmdsize 40 path @executable_path/Frameworks (offset 12)
运行
test
可执行文件
- 使用
lldb
命令,在终端中进入lldb
环境- 使用
file test
命令,将test
可执行文件包装成一个target
- 使用
r
命令,开始运行Process 26452 launched: '/Users/zang/Zang/Spark/Test3/test' (x86_64) 2021-03-05 11:00:25.779799+0800 test[26452:8193402] testApp---- 2021-03-05 11:00:25.780064+0800 test[26452:8193402] TestExample---- Process 26452 exited with status = 0 (0x00000000)
- 执行成功,在
Mach-O
中设置@executable_path
之后的相对路径成功
@loader_path
系统除了提供
@executable_path
之外,还提供了一个@loader_path
参数
@loader_path
:表示被加载的Mach-O
所在的⽬录。每次加载时,都可能被设置为不同的路径,由上层指定
一个动态库有可能被
Mach-O
链接,也有可能被另一个动态库链接
@executable_path
获取的是Mach-O
的路径,而@loader_path
获取的是链接者的路径如果动态库被另一个动态库链接,
@loader_path
将获取到另一个动态库的所在路径
修改上述案例,让
TestExample
动态库链接另一个动态库
链接成
Log
动态库
- 在
TestExample.framework
目录下,创建Frameworks
目录- 在
Frameworks
目录下,创建Log.framework
目录- 在
Log.framework
目录下,创建Headers
目录在
Headers
目录下,创建Log.h
文件,写入以下代码:#import <Foundation/Foundation.h> @interface Log : NSObject - (void)test_example_log:(_Nullable id)e; @end
在
Log.framework
目录下,创建Log.m
文件,写入以下代码:#import "Log.h" @implementation Log - (void)test_example_log:(_Nullable id)e { NSLog(@"Log---%@", e); } @end
在
Log.framework
目录下,创建build.sh
文件,写入以下代码:echo "-------------编译Log.m to Log.o------------------" clang -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -I./Headers \ -c Log.m -o Log.o echo "-------------Log.o to Log------------------" clang -dynamiclib \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -Xlinker -install_name -Xlinker @rpath/Log.framework/Log \ Log.o -o Log
使用
./build.sh
命令,执行Shell
脚本-------------编译Log.m to Log.o------------------ -------------Log.o to Log------------------
执行成功,目录下自动生成
Log
动态库
链接成
TestExample
动态库来到
TestExample.framework
目录,打开TestExample.m
文件,改为以下代码:#import "TestExample.h" #import "Log.h" @implementation TestExample - (void)lg_test:(_Nullable id)e { NSLog(@"TestExample----"); Log *log = [Log new]; [log test_example_log: self]; } @end
链接成
TestExample
动态库,需要指定自身的所在路径,还要为Log
动态库提供@rpath
。因为@rpath
的特性是:谁链接我谁来提供使用
ld
命令的-rpath
参数,可以为链接的动态库提供@rpath
打开
build.sh
文件,改为以下代码:echo "-------------编译TestExample.m to TestExample.o------------------" clang -x objective-c \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -I./Headers \ -I./Frameworks/Log.framework/Headers \ -c TestExample.m -o TestExample.o echo "-------------TestExample.o to TestExample------------------" clang -dynamiclib \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -Xlinker -install_name -Xlinker @rpath/TestExample.framework/TestExample \ -Xlinker -rpath -Xlinker @loader_path/Frameworks \ -F./Frameworks \ -framework Log \ TestExample.o -o TestExample
使用
./build.sh
命令,执行Shell
脚本-------------编译TestExample.m to TestExample.o------------------ -------------TestExample.o to TestExample------------------
执行成功,目录下自动生成
TestExample
动态库
生成
test
可执行文件来到项目根目录,打开
build.sh
文件,改为以下代码:echo "-------------编译test.m to test.o------------------" clang -x objective-c \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -I./Frameworks/TestExample.framework/Headers \ -c test.m -o test.o echo "-------------将test.o链接成可执行文件------------------" clang -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -Xlinker -rpath -Xlinker @executable_path/Frameworks \ -F./Frameworks \ -framework TestExample \ test.o -o test
使用
./build.sh
命令,执行Shell
脚本-------------编译test.m to test.o------------------ -------------将test.o链接成可执行文件------------------
执行成功,目录下自动生成
test
可执行文件
运行
test
可执行文件
- 使用
lldb
命令,在终端中进入lldb
环境- 使用
file test
命令,将test
可执行文件包装成一个target
- 使用
r
命令,开始运行Process 28064 launched: '/Users/zang/Zang/Spark/Test3/test' (x86_64) 2021-03-05 14:47:49.908720+0800 test[28064:8304286] testApp---- 2021-03-05 14:47:49.908996+0800 test[28064:8304286] TestExample---- 2021-03-05 14:47:49.909163+0800 test[28064:8304286] Log---<TestExample: 0x100204480> Process 28064 exited with status = 0 (0x00000000)
- 执行成功,使用
@loader_path
参数可以成功获取链接者的路径
-reexport_framework
上述案例中,
test
可执行文件链接TestExample
动态库,TestExample
动态库链接Log
动态库
- 如果
test
可执行文件想直接调用Log
动态库中的方法,目前是无法调用的
问题本质:因为
test
可执行文件没有链接Log
动态库,所以test
可执行文件也无法使用Log
动态库的导出符号解决办法:
使用
ld
命令的-reexport_framework
参数,将指定的Framework
内全部符号变为可用
链接成
TestExample
动态库来到
TestExample.framework
目录,打开build.sh
文件,改为以下代码:echo "-------------编译TestExample.m to TestExample.o------------------" clang -x objective-c \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -I./Headers \ -I./Frameworks/Log.framework/Headers \ -c TestExample.m -o TestExample.o echo "-------------TestExample.o to TestExample------------------" clang -dynamiclib \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -Xlinker -install_name -Xlinker @rpath/TestExample.framework/TestExample \ -Xlinker -rpath -Xlinker @loader_path/Frameworks \ -Xlinker -reexport_framework -Xlinker Log \ -F./Frameworks \ -framework Log \ TestExample.o -o TestExample
使用
./build.sh
命令,执行Shell
脚本-------------编译TestExample.m to TestExample.o------------------ -------------TestExample.o to TestExample------------------
执行成功,目录下自动生成
TestExample
动态库
使用
otool -l TestExample | grep 'DYLIB' -A 2
命令,查找Mach-O
中DYLIB
关键字cmd LC_ID_DYLIB cmdsize 72 name @rpath/TestExample.framework/TestExample (offset 24) -- cmd LC_REEXPORT_DYLIB cmdsize 56 name @rpath/Log.framework/Log (offset 24)
- 此时
TestExample
动态库增加了一个名称为LC_REEXPORT_DYLIB
的Load Command
,它里面存储了Log
动态库的所在路径
test
可执行文件可以通过LC_REEXPORT_DYLIB
访问到Log
动态库,从而调用Log
动态库中的方法
生成
test
可执行文件来到项目根目录,打开
test.m
文件,改为以下代码:#import <Foundation/Foundation.h> #import "TestExample.h" #import "Log.h" int main(){ NSLog(@"testApp----"); TestExample *manager = [TestExample new]; [manager lg_test: nil]; Log *log = [Log new]; [log test_example_log: @"test-main()"]; return 0; }
打开
build.sh
文件,改为以下代码:echo "-------------编译test.m to test.o------------------" clang -x objective-c \ -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -I./Frameworks/TestExample.framework/Headers \ -I./Frameworks/TestExample.framework/Frameworks/Log.framework/Headers \ -c test.m -o test.o echo "-------------将test.o链接成可执行文件------------------" clang -target x86_64-apple-macos11.1 \ -fobjc-arc \ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk \ -Xlinker -rpath -Xlinker @executable_path/Frameworks \ -F./Frameworks \ -framework TestExample \ test.o -o test
使用
./build.sh
命令,执行Shell
脚本-------------编译test.m to test.o------------------ -------------将test.o链接成可执行文件------------------
执行成功,目录下自动生成
test
可执行文件
运行
test
可执行文件
- 使用
lldb
命令,在终端中进入lldb
环境- 使用
file test
命令,将test
可执行文件包装成一个target
- 使用
r
命令,开始运行Process 29877 launched: '/Users/zang/Zang/Spark/Test3/test' (x86_64) 2021-03-05 17:03:22.710283+0800 test[29877:8382166] testApp---- 2021-03-05 17:03:22.710562+0800 test[29877:8382166] TestExample---- 2021-03-05 17:03:22.710723+0800 test[29877:8382166] Log---<TestExample: 0x100404160> 2021-03-05 17:03:22.710752+0800 test[29877:8382166] Log---test-main() Process 29877 exited with status = 0 (0x00000000)
- 执行成功,
test
可执行文件成功调用Log
动态库中的test_example_log
方法,打印出Log---test-main()
tbd格式
tbd
:全称是text-based stub libraries
,本质上就是⼀个YAML
描述的⽂本⽂
件
tbd
格式的作⽤:
- ⽤于记录动态库的⼀些信息,包括导出的符号、动态库的架构信息、动态库的依赖信息
- ⽤于避免在真机开发过程中直接使⽤传统的
dylib
- 对于真机来说,由于动态库都是在设备上,在
Xcode
上使⽤基于tbd
格式的伪Framework
可以⼤⼤减少Xcode
的⼤⼩
tbd
格式的使用创建
LGApp
项目,将SYCSSColor
文件夹拷贝到项目的根目录
SYCSSColor
目录下,包含tbd
文件和头文件
创建
xcconfig
文件,并配置到Tatget
上,写入以下代码:HEADER_SEARCH_PATHS = ${SRCROOT}/SYCSSColor/Headers
- 指定头文件路径
Header Search Paths
打开
ViewController.m
文件,写入以下代码:#import "ViewController.h" #import <SYColor.h> @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; }
- 此时导入头文件,没有任何问题
在
viewDidLoad
方法中,使用SYColor
的初始化方法- (void)viewDidLoad { [super viewDidLoad]; SYColor *color = [SYColor new]; }
编译失败,提示错误信息:未定义的符号
_OBJC_CLASS_$_SYColor
Undefined symbols for architecture x86_64: "_OBJC_CLASS_$_SYColor", referenced from: objc-class-ref in ViewController.o ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation)
错误原因:项目中并没有配置库文件路径,也没有指定将要链接的库文件名称,所以无法找到库文件中的符号
解决此问题,除了指定库文件的路径和名称之外,还可以直接将库文件拖动到项目的
Frameworks
目录中
点击
Finish
完成
当
SYCSSColor.tbd
文件被拖入项目中,此时编译成功
在项目中,点击
SYCSSColor.tbd
文件,可以看到里面的内容:--- !tapi-tbd tbd-version: 4 targets: [ x86_64-ios-simulator ] uuids: - target: x86_64-ios-simulator value: D4120855-94BD-324C-ACAE-10B7EEB4A991 flags: [ not_app_extension_safe ] install-name: '@rpath/SYCSSColor.framework/SYCSSColor' exports: - targets: [ x86_64-ios-simulator ] symbols: [ _SYIsASCIIAlphaCaselessEqual, _SYIsASCIIDigit, >_SYIsASCIIHexDigit, _SYIsHTMLSpace, _SYToASCIIHexValue, >_SYToASCIILowerUnchecked, __ZN2SY9findColorEPKcj, _displayP3ColorSpaceRef, >_extendedSRGBColorSpaceRef, _linearRGBColorSpaceRef, _sRGBColorSpaceRef ] objc-classes: [ SYColor, SYExtendedColor ] ...
- 其中包含了导出符号的信息,所以
viewDidLoad
方法中,再去使用SYColor
的初始化方法将不再报错- 日常开发中,链接库文件时,需要指定库文件的路径和名称,本质上就是为了找到符号的所在位置
编译虽然通过,但在运行时,程序依然崩溃
dyld: Library not loaded: @rpath/SYCSSColor.framework/SYCSSColor Referenced from: /Users/zang/Library/Developer/CoreSimulator/Devices/BC871859-6A76-4967-A245-287615D883E6/data/Containers/Bundle/Application/69040F02-5186-470F-B2C4-EC5F81125E96/LGApp.app/LGApp Reason: image not found
问题本质:
在编译链接时,提供了符号所在位置,所以编译通过。但在运行时,由于加载的是动态库,动态库在运行时被
dyld
动态加载。这时需要找到符号的真实地址,如果没有找到,程序崩溃如果加载的是静态库,在链接的时候,代码和符号跟可执行文件会合并到一起,所以在运行时根本不需要这一步
tbd
生成原理
tbd
格式文件,本身是通过Xcode
内置工具tapi-installapi
专门来生成的,具体路径为:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/tapi installapi
生成
tbd
文件搭建
SYTimer
项目
SYTimer
是一个动态库项目
打开
Build Setting
,找到Text-Based API
,将Supports Text-Based InstallAPI
设置为Yes
通过
Other Text-Based InstallAPI Flags
给tapi-installapi
工具传递参数。常用参数:-ObjC:将输入文件视为Objective-C文件(默认) -ObjC++:将输入文件视为Objective-C++文件 -x<语言>:值为c、c++、Objective-c和Objective-c++ -Xparser <arg>:传递参数给clang parser。常用参数有:-Wno-deprecated-declarations、-Wno-unavailable-declarations -exclude-public-header <path>:引入的需要解析的public头文件路径
在项目中,
Build Setting
配置如下:
编译项目,在
.framework
文件中,生成.tbd
文件
tbd
参考资料
多架构合并
Fat Binary
Fat Binary
(胖二进制):本质上Fat Binary
就是将多个二进制文件打包到一起,不同架构的动态库可以打包。但打包后依然是多个动态库,Fat Binary
里会包含多个mach-header
,多个动态库也会排列在一起
打包
xcarchive
SYTimer
是一个测试项目
使用
man xcodebuild
查看xcodebuild
命令
- 编译
Xcode
项目所使用的命令使用
xcodebuild
命令,打包SYTimer
项目,指定模拟器平台xcodebuild archive -project 'SYTimer.xcodeproj' \ -scheme 'SYTimer' \ -configuration Release \ -destination 'generic/platform=iOS Simulator' \ -archivePath '../archives/SYTimer.framework-iphonesimulator.xcarchive' \ SKIP_INSTALL=NO
archive
:打包-project
:指定project
-scheme
:指定scheme
-configuration
:指定编译环境-destination
:指定分发平台-archivePath
:指定打包后的输出路径SKIP_INSTALL
:如果设置为YES
,打包时不会把编译产物放到Products
目录打包完成后,来到
archives
目录,生成SYTimer.framework-iphonesimulator.xcarchive
文件
使用
xcodebuild
命令,打包SYTimer
项目,指定真机平台xcodebuild archive -project 'SYTimer.xcodeproj' \ -scheme 'SYTimer' \ -configuration Release \ -destination 'generic/platform=iOS' \ -archivePath '../archives/SYTimer.framework-iphoneos.xcarchive' \ SKIP_INSTALL=NO
打包完成后,来到
archives
目录,生成SYTimer.framework-iphoneos.xcarchive
文件
右键
xcarchive
文件,显示包内容
BCSymbolMaps
:启用Bitcode
后,包含为Bitcode
生成的调试文件dSYMs
:包含调试文件Products
:包含编译产物使用
file SYTimer
命令,查看SYTimer
可执行文件内包含的架构SYTimer: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O dynamically linked shared library arm_v7] [arm64:Mach-O 64-bit dynamically linked shared library arm64] SYTimer (for architecture armv7): Mach-O dynamically linked shared library arm_v7 SYTimer (for architecture arm64):Mach-O 64-bit dynamically linked shared library arm64
打包命令中,并没有指定架构。但打包后
SYTimer
可执行文件中,包含了arm_v7
和arm64
两种架构。这个和SYTimer
项目中的Build Settings
设置有关
使用
xcodebuild
命令,不是简单的将.o
打包成动态库,Xcode
里的配置项也会对其生效
生成
Fat Binary
创建
lipo
目录,和SYTimer
、archives
目录平级
使用
man lipo
查看lipo
命令
- 创建或操作通用文件
使用
lipo
命令,将模拟器和真机两个平台的库文件进行合并lipo -output SYTimer -create ../archives/SYTimer.framework-iphoneos.xcarchive/Products/Library/Frameworks/SYTimer.framework/SYTimer ../archives/SYTimer.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/SYTimer.framework/SYTimer
-output
:指定输出文件-create
:指定将要合并的库文件命令执行后,出现错误提示
fatal error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/lipo: ../archives/SYTimer.framework-iphoneos.xcarchive/Products/Library/Frameworks/SYTimer.framework/SYTimer and ../archives/SYTimer.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/SYTimer.framework/SYTimer have the same architectures (arm64) and can't be in the same fat output file
- 包含了相同的
arm64
架构,无法合并Fat Binary
使用
lipo
命令,最大的问题就是包含相同架构,无法合并Fat Binary
。这种情况只能将所需的架构提取出来,再进行合并使用
lipo
命令,从模拟器平台的库文件中,提取x86_64
架构lipo -output SYTimer-x86_64 -extract x86_64 ../archives/SYTimer.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/SYTimer.framework/SYTimer
-output
:指定输出文件-extract
:从库文件中提取指定架构提取成功,
lipo
目录下生成SYTimer-x86_64
文件
使用
lipo
命令,将真机平台的库文件和提取出的SYTimer-x86_64
文件进行合并lipo -output SYTimer -create ../archives/SYTimer.framework-iphoneos.xcarchive/Products/Library/Frameworks/SYTimer.framework/SYTimer SYTimer-x86_64
合并成功,
lipo
目录下生成SYTimer
文件
lipo
命令的缺陷:
- 需要手动处理头文件、资源文件等内容,然后把它包装成新的
Framework
- 需要处理
dSYMs
文件;作为SDK
的提供者,应该将dSYMs
文件提供给使用者。当程序崩溃后可以恢复调用栈,以便问题的排查。此时不同架构生成的dSYMs
文件放到Framework
中就会比较麻烦,需要手动的匹配对比Framework
包装好,还需要重签名才可使用
XCFramework
XCFramework
:是苹果官⽅推荐的、⽀持的,可以更⽅便的表示⼀个多个平台和架构的分发⼆进制库的格式
- 需要
Xcode11
以上⽀持- 是为更好的⽀持
Mac Catalyst
和ARM
芯⽚的macOS
- 专⻔在
2019年
提出的Framework
的另⼀种先进格式支持平台和架构:
iOS/iPad
:arm64
iOS/iPad Simulator
:x86_64
、arm64
Mac Catalyst
:x86_64
、arm64
Mac
:x86_64
、arm64
和传统的
Framework
相⽐:
- 可以⽤单个
.xcframework
⽂件提供多个平台的分发⼆进制⽂件- 与
Fat Header
相⽐,可以按照平台划分,可以包含相同架构的不同平台的⽂件- 在使⽤时,不需要再通过脚本去剥离不需要的架构体系
生成
XCFramework
创建
xcframework
目录,和lipo
目录平级
使用
xcodebuild
命令,将模拟器和真机两个平台的Framework
合并成XCFramework
xcodebuild -create-xcframework \ -framework '../archives/SYTimer.framework-iphoneos.xcarchive/Products/Library/Frameworks/SYTimer.framework' \ -framework '../archives/SYTimer.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/SYTimer.framework' \ -output 'SYTimer.xcframework'
-create-xcframework
:指定创建一个XCFramework
-framework
:指定将要合并的Framework
所在目录-output
:指定XCFramework
输出文件创建成功,
xcframework
目录下生成SYTimer.xcframework
文件
SYTimer.xcframework
文件内,按照合并的平台生成目录- 一个文件内包括多个平台
- 不同平台出现相同架构可自行处理
打包调试文件
创建的
SYTimer.xcframework
文件中,没有包含BCSymbolMaps
和dSYMs
调试文件,所以还是不合符预期打开
xcframework
目录,删除SYTimer.xcframework
文件,创建build.sh
文件
打开
build.sh
文件,写入以下代码:ARCHIVES=/Users/zang/Zang/Spark/LG/5/archives xcodebuild -create-xcframework \ -framework '../archives/SYTimer.framework-iphoneos.xcarchive/Products/Library/Frameworks/SYTimer.framework' \ -debug-symbols "${ARCHIVES}/SYTimer.framework-iphoneos.xcarchive/BCSymbolMaps/5931C37A-A124-3A84-9700-B35D2FC45E2F.bcsymbolmap" \ -debug-symbols "${ARCHIVES}/SYTimer.framework-iphoneos.xcarchive/BCSymbolMaps/AF91A962-7411-39B7-8D41-A2A5209DCFD2.bcsymbolmap" \ -debug-symbols "${ARCHIVES}/SYTimer.framework-iphoneos.xcarchive/dSYMs/SYTimer.framework.dSYM" \ -framework '../archives/SYTimer.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/SYTimer.framework' \ -debug-symbols "${ARCHIVES}/SYTimer.framework-iphonesimulator.xcarchive/dSYMs/SYTimer.framework.dSYM" \ -output 'SYTimer.xcframework'
ARCHIVES
:定义变量,存储xcarchive
文件所在archives
目录的绝对路径-debug-symbols
:指定调试文件路径,必须使用绝对路径- 使用
Shell
变量${ARCHIVES}
,必须放在""
中使用
./build.sh
命令,执行Shell
脚本。xcframework
目录下生成SYTimer.xcframework
文件
SYTimer.xcframework
文件中,各个平台对应的目录下成功生成调试文件
使用
XCFramework
创建
LGApp
测试项目
将上述案例生成的
SYTimer.xcframework
文件,拖入项目
打开
ViewController.m
文件,写入以下代码:#import "ViewController.h" #import <SYTimer/SYTimer.h> @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; SYTimer *timer= [SYTimer new]; NSLog(@"%@",timer); } @end
选择真机,项目运行成功,输出内容如下:
2021-03-08 10:16:54.288197+0800 LGApp[37012:8784365] <SYTimer: >0x600003dfb2a0>
xcframework
文件和普通Framework
文件的使用别无二致。xcframework
中打包了多个平台的Framework
,比普通Framework
文件更大。但在实际使用中,xcframework
会根据当前链接的平台架构,仅链接相应的库文件,不会将整个xcframework
全部链接来到项目编译后的目录,打开
LGApp.app
文件,Frameworks
目录中只导入了一个SYTimer.framework
进入
SYTimer.framework
目录,打开Info.plist
文件
- 导入的是真机平台的
Framework
xcframework
的优势:
- 不用手动处理头文件、资源文件等内容
- 重复架构可自行处理
- 更方便的导入调式符号
- 仅链接相应平台架构的库文件
总结
动态库与静态库的区别:
- 静态库:只是
.o
文件的合集- 动态库:
.o
文件是链接过后的最终产物,所以动态库不能合并解决
image not found
错误
- 生成动态库时,指定自身所在路径,提供
@rpath
之后的相对路径@rpath
:可以保存⼀个或多个路径的变量,谁链接我谁来提供@executable_path
:表示可执⾏程序所在的⽬录,解析为可执⾏⽂件的绝对路径@loader_path
:表示被加载的Mach-O
所在的⽬录。每次加载时,都可能被设置为不同的路径,由上层指定多架构合并,使用
XCFramework
更具优势
- 不用手动处理头文件、资源文件等内容
- 重复架构可自行处理
- 更方便的导入调式符号
- 仅链接相应平台架构的库文件