美文网首页
iOS高级强化--009:动态库

iOS高级强化--009:动态库

作者: 帅驼驼 | 来源:发表于2021-03-08 14:50 被阅读0次
什么是动态库?

与静态库相反,动态库在编译时并不会被拷⻉到⽬标程序中,⽬标程序中只会存储指向动态库的引⽤。等到程序运⾏时,动态库才会被真正加载进来。格式有:.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:链接指定库,这里指定了两个必须使用的系统库,systemFoundation

使用./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_DYLIBLoad 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-OLC_LOAD_DYLIB存储的动态库路径不正确

解决办法:当链接成为一个动态库时,要在动态库中指定正确的所在路径

在动态库中,有一个名称为LC_ID_DYLIBLoad 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可执行文件)提供一个变量,动态库基于这个变量指定自身的相对路径

@rpathRunpath 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
  • @rpathtest可执行文件提供,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_RPATHLoad 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-ODYLIB关键字

         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_DYLIBLoad 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 Flagstapi-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_v7arm64两种架构。这个和SYTimer项目中的Build Settings设置有关

使用xcodebuild命令,不是简单的将.o打包成动态库,Xcode里的配置项也会对其生效

生成Fat Binary

创建lipo目录,和SYTimerarchives目录平级

使用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 CatalystARM芯⽚的macOS
  • 专⻔在2019年提出的Framework的另⼀种先进格式

支持平台和架构:

  • iOS/iPadarm64
  • iOS/iPad Simulatorx86_64arm64
  • Mac Catalystx86_64arm64
  • Macx86_64arm64

和传统的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文件中,没有包含BCSymbolMapsdSYMs调试文件,所以还是不合符预期

打开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更具优势

  • 不用手动处理头文件、资源文件等内容
  • 重复架构可自行处理
  • 更方便的导入调式符号
  • 仅链接相应平台架构的库文件

相关文章

网友评论

      本文标题:iOS高级强化--009:动态库

      本文链接:https://www.haomeiwen.com/subject/ghrqqltx.html