iOS开发进阶四:静态库

静态库原理

  • .a:静态库
  • .framework:既有静态库也有动态库
  • .dylib:传统意义上的动态库
  • .xcframework:2019年苹果推出的用于解决不同架构的库导致的开发问题

Framework 实际上是一种打包方式,将库的二进制文件,头文件和有关的资源文件打包到一起,方便管理和分发。

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

使用场景

  • 某些代码需要给别⼈使⽤,但是我们不希望别⼈看到源码,就需要以库的形式进⾏封装,只暴露出头⽂件。
  • 对于某些不会进⾏⼤改动的代码,我们想减少编译的时间,就可以把它打包成库。因为库是已经编译好的⼆进制,编译的时候只需要Link⼀下,不会浪费编译时间。

(Library)就是一段编译好的二进制代码,加上头文件就可以供别人使用。(Library)在使⽤的时候需要链接(Link),链接的⽅式有两种:静态动态静态库静态链接库:可以简单的看成⼀组⽬标⽂件(.o文件)的集合。即很多⽬标⽂件经过压缩打包后形成的⽂件。

链接静态库

在测试项目,包含有静态库libAFNetworking.a和使用AFNetworking的头文件。

链接静态库代码准备.png

静态库.a是什么格式?

使用file命令,查看一个文件的具体格式。

chenshuangchao@chenshuhaodeMBP ~ % file /Users/chenshuangchao/Desktop/链接 静态库AFNetworking/AFNetworking/libAFNetworking.a
/Users/chenshuangchao/Desktop/链接静态库AFNetworking/AFNetworking/libAFNetworking.a: current ar archive

  • executable 表示可执行的;
  • a.out为可执行文件;
  • shared object表示动态链接库;
  • relocatable表示可重定位,即目标文件;
  • current ar archive表示静态链接库。

archive是文档的意思;也就是我们常说的静态库是目标文件.O的合集。

怎么验证静态库是目标文件.O的合集?可以使用ar指令。

ar -t /Users/chenshuangchao/Desktop/链接静态库AFNetworking/AFNetworking/libAFNetworking.a 
通过ar指令查看.a文件的合集.png

验证了静态库是目标文件.O的合集。

整个编译过程先是把源文件编译生成目标文件(.o),再通过链接器生成可执行文件或者动态库。

1、查看clang命令

需要用到clang,那我们先了解一下clang是什么,在终端输入man clang

man指令查看Clang的作用.png

clangC、C++、Objective-C的编译器。它也是一个工具的合集,包含了预处理、解析、优化、代码生成、汇编化、链接。

2、使用clang命令,将.m文件编译成.o文件。

使用了AFNetworking的情况下,需要将AFN的头文件加载到编译命令中,库文件在编译时不需要,在链接时会根据头文件中的重定位符号表,去找到库文件中具体的符号。

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

  • -x:指定编译文件语言类型
  • -target:指定生成架构
  • -fobjc-arc:使用ARC
  • -isysroot:使用SDK的路径
  • -I:指定头文件路径Header Search Paths
  • -c:生成目标文件
  • -o:输出文件
将使用AFN的.m文件生成.o文件.png

如果出现isysroot错误

no such sysroot directory: '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk' [-Wmissing-sysroot]

可以在访达中通过command+shift+G查找路径/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/找到电脑中的sdk。

确定本机SDK版本.png
  • 目标文件中包含重定位符号表,它保存了文件中使用的所有符号,链接时会根据重定位符号表生成具体的符号信息。
  • 所以在生成目标文件时,只需静态库的头文件即可。重定位符号表只需要记录哪个地方的符号需要重定位,然后在链接过程中会自动将符号重定位。

3、由.o目标文件生成可执行文件

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

  • -L:指定库文件路径(.a\.dylib库文件)Library Search Paths
  • -l:指定链接的库文件名称(.a\.dylib库文件)Other Linker Flags -lAFNetworking

通过库文件路径下的文件名,可以自动找到libAFNetworking.a那个库文件。

静态库链接成功的三要素:

  • -I:指定头文件路径Header Search Paths
  • -L:指定库文件路径(.a\.dylib库文件)Library Search Paths
  • -l:指定链接的库文件名称(.a\.dylib库文件)Other Linker Flags -lAFNetworking

创建静态库

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

创建静态库准备.png

1、将TestExample.m生成TestExample.o文件

将一个.m文件生成一个.o文件,未使用第三方库的情况

chenshuangchao@chenshuhaodeMBP 链接静态库AFNetworking % cd /Users/chenshuangchao/Desktop/静态库原理/StaticLibrary
chenshuangchao@chenshuhaodeMBP StaticLibrary % clang -x objective-c -target x86_64-apple-macos10.15.7 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk -c TestExample.m -o TestExample.o

2、将TestExample.o文件变为静态库

因为静态库就是.o文件的合集,那么一个.o文件也可以是一个合集,一个.o文件也可以是一个静态库,可以直接将.o文件的后缀改为.a或者.dylib

