前言

一、首先先来看一下.a文件

二、再来了解一下什么是clang

三、将.m编译成.o

四、为什么目标文件只需要引入头文件

五、从编译目标文件到最终生成可执行文件

六、验证静态库是.o文件的合集

七、.o生成.a

八、libtool

九、clang module 初识

十、Framework

十一、编写简单的脚本 - shell

十二、Dead Code Strip


通过对符号的理解,那么继续来探索一下静态库和动态库的原理,今天主要探索静态库。探索之前我们先来了解一下基本的一些概念:

1、常用的库文件格式:.a .dylib(苹果不允许ios使用.dylib格式的动态库,因为没有签名) .framework .xcframework

2、库就是一段编译好的二进制代码,加上头文件就以及相关资源可以供别人使用。


一、首先先来看一下.a文件

通过file libAFNetworking.a

可以看到.a 是一个文档格式
通过 ar -t libAFNetworking.a 可以看到.a里面所有的.o文件

也就验证了.a 是所有.o文件的合集
ar 命令就是一个创建或者修改一个文档格式库的命令

二、再来了解一下什么是clang

简单来说:clang - the Clang C, C++, and Objective-C compiler (编译器)
具体来说:clang is a C, C++, and Objective-C compiler which encompasses prepro-
cessing, parsing, optimization, code generation, assembly, and linking.
也就是说 clang是一个工具的集合,包括预处理,解析,优化,代码生成,汇编器,和链接。
clang就是编译器、汇编器、链接器的一个接口。

三、将.m编译成.o

在一个test.m中,我将要把AFNetworking链接进去。

#import <Foundation/Foundation.h>
#import <AFNetworking.h>
int main(){
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    NSLog(@"testApp----%@", manager);
    return 0;
}

通过命令:

clang -x objective-c \ 
-target x86_64-apple-macos11.3 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.3.sdk \
-I./AFNetworking \
-c test.m -o test.o

clang -x objective-c : 指定编译语言
-target x86_64-apple-macos11.3: 指定编译架构
-fobjc-arc :指定arc
-isysroot: 指定sdk
-I.:指定要链接的文件目录
-c :编译成.o

以上是编译单个.o文件的,那么编译多个.o文件到合并的过程是什么呢?
简单的来说就是把所有的.m都编译成.o,然后把.o的路径放到一个文件里面去,clang在编译的时候会接受这个文件,这样就会在链接的过程中,把.o进行合并。

四、为什么目标文件只需要引入头文件


可以看到.o文件中有一张重定位符号表。它保存了.o文件中使用的所有符号。链接的时候根据重定位符号表重定位生成具体的符号信息。所以这就是为什么目标文件中,只需要告诉它头文件信息就可以了。

五、从编译目标文件到最终生成可执行文件

 clang -target x86_64-apple-macos11.3 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.3.sdk \
-L./AFNetworking \
-lAFNetworking \
test.o -o test

-L./AFNetworking : 指定库文件路径(.a .dylib)
-lAFNetworking : 指定链接的库文件名称
可以看到目标文件已经生成:

六、验证静态库是.o文件的合集

我创建一个类TestExample放到StaticLibrary文件中。

#import "TestExample.h"
@implementation TestExample

- (void)magic_test:(_Nullable id)e {
    NSLog(@"TestExample----");
}
@end

(1)根据上面的命令生成.o文件 TestExample.o
(2)直接改.o文件名为 libTestExample.dylib
(3)用test.m去链接这个静态库,先生成test.o
(4)然后链接,最终生成test可执行文件
(6)通过lldb 命令 运行这个可执行文件。

可以看到完美打印,证明静态库就是.o文件的合集。

七、.o生成.a

ar -rc libTestExample.a TestExample.o

八、libtool

合并静态库

九、clang module 初识

比如 #import <Foundation/Founddation.h> 的时候,通过module (clang -fmodules),可以将.h 预先编译成二进制,缓存到当前电脑的缓存中,在其他地方import的时候,可以直接拿来用。

重点是 autolink,当我们‘import <模块>’时,不需要我们再去往链接器去配置链接参数。比如'import '我们在代码里使用这个framework格式的库文件,那么在生成目标文件时,会自动在目标文件的'mach-o'中,插入一个 ‘load command’格式是'LC_LINKER_OPTION',存储这样一个链接器参数 '-framework '

十、Framework

1、简单的来说Framework就是一种打包方式,将库的二进制文件,头文件和有关的资源打包到一起,方便管理和分发。

那么我们自己制作的Framework和系统提供的Framework,比如 UIKit.Framework 的区别是什么呢?
系统的Framework是不需要拷贝到目标程序中的,是嵌入在电脑或者手机中的。而我们自己的制作的Framework,包括静态库和动态库,最后都是要拷贝到App中的。app和Extension的Bundle是共享的。因此我们制作的Framework又叫做 Embedded Framework

2、链接Framework


首先通过命令

clang -x objective-c \
-target x86_64-apple-macos11.3 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.3.sdk \
-I./Frameworks/TestExample.framework/Headers \
-c test.m -o test.o

生成.o文件
然后通过命令

clang -target x86_64-apple-macos11.3 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.3.sdk \
-F./Frameworks \
-framework TestExample \
test.o -o test

生成test可执行文件,其中遇到的一个坑点是,一定要把TestExample的后缀名给去掉,要不然会找不到framework。

其中-F 就是放置framework的目录,就是xcode中的framework search path

-framework <framwork_name> 指定链接的framework名称


3、总结一下链接成功一个库文件的3个必要因素:

1、头文件 (-I<directory>)  -> header search path

2、库文件目录(-L<dir>)  -> library search path 或者  -F<dicrectory> -> framework search path

