iOS静态库中类的分类问题和符号冲突问题(Xcode other Link Flags)

原文地址

什么是可执行文件?

要理解静态库我们就得清楚最终可执行文件(.out)的生成过程了

目标文件的生成过程

当我们写的源代码 hello.c 经过上述4个步骤:预处理(Prepressing)、编译(Compilation)、汇编(Assembly)和链接(Linking)后,就生成了我们的可执行文件 a.out 了。
注意:可重定位目标文件是(.o) 而 可执行文件是 (.out),以下文章描述都遵循这种叫法。

当我们明白到什么是可执行文件后,那么再来看看究竟什么是静态库?

静态库定义:

其实一个静态库可以简单地看成一组目标文件(.o)的集合,即很多目标文件经过压缩打包后形成的一个文件。
比如我们在Linux中最常用的C语言静态库libc位于 /usr/lib/libc.a,它属于 glibc 项目的一部分。而 glibc 本身是用C语言开发的,它由成百上千个C语言源代码文件组成,也就是说,编译完成以后有相同数量的目标文件,比如输入输出有 printf.o,scanf.o;文件操作有fread.o,fwrite.o;时间日期有date.o,time.o;内存管理有malloc.o等。把这些零散的目标文件直接提供给库的使用者,很大程度上会造成文件传输、管理和组织方面的不便,于是通常人们使用 ar 压缩程序将这些目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索,就形成了libc.a这个静态库文件。

例如当我们使用如下代码的时候

#include<stdio.h>
int main(int argc, const char* argv[] )
{
   printf("Hello World");
}

我们先用编译器和汇编器将上述代码生成目标文件 hello.o。

$gcc -c hello.c

示例代码用到了 printf () 这个iO函数,而该函数的符号就位于 printf.o 中,所以我们就需要让链接器链接 printf.o,当然printf.o 也会依赖其他目标文件,而 printf.o 等文件又在 静态库 libc.a 当中,
我们就需要让链接器 (ld) 静态链接到静态库 libc.a 中去寻找示例代码中需要用到的目标文件,并生成目标文件 hello.out。

$ ld -static -e main hello.o /usr/lib/libc.a -o hello.out

我们今天的主题主要在于链接与静态库这一步中


Xcode 中配置链接器(other linker flags)

other linker flags 是 xcode 这个集成开发环境所特有的,目的是让连接器器 ld 除了默认参数外再根添加额外参数进行链接工作。

Object-C 链接特性:

The "selector not recognized" runtime exception occurs due to an issue between the implementation of standard UNIX static libraries, the linker and the dynamic nature of Objective-C. Objective-C does not define linker symbols for each function (or method, in Objective-C) - instead, linker symbols are only generated for each class. If you extend a pre-existing class with categories, the linker does not know to associate the object code of the core class implementation and the category implementation. This prevents objects created in the resulting application from responding to a selector that is defined in the category.

Object-C的链接器并不会为每个方法建立符号表,而是为每个类建立链接符号。这样的话静态库中定义了已存在的类的分类,链接器就以为这个类存在了,不会将分类和核心类代码关联(合并)起来,这样在最后可执行文件中,就会找不到分类里所定义的方法。

例如如下错误:

Snip20170613_4.png

就看 log 可以看出,是 NSString 的一个分类方法 designByOhterLinker 找不到实现了,而这个方法,确实是一个静态库里面的一个分类方法。

Snip20170613_5.png

如何解决这个问题?

三个Linker 参数:

  • -ObjC
  • -all_load
  • -force_load
  • -dead_strip (8.27日更新)
-ObjC :

This flag causes the linker to load every object file in the library that defines an Objective-C class or category. While this option will typically result in a larger executable (due to additional object code loaded into the application), it will allow the successful creation of effective Objective-C static libraries that contain categories on existing classes.

加入这个参数后,链接器会将静态库中的每个类和分类加载到最后的可执行文件,当然,这个参数会导致可执行文件比较大,原因是加载了更多的额外对象的代码到可执行文件当中去,但是这会解决 Objec-C 中静态库中已存在的类包含的分类问题。

上面说得很清楚,-ObjC 会解决静态库中已存在的类的分类问题,那么,如果分类存在与静态库,但是类并不在静态库的这种情况,该怎么办呢?

Important: For 64-bit and iPhone OS applications, there is a linker bug that prevents -ObjC from loading objects files from static libraries that contain only categories and no classes. The workaround is to use the -allload or -forceload flags.

说得很清楚,使用-all_load 或 -force_load 就可以解决上述问题。

-all_load:

该参数把所找到的目标文件都加载到可执行文件当中去,但是这就存在一个问题了,如果两个静态库中,都使用了同一份可重定位的目标文件(这是一个很常见的问题,例如大家的目标文件都使用了用以名字 base64.o)就会发生 ld: duplicate symbol 符号冲突问题,所以不太建议使用。