TestExample.o-->libTestExample.dylib-->libTestExample

或者使用ar命令: ar -rc libTestExample.a TestExample.o

注意:一定要命名为libTestExample-l指令可以找到libTestExample,但是找不到TestExample

3、将test.m生成test.o

test.m中调用TestExample类生成的静态库。必须指定头文件。

clang -x objective-c -target x86_64-apple-macos10.15.7 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk -I ./StaticLibrary -c test.m -o test.o

4、将test.o链接生成可执行文件

clang -target x86_64-apple-macos10.15.7 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk -L./StaticLibrary -lTestExample test.o -o test

5、验证可执行文件test

在终端输入lldb,进入lldb环境。

运行file test,将可执行文件包装成一个target

使用r命令,开始运行。

静态库合并

静态库合并的两种方式:

  • 使用ar命令
  • 使用Xcode提供的libtool命令

通常会使用libtool

ar命令

  • 可以查看静态库
  • 也能将多个.o文件合并成静态库
  • 可以将静态库中包含的.o文件全部解压出来
  1. 使用ar x libAFNetworking.a命令,解压libAFNetworking.a静态库。
  2. ar r lib_AF_SD.a *.o命令,将目录下的所有.o文件,合并成一个.a文件

对两个.a文件的合并,可以通过先将.a文件解压成.o文件,再将所有.o文件合并。

libtool合并.a文件

libtool -static -o libCSC.a libAFNetworking.a libSDWebImage.a
静态库合并.png

合并成功,然后使用objdump查看重定位符号。

objdump --macho --rebase libCSC.a

Framework

Framework实际上是⼀种打包⽅式,将库的⼆进制⽂件,头⽂件和有关的资源⽂件打包到⼀起,⽅便管理和分发。

Framework和系统的UIKit.Framework还是有很⼤区别。系统的Framework不需要拷⻉到⽬标程序中,而自定义的Framework哪怕是动态的,最后也要拷⻉到App中(AppExtensionBundle是共享的),因此苹果⼜把这种Framework称为Embedded Framework

Framework可以是静态库,也可以是动态库。论是静态库还是动态库,Framework都包含了头文件、库的原始文件、签名和资源文件。

mudule

muduleclang提供的解析.h头文件的一种解析格式

mudule可以把.h头文件预先编译成二进制,存储到系统缓存目录中。好处:当编译.m文件时,避免反复编译.h文件

例如:当一个.h文件在很多.m中被import,当这些.m文件被编译时,不需要一遍又一遍的重复编译.h文件,它会直接使用mudule预先编译好的二进制

Auto-Link

Auto-LinkLC_LINKER_OPTION链接器的特性。启用该特性后,在import <模块>时不需要再往链接器去配置链接参数。

例如:import ,代码中使用这个Framework格式的库文件,在生成目标文件时,会自动在目标文件的Mach-O中,插入一个load command,格式是LC_LINKER_OPTION,存储链接器参数-framework

创建Framework

准备的文件

第一步:由TestExample.m生成TestExample.o

clang -x objective-c -target x86_64-apple-macos10.15.7 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk -c TestExample.m -o TestExample.o 
第一步:初始状态.png

第二步:将TestExample.o文件格式去掉.o,变成文本编辑文稿

第二步:将.o文件格式移除.png

第三步:新建一个文件夹,改为TestExample.framework

  1. 在文件夹内新建Headers文件夹
  2. 将TestExample.h移动到Headers
  3. 然后将libTestExample.a文件去掉后缀名,改为TestExample
  4. 像正式项目一样,将TestExample.framework移动到Framework文件夹下
    第三步:模仿静态库创建framework.png

第四步:将test.m编译成test.o

test.m中调用了TestExample.h

clang -x objective-c -target x86_64-apple-macos10.15.7 -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

第五步:将test.o链接framework,连接成可执行文件

clang -target x86_64-apple-macos10.15.7 -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk -F./Frameworks -framework TestExample test.o -o test
  • -F:指定Framework所在目录Framework Search Paths
  • -framework:指定链接Framework的名称Other Linker Flags -framework AFNetworking

第六步:LLDB环境验证。

第六步:LLDB验证成功.png

Framework链接成功的三要素:

  • -I:指定头文件路径Header Search Paths
  • -F:指定Framework所在目录Framework Search Paths
  • -framework:指定链接Framework的名称Other Linker Flags -framework AFNetworking

Shell脚本初探

第一步:新建一个shell脚本.png

可以利用shell脚本,避免文件夹的来回切换。

shell执行成功.png
SYSROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk
FILE_NAME=test
HEADER_SEARCH_PATH=./StaticLibrary

echo "第一步-----开始编译test.m"

clang -x objective-c \
-target x86_64-apple-macos11.1 \
-fobjc-arc \
-isysroot $SYSROOT \
-I${HEADER_SEARCH_PATH} \
-c ${FILE_NAME}.m -o ${FILE_NAME}.o