3、库文件名称(-l<library_name>)  other link flags -lAFNetworking  或者  -framework <framwork_name> -> other link flags -framework AFNetworking
或者直接在Xcode->Build Phases->Link Binary With Libraries中添加对应的库文件

其中.a库文件的查找规则是:先找lib+<library_name>的动态库,找不到,再去找lib+<library_name>的静态库,还找不到就报错。

十一、编写简单的脚本 - shell

shell 是解释型语言:程序不需要编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次。
创建一个make.sh

#注意变量等号两边不能有空格
SYSROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.3.sdk

FILE_NAME=test

echo "-----开始编译test.m -> test.o"
#编译test.m -> test.o
clang -x objective-c \
-target x86_64-apple-macos11.3 \
-fobjc-arc \
-isysroot ${SYSROOT} \
-I./StaticLibrary \
-c ${FILE_NAME}.m -o ${FILE_NAME}.o

echo "-----开始进入StaticLibrary目录"
#进入StaticLibrary目录
pushd ./StaticLibrary

echo "-----开始编译TestExample.m -> TestExample.o"
#编译TestExample.m -> TestExample.o
clang -x objective-c \
-target x86_64-apple-macos11.3 \
-fobjc-arc \
-isysroot ${SYSROOT} \
-c TestExample.m -o TestExample.o

echo "-----开始生成.a文件"
#生成.a文件
ar -rc libTestExample.a TestExample.o

echo "-----返回之前目录"
#返回之前目录
popd

echo "-----链接libTestExample.a,生成可执行文件"
#链接libTestExample.a,生成可执行文件
clang -target x86_64-apple-macos11.3 \
-fobjc-arc \
-isysroot ${SYSROOT} \
-L./StaticLibrary \
-lTestExample \
${FILE_NAME}.o -o ${FILE_NAME}

然后给make.sh 赋予可执行权限 chmod +x ./make.sh
运行./make.sh

十二、Dead Code Strip

1、还是以上面的代码例子为例,我将test.m中的TestExample的方法注释掉

#import <Foundation/Foundation.h>
#import "TestExample.h"
int main(){
    NSLog(@"testApp----");
//    TestExample *manager = [TestExample new];
//    [manager magic_test: @"123"];
    return 0;
}

然后查看test可执行文件中是否包含TestExample的相关代码: objdump --macho -d test

可以发现并没有看到TestExample的相关方法。
这也就证明,在执行clang命令的时候,dead code strip是默认生效的。

2、oc的分类会被dead code strip脱掉

由于oc的动态性,分类是运行的过程中才去加载的,但是dead code strip是在链接的过程中就执行了,所以会发现分类没有使用,从而被脱掉。

3、-noall_load、-ObjC、 -all_load 、-force_load

如果解决分类被剥离的问题呢,链接器给我们提供了这4个参数。
-noall_load: 默认参数,是不全部链接
-ObjC:oc的所有代码不要剥离,其他的可以剥离
-all_load:所有代码都不要剥离
-force_load:指定哪些静态库不要剥离

 OTHER_LDFLAGS=-Xlinker -force_load ${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/StaticFramework.framework/StaticFramework

显然 解决以上问题,可以使用 -ObjC、-all_load或者-force_load

4、.o和.o链接 和 .o和.a链接的区别

.o链接.o 是先把所有.o文件合并成一个大的.o,然后再去链接生成可执行文件。
.o链接.a 是会在链接.a的过程中,会把".a 进行死代码剥离"。

5、.o和.o链接的时候(app主工程链接的时候),dead code strip是无效的,那么有什么方法可以剥离无用的代码呢?

可以通过链接器提供的另外一个参数 LTO 来进行剥离。关于LTO,后面会详细分析。

6、和xcode->build setting中 dead code stripping的区别


上面说的 -noall_load、 -ObjC、 -all_load 、-force_load 这些参数只针对链接静态库有效。
而xcode->build setting 中的 dead code 设置是不一样的。这里是我们在链接的过程中,链接器提供了一种优化代码的方式。

-dead_strip

通过man ld 查看

  -dead_strip
                 Remove functions and data that are unreachable by the entry
                 point or exported symbols. // 没有被入口点和导出符号用到的函数或者代码,就会被删除。

比如:test.m

#import <Foundation/Foundation.h>
#import "TestExample.h"
// 全局符号 
// 2. 导出符号使用->干掉
void global_function() {
    
}
// point
// dead strip
// 1. 入口点使用->干掉
int main(){
    //global_function();
    NSLog(@"testApp----");
//    TestExample *manager = [TestExample new];
//    [manager lg_test: nil];
    return 0;
}
// 本地符号
static void static_function() {
    
}

如果我们不再main函数中使用global_function(),此时通过之前的脚本编译这个test.m文件生成test,然后可以查看它的符号表中会包含global_function()。
那么当我们往链接器再传入-dead_strip参数的时候(-Xlinker -dead_strip),重新编译,会发现global_function被干掉了。

当我们使用global_function的时候,可以通过 -Xlinker -why_live -Xlinker _global_function 知道为什么global_function符号存在的原因:就是因为被入口点使用了。

那么我们往链接器再传入-all_load参数,重新编译,看下TestExample是否会被干掉。显然没有把TestExample给干掉,原因是:oc是动态语言,再运行的时候才会去执行,所以-dead_strip是不会把oc相关代码给干掉的。

总结:-dead strip 和 -noall_load、 -ObjC、 -all_load 、-force_load 不是一回事,前者只是链接器提供了一种优化代码的方式,后者只针对静态库。

-dead strip 规则:
(1)没有被入口点使用的方法或者函数,会被干掉
(2)没有被导出符号使用的方法或者函数,会被干掉
(3)不会干掉oc相关代码