什么是库(Library)?
常见库文件格式:.a
,.dylib
,.framework
,.xcframework
,.tdb
什么是库(Library)?
库(Library
)本质上就是一段编译好的二进制代码,加上头文件就可以供别人使用。
应用场景?
- 某些代码需要给别人使用,但是不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件。
- 对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要
Link
一下,不会浪费编译时间。
什么是链接(Link)?
库在使用的时候需要链接(Link
),链接 的方式有两种:
- 静态
- 动态
静态库
静态库即静态链接库:可以简单的看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的文件。Windows
下的 .lib
,Linux
和 Mac
下的 .a
。Mac
独有的.framework
。
缺点: 浪费内存和磁盘空间,模块更新困难。
静态库链接
将一份AFNetworking
静态库文件(.h
头文件和.a
组成)和test.m
放到统一目录。test.m
如下:
#import <Foundation/Foundation.h>
#import <AFNetworking.h>
int main(){
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
NSLog(@"test----%@", manager);
return 0;
}
直接终端查看下.a
静态库究竟是什么。
➜ AFNetworking file libAFNetworking.a
libAFNetworking.a: current ar archive
可以看到.a
实际上是一个文档格式。也就是.o
文件的合集。可以通过ar
命令验证下。
ar -- create and maintain library archives
➜ AFNetworking ar -t libAFNetworking.a
__.SYMDEF
AFAutoPurgingImageCache.o
AFHTTPSessionManager.o
AFImageDownloader.o
AFNetworkActivityIndicatorManager.o
AFNetworking-dummy.o
AFNetworkReachabilityManager.o
AFSecurityPolicy.o
AFURLRequestSerialization.o
AFURLResponseSerialization.o
AFURLSessionManager.o
UIActivityIndicatorView+AFNetworking.o
UIButton+AFNetworking.o
UIImageView+AFNetworking.o
UIProgressView+AFNetworking.o
UIRefreshControl+AFNetworking.o
WKWebView+AFNetworking.o
.a
确实是.o
文件的合集。清楚了.a
后将AFNetworking
链接到test.m
文件。1.通过
clang
将test.m
编译成目标文件.o
DESCRIPTION
clang is a C, C++, and Objective-C compiler which encompasses prepro-
cessing, parsing, optimization, code generation, assembly, and linking.
Depending on which high-level mode setting is passed, Clang will stop
before doing a full link. While Clang is highly integrated, it is
important to understand the stages of compilation, to understand how to
invoke it. These stages are:
Driver The clang executable is actually a small driver which controls
the overall execution of other tools such as the compiler,
assembler and linker. Typically you do not need to interact
with the driver, but you transparently use it to run the other
tools.
通过
man
命令我们看到clang
是C
、C++
、OC
的编译器
,是一个集合包含了预处理
、解析
、优化
、代码生成
、汇编化
、链接
。clang -x objective-c \
-target x86_64-apple-ios14-simulator \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
-I ./AFNetworking \
-c test.m -o test.o
回车后就生成了test.o
目标文件。
\
为了转译回车,让命令换行更易读。-x
制定编译语言,-target
指定编译平台,-fobjc-arc
编译成ARC
,-isysroot
指定用到的Foundation
的路径,-I<directory>
在指定目录寻找头文件header search path
为什么生成目标文件只需要告诉头文件的路径就可以了?
因为在生成目标文件的时候,重定位符号表只需要记录哪个地方的符号需要重定位。在连接的时候链接器会自动重定位。(上面的例子中只需要保留AFHTTPSessionManager
的符号。)
2..o
生成可执行文件
clang -target x86_64-apple-ios14-simulator \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
-L./AFNetworking \
-lAFNetworking \
test.o -o test
这个时候test
可执行程序就生成了。
-L
要链接的库文件(libAFNetworking.a
)目录,-l
要链接的库文件(libAFNetworking.a)这里只写AFNetworking
是有查找规则的:先找lib+<library_name>的动态库,找不到,再去找lib+<library_name>的静态库,还找不到,就报错
。会自动去找libAFNetworking
。
经过上面的编译和链接清楚了其它参数都是固定的,那么链接成功一个库文件有3个要素:
1. -I<directory>
在指定目录寻找头文件 header search path
(头文件
)
2. -L<dir>
指定库文件路径(.a\.dylib
库文件) library search path
(库文件路径
)
3. -l<library_name>
指定链接的库文件名称(.a\.dylib
库文件)other link flags
-lAFNetworking (库文件名称
)
生成静态库
将自己的一个工程编译成.a
静态库。工程只有一个文件HPExample``.h
和 .m
:
#import <Foundation/Foundation.h>
@interface HPExample : NSObject
- (void)hp_test:(_Nullable id)e;
@end
#import "HPExample.h"
@implementation HPExample
- (void)hp_test:(_Nullable id)e {
NSLog(@"hp_test----");
}
@end
将HPExample.m
编译成.o
文件:
clang -x objective-c \
-target x86_64-apple-macos11.0 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
-I./StaticLibrary \
-c HPExample.m -o HPExample.o
这个时候生成了HPExample.o
文件,由于工程只有一个.o
文件,直接将文件修改为libExample.dylib
或者libHPExample.a
。
然后创建一个test.m
文件调用HPExample
:
#import <Foundation/Foundation.h>
#import "HPExample.h"
int main(){
NSLog(@"testApp----");
HPExample *manager = [HPExample new];
[manager hp_test: nil];
return 0;
}
将test.m
编译成test.o
:
clang -x objective-c \
-target x86_64-apple-macos11.0 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
-I./StaticLibrary \
> -c test.m -o test.o
test.o
链接HPExample
:
clang -target x86_64-apple-macos11.0 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
-L./StaticLibrary \
-lHPExample \
test.o -o test
test
。终端
lldb
执行test
:➜ staticLibraryCreat lldb
(lldb) file test
Current executable set to '/Users/binxiao/projects/library/staticLibraryCreat/test' (x86_64).
(lldb) r
Process 2148 launched: '/Users/binxiao/projects/library/staticLibraryCreat/test' (x86_64)
2021-02-13 13:22:49.150091+0800 test[2148:13026772] testApp----
2021-02-13 13:22:49.150352+0800 test[2148:13026772] hp_test----
Process 2148 exited with status = 0 (0x00000000)
.a
就是.o
的合集。file test
是创建一个target
,r
是运行的意思。接着再看下
libHPExample.a
文件。objdump --macho --private-header libHPExample.a
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL 0x00 OBJECT 4 1160 SUBSECTIONS_VIA_SYMBOLS
静态库的合并
根据上面的分析,那么静态库的合并也就是将所有的.o
放到一个文件中。
有两个.a
库:
静态库的合并有两种方式:libAFNetworking.a
,libSDWebImage.a
1.ar -rc libAFNetworking.a libSDWebImage.a
ar -rc libAFNetworking.a libSDWebImage.a
就相当于将后面的libSDWebImage.a
合并到libAFNetworking.a
。
2.libtool -static -o <OUTPUT NAME> <LIBRARY_1> <LIBRARY_2>
libtool
合并静态库。
libtool -static \
-o \
libMerge.a \
libAFNetworking.a \
libSDWebImage.a
//libAFNetworking.a要为目标文件路径,libMerge.a为输出文件
libAFNetworking.a
和libSDWebImage.a
为libMerge.a
了。在这个过程中libtool
会先解压两个目标文件,然后合并。在合并的过程中有两个问题:1.冲突问题。
2.
.h
文件。
clang
提供了mudule
可以预先把头文件(.h
)预先编译成二进制缓存到系统目录中, 再去编译.m
的时候就不需要再去编译.h
了。
LC_LINKER_OPTION
链接器的特性,Auto-Link
。启用这个特性后,当我们import <模块>
,不需要我们再去往链接器去配置链接参数。比如import <framework>
我们在代码里使用这个framework格式的库文件,那么在生成目标文件时,会自动在目标文件的Mach-O
中,插入一个load command
格式是LC_LINKER_OPTION
,存储这样一个链接器参数-framework <framework>
。
动态库
与静态库相反,动态库在编译时并不会被拷⻉到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。格式有:.framework
、.dylib
、.tdb
。
缺点:会导致一些性能损失。但是可以优化,比如延迟绑定(Lazy Binding
)技术。
.tdb
tbd
全称是text-based stub libraries
本质上就是一个YAML
描述的文本文件。他的作用是用于记录动态库的一些信息,包括导出的符号、动态库的架构信息、动态库的依赖信息。用于避免在真机开发过程中直接使用传统的dylib
。对于真机来说,由于动态库都是在设备上,在Xcode
上使用基于tbd
格式的伪framework
可以大大减少Xcode
的大小。
framework
Mac OS/iOS
平台还可以使用 Framework
。Framework
实际上是一种打包方式,将库的二进制文件、头文件和有关的资源文件打包到一起方便管理和分发。
Framework
和系统的 UIKit.Framework
还是有很大区别。系统的 Framework
不需要拷⻉到目标程序中,我们自己做出来的 Framework
哪怕是动态的,最后也还是要拷⻉到 App
中(App
和 Extension
的 Bundle
是共享的),因此苹果又把这种 Framework
称为 Embedded Framework
。
Embedded Framework
开发中使用的动态库会被放入到ipa
下的framework
目录下,基于沙盒运行。
不同的App
使用相同的动态库,并不会只在系统中存在一份。而是会在多个App
中各自打包、签名、加载一份。
framework
即可以代表动态库也可以代表静态库。生成framework
test.m
:clang -x objective-c \
-target x86_64-apple-macos11.0 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
-I ./Frameworks/HPExample.framework/Headers \
-c test.m -o test.o
.o
生成test
可执行文件clang -target x86_64-apple-macos11.0 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
-F./Frameworks \
-framework HPExample \
test.o -o test
framework
也就需要三个条件:1.
-I<directory>
:在指定目录寻找头文件 header search path
(头文件)2.
-F<directory>
:在指定目录寻找framework framework search path
3.
-framework <framework_name>
:指定链接的framework
名称 other link flags -framework AFNetworking
脚本执行命令
上面都是通过命令行来进行编译连接的,每次输入都很麻烦(即使粘贴复制),我们可以将命令保存在脚本中,通过执行脚本来执行命令。
还是以HPExample
为例,整理后脚本如下(可以加一些日志观察执行问题):
echo "test.m -> test.o"
clang -x objective-c \
-target x86_64-apple-macos11.0 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
-I ./StaticLibrary \
-c test.m -o test.o
echo "pushd -> StaticLibrary"
#cd可以进入到一个目录不推荐使用,cd会修改目录栈上层,推荐使用 pushd,pushd是往目录栈中push一个目录。
pushd ./StaticLibrary
echo "HPExample.m -> HPExample.o"
clang -x objective-c \
-target x86_64-apple-macos11.0 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
-I./StaticLibrary \
-c HPExample.m -o HPExample.o
echo "HPExample.o -> libHPExample.a"
#打包.o成静态库
ar -rc libHPExample.a HPExample.o
echo "popd -> StaticLibrary"
popd
echo "test.o -> test"
#链接
clang -target x86_64-apple-macos11.0 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
-L./StaticLibrary \
-lHPExample \
test.o -o test
pushd
和popd
自动生成的。可以简单优化下脚本:
SYSROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk
#${SYSROOT}和$SYSROOT都行,如果要匹配比如${SYSROOT}.mm则用{}
FILE_NAME=test
HEADER_SEARCH_PATH=./StaticLibrary
function MToOOrExec {
if [[ $2 == ".m" ]]; then
clang -x objective-c \
-target x86_64-apple-macos11.0 \
-fobjc-arc \
-isysroot ${SYSROOT} \
-I${HEADER_SEARCH_PATH} \
-c $1.m -o $1.o
else
clang -target x86_64-apple-macos11.0 \
-fobjc-arc \
-isysroot ${SYSROOT} \
-L${HEADER_SEARCH_PATH} \
-l$1 \
${FILE_NAME}.o -o ${FILE_NAME}
fi
return 0
}
echo "test.m -> test.o"
MToOOrExec ${FILE_NAME} ".m"
echo "pushd -> StaticLibrary"
#cd可以进入到一个目录不推荐使用,cd会修改目录栈上层,推荐使用 pushd,pushd是往目录栈中push一个目录。
pushd ${HEADER_SEARCH_PATH}
echo "HPExample.m -> HPExample.o"
MToOOrExec HPExample ".m"
echo "HPExample.o -> libHPExample.a"
#打包.o成静态库
ar -rc libHPExample.a HPExample.o
echo "popd -> StaticLibrary"
popd
echo "test.o -> test"
#链接
MToOOrExec HPExample ".o"
dead code strip
对于上面的例子,如果我们在test.m
中不使用HPExample
只是导入。
#import <Foundation/Foundation.h>
#import "HPExample.h"
int main(){
NSLog(@"test----");
// HPExample *manager = [HPExample new];
// [manager hp_test: nil];
return 0;
}
clang
的dead code strip
是生效的。在有分类的情况下
看另外一个例子,我们直接用
Xcode
创建一个framework
,设置为静态库。(Targets -> Build Settings -> Linking -> Macho-type
)这个库有一个
HPTestObject
以及HPTestObject+HPAdditions
。实现如下:HPTestObject
//.h
#import <Foundation/Foundation.h>
@interface HPTestObject : NSObject
- (void)hp_test;
@end
//.m
#import "HPTestObject.h"
#import "HPTestObject+HPAdditions.h"
@implementation HPTestObject
- (void)hp_test {
[self hp_test_additions];
}
@end
HPTestObject+HPAdditions
//.h
#import "HPTestObject.h"
@interface HPTestObject (HPAdditions)
- (void)hp_test_additions;
@end
//.m
#import "HPTestObject+HPAdditions.h"
@implementation HPTestObject (HPAdditions)
- (void)hp_test_additions {
NSLog(@"log: hp_test_additions");
}
@end
HPTestObject
设置为public
:我们知道分类是在运行时动态创建的,dead code strip
是在链接的过程中生效的。那么应该在链接的时候会strip
掉分类。
我们创建一个workspace验证下
workspace
A. 可重用性。多个模块可以在多个项目中使用。节约开发和维护时间。
B. 节省测试时间。单独模块意味着每个模块中都可以添加测试功能。
C. 更好的理解模块化思想。
1.File -> save as workspace
TestApp
)。workspace
,添加一个project
(创建的TestApp
)(⚠️需要关闭打开的文件才会出现Add Files to TestDeadCodeStrip
):ViewController.m
中使用HPTestObject
#import <HPStaticFramework/HPTestObject.h>
- (void)viewDidLoad {
[super viewDidLoad];
HPTestObject *hpObject = [HPTestObject new];
[hpObject hp_test];
}
libc++abi.dylib: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[HPTestObject hp_test_additions]: unrecognized selector sent to instance 0x600001048020'
terminating with uncaught exception of type NSException
dead code strip
脱掉了分类。要解决问题还是要告诉编译器不要脱。6.配置
XCConfig
告诉编译器不要脱。//-Xlinker 告诉 clang -all_load 参数是传给 ld 的。
OTHER_LDFLAGS=-Xlinker -all_load
再次运行App:
TestApp[8958:13347736] log: hp_test_additions
⚠️-Xlinker
告诉 clang
-all_load
参数是传给ld
的。-all_load
:全部链接
OTHER_LDFLAGS=-Xlinker -all_load
-ObjC
: OC
相关的代码不要剥离
//OTHER_LDFLAGS=-Xlinker -ObjC
-force_load
:指定哪些静态库不要 dead strip
HPSTATIC_FRAMEWORK_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/HPStaticFramework.framework/HPStaticFramework
OTHER_LDFLAGS=-Xlinker -force_load $HPSTATIC_FRAMEWORK_PATH
-noall_load
: 默认,没有使用静态库代码则不往可执行文件添加。This is the default. This option is obsolete.
以上4种参数仅针对静态库。dead code strip
是在链接过程中连接器提供的优化方式。
-dead_strip
Remove functions and data that are unreachable by the entry point or exported symbols.
移除没有被入口点(也就是main
)和导出符号用到的代码。
接着libraryDeadCodeStrip
工程验证下:
修改test.m
如下:
#import <Foundation/Foundation.h>
//#import "HPExample.h"
//全局函数
void global_function() {
}
//entry point
int main(){
// global_function();
NSLog(@"test----");
// HPExample *manager = [HPExample new];
// [manager hp_test: nil];
return 0;
}
//本地
static void static_function(){
}
运行
build.sh
libHPExample.a
相关的代码,加上all_load
再查看下:build.sh
修改增加-Xlinker -all_load \
hp_test
方法已经有了。修改
-Xlinker -all_load
为-Xlinker -dead_strip
再查看下:global_function
和hp_test
都没有了。打开
main
中global_function()
的注释再看下:所以dead code strip
和-all_load
、-ObjC
、-force_load
、-noall_load
不是一个东西,他有一定规则:
- 入口点没有使用->干掉
- 没有被导出符号使用->干掉
接着-Xlinker -dead_strip
和-Xlinker -all_load
一起添加:
-why_live
可以查看某一个符号为什么没有被干掉,比如我们要知道global_function
为什么没有被干掉: -Xlinker -why_live -Xlinker _global_function
.o -> .o
与.o -> .a
.o -> .o
是合并成一个大的.o
再去链接生成可执行文件。先组合再链接。所以这里dead code strip
干不掉,可以通过LTO
(Link-Time Optimization
)去优化。.o
链接静态库是.o
是去使用静态库。先dead code strip
再使用。
Do Not Embed
用于静态库
Embed & Sign
嵌入,用于动态库,动态库在运行时链接,所以它们编译的时候需要被打进
bundle
里面。静态库链接的时候代码就已经在一起了,所以不需要拷贝,直接Do Not Embed
就可以了。可以通过file
命令验证:file HPStaticFramework.framework/HPStaticFramework
HPStaticFramework.framework/HPStaticFramework: current ar archive random library
current ar archive
:说明是静态库,选择Do not embed
Mach-0 dynamically
:说明是动态库,选择Embed
Embed Without Signing
Signing
:只用于动态库,如果已经有签名了就不需要再签名。终端执行codesign -dv
判断:
codesign -dv HPStaticFramework.framework
Executable=/Users/***/Library/Developer/Xcode/DerivedData/TestDeadCodeStrip-fhbiunbplvqefkftfystdixdxmkq/Build/Products/Debug-iphonesimulator/HPStaticFramework.framework/HPStaticFramework
Identifier=HotpotCat.HPStaticFramework
Format=bundle with generic
CodeDirectory v=20100 size=204 flags=0x2(adhoc) hashes=1+3 location=embedded
Signature=adhoc
Info.plist entries=20
TeamIdentifier=not set
Sealed Resources version=2 rules=10 files=2
Internal requirements count=0 size=12
命令总结
clang命令参数
-x
: 指定编译文件语言类型-g
: 生成调试信息-c
: 生成目标文件,只运行preprocess
,compile
,assemble
不链接-o
: 输出文件-isysroot
: 使用的SDK路径-I<directory>
: 在指定目录寻找头文件 header search path
-L<directory>
:指定库文件路径(.a.dylib库文件)library search path
-l<library_name>
: 指定链接的库文件名称(.a.dylib库文件)other link flags -lAFNetworking
。链接的名称为libAFNetworking
/AFNetworking
的动态库或者静态库,查找规则:先找lib+<library_name>
的动态库,找不到,再去找lib+<library_name>
的静态库,还找不到,就报错。-F<directory>
: 在指定目录寻找framework
,framework search path
-framework <framework_name>
: 指定链接的framework名称
,other link flags -framework AFNetworking
test.m编译成test.o过程
- 使用OC
- 生成指定架构的代码,
Big Sur
是:x86_64-apple-macos11.1
,之前是:x86_64-apple-macos10.15
。iOS模拟器是:x86_64-apple-ios14-simulator
。更多内容可以参考target部分。 - 使用ARC
- 使用的SDK的路径在:
Big Sur
是:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk
之前是:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk
模拟器是:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
更多内容可以参考sdk部分。 - 用到的其他库的头文件地址在./Frameworks
命令示例:
clang -x objective-c \
-target x86_64-apple-ios14-simulator \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
-I ./AFNetworking \
-c test.m -o test.o
test.o链接生成test可执行文件
clang链接.a
静态库
顺序和生成.o
差不多,不需要指定语言。
命令示例:
clang -target x86_64-apple-ios14-simulator \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
-L./AFNetworking \
-lAFNetworking \
test.o -o test
ld链接.framework
静态库
ld -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
-lsystem -framework Foundation \
-lAFNetworking \
-L.AFNetworking \
test.o -o test
作者:HotPotCat
链接:https://www.jianshu.com/p/298efeb8732c