echo "第二步-----开始进入StaticLibrary,将TestExample.m编译成TestExample.o"
pushd ./StaticLibrary

clang -x objective-c \
-target x86_64-apple-macos11.1 \
-fobjc-arc \
-isysroot $SYSROOT \
-c TestExample.m -o TestExample.o

echo "第三步-----将TestExample.o修改为libTestExample.a"
ar -rc libTestExample.a TestExample.o

echo "第四步-----退出StaticLibrary,开始test.o链接成test"
popd

clang -target x86_64-apple-macos11.1 \
-fobjc-arc \
-isysroot $SYSROOT \
-L./StaticLibrary \
-lTestExample \
${FILE_NAME}.o -o ${FILE_NAME}

dead_strip与静态库

一、探究死代码剥离的现象

链接库文件的过程中,默认设置为-noall_load,符合剥离条件的代码全部剥离。

第一步:准备探究死代码剥离现象.png

首先在test.m中引入#import "TestExample.h",但是注释掉对象实例化的代码。利用上面的shell脚本,生成可执行文件。

第二步:生成可执行文件test.png

使用objdump --macho -d test命令,查看__TEXT代码段信息。

第三步:在可执行文件test的__TEXT段中并没有lg_test方法.png

test.m的注释取消掉,也就是在test.m中使用TestExample类,再把上面的流程重新走一遍。

再次执行时可执行文件中__TEXT有lg_test方法.png

再次执行发现可执行文件中包含了lg_test

两次对比,验证了clang在链接过程中,默认设置为-noall_load

二、实际项目中出现问题的场景

依赖库中存在分类时,如果Other Linker Flags使用默认的-noall_load,在主项目中调用依赖库的分类方法,会发生什么?

新建一个主项目,给主项目添加workspace。

打开主项目 -> File -> Save as workspace

再将依赖库添加到workspace进行链接。

打开刚生成的XXX.xcworkspace -> 关掉编辑器中的所有文件 -> 点击左下角加号 -> Add File to "XXX" -> 选择想要添加的库项目。

将静态库添加到主项目中。默认需要先编译静态库,再在主项目中使用静态库,需要在两个target之间来回切换。


将库嵌入方式改为Do Not Embed.png

能否实现在编译主项目的同时编译静态库呢?

将静态库的framework以 Embed & Sign模式添加到主项目即可。

主项目的target -> General -> Frameworks, Libraries, and Embedded Content ->加号添加或者把framework拖进来

  • Do Not Embed:不会将Framework拷贝到IPA包里。用于静态库
  • 项目使用的Framework是静态库,链接时静态库的代码和符号会跟App进行合并,故此不需要将Framework拷贝到IPA包里
  • Embed & Sign:将Framework拷贝到IPA包里,并进行签名操作。用于动态库
  • Embed Without Signing:如果Framework已有正确签名,可以使用此项

在主项目中调用依赖库的分类方法。


分类运行时加载场景会出现方法被剥离引起奔溃.png

结果:直接运行会发生奔溃,奔溃的原因是找不到方法,因为分类方法是运行时查找的,链接器默认是-noall_load,在库链接时,会把无用的代码剥离掉,运行时就会出现找不到方法,发生奔溃。

解决办法:

在项目中使用-all_load,不剥离任何代码,那肯定可以调用成功。但是这样就无法享受到编译器为我们准备的剥离功能。我们希望的是,按我们的意愿来剥离库,所以-all_load不是最优解。

如何指定库强制加载?

在主项目中创建XCConfig,将XCConfig配置到Tatget上,然后写入以下代码:

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

这段代码的作用是通过Xlinkerclang告诉链接器,让LGStaticFramework这个库强行加载,不要剥离。

链接库文件时的配置.png

  • -noall_load:默认值,符合剥离条件的代码全部剥离
  • -all_load:不要剥离任何代码
  • -ObjCOC代码不要剥离
  • -force_load:指定某个静态库不要剥离代码,参数后面需要拼接静态库路径

三、dead_strip死代码剥离

-noall_loadXcodeDead Code Stripping配置项,完全不是一个东西。

  • 库链接配置决定,剥离的规则,在ld中有-noall_load-all_load-ObjC-force_load四种参数的配置。只有在链接对象是一个静态库时,才会起效。
  • 在ld中指定-dead_strip参数,是编辑器根据我们的配置,自动的做死代码剥离。
    死代码剥离的配置.png

Link-Time Optimization

多个.o文件链接成可执行文件,和.o链接静态库有一些差异:

  • .o链接成可执行文件:先将多个.o文件合并成一个大的.o文件,再去链接成可执行文件。先组合再链接,故此Dead Code Stripping的优化无法生效
  • .o链接静态库:相当于.o直接使用静态库。先Dead Code Stripping再使用

此时可以使用链接器提供的另一个参数,LTO(Link-Time Optimization)链接时间优化

使用Monolothic选项,在多个.o文件链接时进行优化,此优化在Dead Code Stripping之后触发

你可能感兴趣的:(iOS开发进阶四:静态库)