注册

什么是库(Library)?

常见库文件格式:.a.dylib.framework.xcframework.tdb

什么是库(Library)?

库(Library)本质上就是一段编译好的二进制代码,加上头文件就可以供别人使用。

应用场景?

  1. 某些代码需要给别人使用,但是不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件。
  2. 对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要 Link 一下,不会浪费编译时间。

什么是链接(Link)?

库在使用的时候需要链接(Link),链接 的方式有两种:

  1. 静态
  2. 动态

静态库

静态库即静态链接库:可以简单的看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的文件。Windows 下的 .libLinux 和 Mac 下的 .aMac独有的.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.通过clangtest.m编译成目标文件.o

clang - the Clang C, C++, and Objective-C compiler
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命令我们看到clangCC++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是创建一个targetr是运行的意思。
接着再看下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.alibSDWebImage.alibMerge.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 平台还可以使用 FrameworkFramework 实际上是一种打包方式,将库的二进制文件、头文件和有关的资源文件打包到一起方便管理和分发。

Framework 和系统的 UIKit.Framework 还是有很大区别。系统的 Framework 不需要拷⻉到目标程序中,我们自己做出来的 Framework 哪怕是动态的,最后也还是要拷⻉到 App 中(App 和 Extension 的 Bundle 是共享的),因此苹果又把这种 Framework 称为 Embedded Framework

Embedded Framework

开发中使用的动态库会被放入到ipa下的framework目录下,基于沙盒运行。
不同的App使用相同的动态库,并不会只在系统中存在一份。而是会在多个App中各自打包、签名、加载一份。


framework即可以代表动态库也可以代表静态库。

生成framework

6551a5a535f696c73813603d25761799.png


编译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
这个时候就已经自动编译链接完成了,其中路径是pushdpopd自动生成的。
可以简单优化下脚本:

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;
}
默认clangdead 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


81f35a21e6d019e90b53848b16c3148f.png

2.创建一个project(TestApp)。

32426775273d095debddb6776767d432.png

3.打开workspace,添加一个project(创建的TestApp)(⚠️需要关闭打开的文件才会出现Add Files to TestDeadCodeStrip):

814767013ca8890f57b96b51991f1d86.png

1cc5f818dffbe6df1027a3695347a97a.png

4.ViewController.m中使用HPTestObject

#import <HPStaticFramework/HPTestObject.h>

- (void)viewDidLoad {
[super viewDidLoad];
HPTestObject *hpObject = [HPTestObject new];
[hpObject hp_test];
}

5.运行

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

-ObjCOC相关的代码不要剥离

//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_functionhp_test都没有了。
打开mainglobal_function()的注释再看下:

所以dead code strip-all_load-ObjC-force_load-noall_load不是一个东西,他有一定规则:

  1. 入口点没有使用->干掉
  2. 没有被导出符号使用->干掉

接着-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再使用。


dd65010f7f62d44b5c76bc87932c6a97.png


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

  1. 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: 生成目标文件,只运行preprocesscompileassemble不链接
-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过程

  1. 使用OC
  2. 生成指定架构的代码,Big Sur是:x86_64-apple-macos11.1,之前是:x86_64-apple-macos10.15。iOS模拟器是:x86_64-apple-ios14-simulator。更多内容可以参考target部分。
  3. 使用ARC
  4. 使用的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部分。
  5. 用到的其他库的头文件地址在./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










0 个评论

要回复文章请先登录注册