iOS高级进阶系列之-库(上)静态库探索

5,305 阅读12分钟

系列文章:OC底层原理系列OC基础知识系列Swift底层探索系列iOS高级进阶系列

补充

上篇文章我们对符号有了一定的认识,这里再补充点关于符号的内容。我们有时候需要知道符号的种类,我们通过命令nm -pa 文件名查看符号

我们看到地址后面t、d、T等关键字,这些关键字就是符号种类,下面整理了一下符号种类划分

命令查找

上面用到命令-pa,这里我们来说下-pa的含义-pa其实包含两部分-p、-a,我们可以通过查看命令符号查看

  • 1.在终端输入man nm,来到name list
  • 2.向下滚动,查找-p,-a

我们可以知道-a显示所有符号表项,包括插入的使用调试器-p不排序;按符号表顺序显示

静态库

库相关知识

我们常用的库文件格式有.a、.dylib、.framework、.xcframework

  • .a:常见的静态库
  • .dylib:传统上说的动态库
  • .framework:既有动态库,又有静态库
  • .xcframework:是苹果2018年推出来的,可以将不同架构库整合到一起。好处就是模拟器,真机可以通用,上架AppStore,不需要将xcframework中的真机架构分离.framework还需要用脚本分离 我们这里主要介绍.a.framework格式的静态库

库含义

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

库的使用时机

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

什么是Link

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

  • 1.静态
  • 2.动态

什么是静态库

静态库即静态链接库:可以简单的看成一组目标文件的集合。即很多目标文件经过压缩打包后形成的文件。Windows下的 .libLinux 和 Mac 下的 .aMac独有的.framework

  • 缺点: 浪费内存磁盘空间模块更新困难

什么是动态库

与静态库相反,动态库编译时不会拷⻉到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时动态库才会被真正加载进来。格有:.framework、.dylib、.tdb

  • 缺点: 会导致一些性能损失。但是可以优化,比如延迟绑定(Lazy Binding)技术

如何生成静态库

我们实操一个.m文件,.m如下代码:

上面代码我们导入了AFN,main函数中使用了AFN的代码,并打印了manager,这就是就是一个很常用的简单的类中引用其它第三方的代码

和这部分代码放在一个文件夹里还有AFN的代码,里面包含了AFN的静态库 我们查看下libAFNetworking.a到底是什么格式

我们发现是一个文档格式,文档格式其实就是.o文件的合集,怎么验证呢?

我们需要使用ar

可以看到它是.o文件的合集

生成.o文件

我们知道编译过程先生成目标文件(也就是.o文件),之后通过链接器生成可执行文件 所以将我们最上面的.m文件生称.o文件,然后就会用到我们经常使用的clang,我们来看下clang是什么

就是C、C++和OC的编译器

下面我们通过clang将.m文件编译成.o文件

  • 1.-x制定语言/转译字符,输入后告诉终端敲回车是换行,不是执行
  • 2.制定平台
  • 3.编译成arc环境
  • 4.指定使用的SDK路径
  • 5.将.m编译成.o 但是我们发现报错了,因为我们还引入了AFN,提示我们找不到,所以我们要加入AFN
  • 1.I在指定目录寻找头文件AFN