-force_load:

该参数的作用跟 -all_load 其实是一样的,但是 -force_load 需要指定要进行全部加载的库文件的路径,这样的话,只要完全加载一个库文件,不影响其余库的可重定位目标文件的按需加载。

但是也有一种最头痛,就是当两个静态库中使用了相同的目标文件

Snip20170613_1.png

上图的两个上图的两个 libMyOtherStaticLibrary.a 和 libMyStaticLibrary 中的 MyClass.o 类发生了冲突
那么,这个时候有两种解决方法:

1、利用 -force_load 让链接器指定编译把其中一个静态库的目标文件,不加载另一个静态库的重复目标文件

具体做法:

Snip20170613_2.png

但是这么做有一个弊端,如果这两个静态库同时都使用到了分类(基本上都会使用吧)那么如果只让编译器加载其中一个静态库的目标文件 (-force_load),而不将另一个静态库中的分类合并加载到目标文件的话,也是会导致运行的时候导致上述的崩溃问题。但是如果 -foce_load 两个静态库,又会有符号冲突,那么,怎么办呢?


2、简单来说就是去除某个静态库中的重复目标文件,然后再打包

具体做法:
1)通过使用压缩工具命令 ar -t 去查看两个静态库文件里的目标文件那些存在冲突
如下:

-Snip20170613_7.png
Snip20170613_6.png

很明显就是 MyClass.o 这个目标文件发生符号冲突了, 其实不这样看也行,反正编译的时候 Clang 编译器就就会有符号冲突的报错,上图 Xcode 报错的那个图就是很好的例子,可以看错那些目标文件重复了。

2)将其中一个静态库中的重复目标文件去掉,然后再次打包成静态库使用

  • 首先利用 lipo 命令将其中一个iOS静态库的文件解压出来(因为iOS的静态库文件是一个将不同 CPU 架构静态库合并的一个打包文件)。
-Snip20170613_9.png

可以看出 libMyOtherStaticLibrary.a 中包含了 armv7 跟 arm64 两种架构的静态库文件

  • 分别将两种不同架构的静态库文件提取出来
-Snip20170613_10.png
-Snip20170613_11.png
  • 使用 ar 压缩工具分别将这两个不同架构的静态库文件与另一个发生冲突的静态库文件中的目标文件剔除出去。
-Snip20170613_12.png

通过上面命令看出已成功将 MyClass.o 剔除出静态库

  • 利用 lipo 将两个不同架构的静态库重新打包封装成 iOS 的静态库文件
-Snip20170613_13.png
5cac35f4-a80d-4e91-8d23-137d1d946c47.png

然后在 libMyOtherStaticLibraryOut.a 这个静态库重新放到工程当中去替换原来的 libMyOtherStaticLibrary.a

Other linker flags 只需用 -ObjC 就可以了

编译,Successful!
运行,完美!

-dead_strip (2017.8.27 更新)

参数的作用在于解决我们上面可重定位目标文件(.o)中类符号的冲突问题,如果发生了这种情况,使用该参数就是一个非常快捷的办法了,让 Clang 编译器帮助我们去除重复符号的可重定位目标文件问题。
但是使用这个参数却有一个问题,就是如果我们使用了该参数,就不能使用 -all_load 或 -force_load,因为,如果我们指定了让编译器帮我们决定哪些目标文件该被链接,哪些不被链接(-dead_strip),那么我们就不能手动的强制地让所有目标文件都进行链接了(-all_load 或 -force_load)。如果是这样的话,我们又回到最初的问题了-ObjC 会解决静态库中已存在的类的分类问题,那么,如果分类存在与静态库,但是类并不在静态库的这种情况,该怎么办呢? 显然,对于这种情况,还是得使用上面的方法


没人说你不行,是你自己说你自己不行

推荐阅读更多精彩内容

  • 静态库与动态库的区别 首先来看什么是库,库(Library)说白了就是一段编译好的二进制代码,加上头文件就可以供别...
    吃瓜群众呀阅读 8,180评论 3 39
  • 仅以方便自己查阅记录前言1.静态库和动态库有什么异同?静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗...
    190CM阅读 2,264评论 0 4
  • 作者:查汝军 七十万年前 你还是山顶洞上的一个群居时 怪咖这个地方应该是一片荒芜 到处都是土 到处都是泥泞卵石 甚...
    影视猪阅读 109评论 0 0
  • 窗被推开了 阳光在窗台上倾斜着 鸟儿在唱歌,花儿笑起来了 柳树上的嫩芽跳起了舞蹈 我还没准备好 春风就俏皮地吹乱了...
    小诗鸽阅读 50评论 1 1
  • 每天写1000字感觉真有点难写,写了这些天,每天都不知道写点什么。需要绞尽脑汁去想今天到底要写什么。不想写太多私人...
    崔宏雷阅读 454评论 0 50