回车后就生成了.o文件(上面AFN文件夹和test.m平级,才能这么告诉AFN位置。

生成静态库

下面我们将生成的.o文件通过链接器生成静态库

  • 1.这里不需要制定语言,直接选择架构
  • 2.选择arc环境
  • 3.由于项目中有NSLog,其导出符号,要知道其来源自Foundation,让他去我们指定的Xcode中寻找
  • 4.指定目录寻找头文件(链接的过程就是把我们重定位符号表中的符号进行重定位,这里就需要知道符号的真实位置,AFN的符号表放在了libAFNetworking.a文件里了,它需要和我们生成的test.o文件的符号表进行融合,最后生成一张新的符号表
  • 5.指定连接的库文件名称
  • 6.将.o文件输出为test文件

上面命令敲完后报错,原因是架构问题,AFN采用的架构是模拟器架构,而我们导出的架构是x86架构,不兼容报错

总结

生成静态库的三要素:

  • 1.-I在指定目录寻找头文件(在生成.o文件时)对应的是header search path
  • 2.-L指定库文件路径(.a.dylib库文件)对应的是library search path
  • 3.-l指定链接的库文件名称(.a.dylib库文件)对应的是other link flags -lAFNetworking

理解静态库为.o文件合集

创建一个类,在.m写一个OC方法,里面打印 下面我们将TestExample生成静态库(命令和上面一样)

  • 1.先生成.o文件
  • 2.下面将生成的.o文件改成lib.dylib

变成了可执行文件

  • 3.将.dylib删除掉

依然是可执行文件

  • 4.查看下文件属性
  • 5.我们让test.o链接这个libTestExample,如果链接成功了,也就证明静态库就是.o文件的合集

看到上面生成的文件是没有报错的

  • 6.进入lldb,调用file testtest进行包装,然后在运行r进行运行

我们看到运行成功了,这也就是我在test.m的输出内容

【总结】:链接成功了,也就说明静态库就是.o文件的合集,验证我们开始说的内容

我们可以通过objdump来查看文件的属性

我们看到它还是个目标文件 【思考】:上面说了静态库就是.o文件的合集,那么合并静态库就是将更多的.o文件合并到一起

合并静态库

合并静态库,用到命令libtool,我们先看下libtool的作用

就是创建librariesranlib添加或者更新一系列静态库文件格式

下面通过libtool进行静态库合并

  • 1.-static合并的为静态库
  • 2.-o输出
  • 3.libLj.a名字
  • 4.合并的静态库位置路径 静态库合并libtool会自动帮我们处理.o文件,通过上面讲解我们知道静态库合并最重要的点就是头文件的处理。这里介绍一个命令:mudule:mudule是用来专门处理.h头文件的命令,它可以将我们的头文件预先编译成二进制然后缓存到目录中,当我们在其它类引入该头文件时不用再重新编译,直接拿来用就好了(将Swift静态库会再详细讲)之所以提这个,是为了引出编译器的一个特性:Auto-Link

Auto-Link

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

Framework

  • 定义:Mac OS/iOS平台还可以使用Framework。Framework 实际上是一种打包方式,将库的二进制文件头文件有关的资源文件打包到一起方便管理和分发
  • 区别:Framework系统的UIKit.Framework还是有很大区别系统的Framework不需要拷⻉到目标程序中,我们自己做出来的Framework哪怕是动态的,最后也还是要拷⻉到App中(App和Extension的Bundle是共享的),因此苹果又把这种Framework称为EmbeddedFramework。

链接Framework

我们链接一个Framework,先准备素材

.a文件的制作在上面已经讲过了

  • 1.创建新的文件夹TestExample.framework

将上面的.h放到Headers文件下,将libTestExample.a删除.a删除lib生成红框里的文件,这个就是一个模拟的Framework

  • 2.将test.m编译成.o文件
  • 3.将.o和TestExample.framework进行链接
  • -F(directory)在指定目录寻找framework,相当于framework search path
  • -framework<framework_name> 指定链接的framework名称,相当于other link flags -framework AFNetworking

制作Framework

上面我们生成可执行文件在终端写了不少命令,每次生成可执行文件都需要写一次命令,有没有一种办法,只需要一行命令就能够生成我们需要的文件呢?答案:有,shell脚本

制作shell脚本

  • 1.创建一个shell脚本
  • 2.写shell命令 shell语言是一门解释型语言,它会一行一行解释你的语言,而不是像OC,编译好了再去执行

写sell脚本前,先看下目录结构,方便命令理解

写脚本就是将上面终端的命令粘贴复制过来,下面已经写了注释,不再多说明

  • 3.执行shell脚本

发现报错:你没有可执行权限,所以我们要给build.sh一个可执行权限

  • 4.添加可执行权限,再执行

添加可执行权限 看到通过脚本生成了我们想要的东西,完美执行。

  • 5.添加日志,在执行过程中,我们想知道脚本都干了什么事情 再次执行,就发现我们刚才输入的东西打印了出来

【注意】shell脚本为了大家更好的理解,写了说明,执行的时候需要将说明删除,否则会报错

扩展

我们在上面引入系统SDK查找路径,写了很长一大串,我们可以使用变量来进行定义(类似宏定义) 后面我们就可以使用SYSROOT代替这个路径 如果外部还有其它字符串,我们就需要使用{} 【注意】:=两端不能有空格,否则会默认为其它命令

Dead_Strip

上面我们生成了可执行文件,其中test.m文件代码如下:

我们看到我们只是引入了TestExample头文件,并没有调用。那么可执行文件中包不包含TestExample.m的代码呢? 下面我们通过命令查看下: 发现并没有TestExample的代码,下面我们将test.m注释的代码打开,再执行以下shell脚本 我们看到这次的代码变多了,而且存在TestExample.m的代码。其实可执行文件就是讲.o文件的代码合并到一起,在这里因为test.m使用了TestExample类,在链接的时候就会将TestExample静态库.o文件的代码链接过来,放在一起。 Dead_Strip默认生效的,但是这过程会有些问题

分类问题

前面文章讲过分类实在运行时动态创建的,但是我们进行可执行文件创建时,项目并未运行,这就有问题了,下面我们通过项目来讲述

  • 1.创建LjOneObject以及LjOneObject+Category分类
  • 2.在方法lj_test调用分类方法lj_test_category

上面说了分类是在运行时创建的,而Dead_Strip是在链接过程中生效,当它发现lj_test_category方法所在的分类不存在,Dead_Strip将会将这个方法脱掉。下面我们来验证下

验证Dead_Strip脱掉分类方法

项目运行我们需要加一个workspace,这个大家都很熟悉 我们这里说下workspace好处:

  • 1.可重用性。多个模块可以在多个项目中使用。节约开发和维护时间。
  • 2.节省测试时间。单独模块意味着每个模块中都可以添加测试功能。
  • 3.更好的理解模块化思想。 下面我们创建一个workspace 给这个workspace起名叫TestDeadStrip

想将上面的LjApp加入到我们的workspace中,怎么做呢? 将LjApp加入后,两个项目就会共存,此时我引入头文件,调用方法,此时就会调用分类方法 当我们运行代码发现会崩溃,告诉我们找不到分类方法lj_test_category 【原因】:就是上面我们说的,在链接的时候Dead_Strip会将分类方法脱掉,不会链接到静态库中

要解决这个问题还是要告诉我们的链接器哪些方法你不要脱,我后面会补全,下面我们就用到.xcconflg,有三种方法:

  • 1.-all_load

OTHER_LDFLAGS就是通过clang给我们的dyld传递参数的,但是clang没有-all_load,然后我们通过-Xlinker,来告诉clang,我的-all_load是传给dyld的不是传给你的

运行代码,发现正常打印出结果了 全部链接,问题就是享受不到系统对静态库的优化,此时就使用-ObjC

  • 2.-ObjC

这个就是说不要剥离OC的代码,其它的代码该怎么处理怎么处理

  • 3.-force_load

就是指定静态库不要剥离,其它的该怎么进行就怎么进行,后面跟的是静态库所在路径

写到最后

自己在探究的过程中,可能想到哪里就会去写代码,然后在写到文章里,所以有时候并没有一定连贯性,有问题了可以在下面写下来,我们多沟通交流共同进步,下篇文件会说动态库的有关知识