2

最近整理一些项目以前依赖留下的问题,在使用 CocoaPods 和 Carthage 的时候引出了关于静态库和动态库的思考,手动编译静态库的朋友应该知道,如果自己编译了一个静态库,这个静态库依赖了 iOS 自带的库,即使你在 Xcode 中显式指定了依赖,当你在另一个工程中使用这个静态库的时候,依旧需要显式在那个工程指定这个静态库所依赖的系统库。

举个栗子

AFNetworking 是个好东西,大家都喜欢用,在日常使用的时候,一般都是使用 CocoaPods 引入这个库,笔者前面有篇文章分析了 CocoaPods 做了什么工作,但是却没有分析 CocoaPods 为何如此设计,这里先卖个关子不讲。AFNetworking 依赖 Security.framework MobileCoreServices.framework SystemConfiguration.framework 三个系统框架,如果我们将其编译为静态库,然后这个静态库被其他工程依赖,那么其他工程依旧需要引入这三个框架。如果我们将其编译为动态库,则不需要依赖这三个框架。这个情况引起了笔者的兴趣

分析

既然问题是编译链接,那么就需要从动态库和静态库入手分析,首先,我们需要明确的是,系统提供的 framework 都是动态库的形式提供的。这很好理解,因为 UIKit 这类 framework 被使用的太频繁了,内存中完全只需要保留一份副本。这样也能减轻 App 的大小。那么直接来着手模拟一下其编译过程。

模拟静态库的编译

创建一个动态库用于模拟系统库

dynamic.h
----------
void hello();

dynamic.c
----------
#include "dynamic.h"
#include <stdio.h>

void hello() {
    printf("Hello World");
}

然后编译打包动态库

> gcc -c -o dynamic.o dynamic.c # 编译为对象文件
> gcc -shared dynamic.o -o libdynamic.so

创建一个静态库用于模拟第三方静态库

static.h
--------
void sayHello();

static.c
--------
#include "static.h"
#include "dynamic.h"

void sayHello() {
    hello();
}

然后编译静态库

> gcc -c -o static.o static.c
> ar -r libstatic.a static.o

这里大家有没有发现一个问题,静态库实际上并没有链接动态库,仅仅只是 include 了一个头文件用于编译通过。最终生成的静态库根本不知道动态库的存在。
然后创建一个带有 main 函数的程序

hello.c
-------
#include "static.h"
int main(int argc, char *argv[]) {
    sayHello();
}

然后编译

> gcc -o hello hello.c -L. -lstatic
# 直接报错没有找到 hello() 的二进制代码
Undefined symbols for architecture x86_64:
  "_hello", referenced from:
      _sayHello in libstatic.a(static.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

模拟动态库的编译

创建一个动态库用于模拟系统库

dynamic1.h
----------
void hello();

dynamic1.c
----------
#include "dynamic1.h"
#include <stdio.h>

void hello() {
    printf("Hello World");
}

然后编译打包动态库

> gcc -c -o dynamic1.o dynamic1.c # 编译为对象文件
> gcc -shared dynamic1.o -o libdynamic1.so

创建动态库用于模拟第三方动态库

dynamic2.h
--------
void sayHello();

dynamic2.c
--------
#include "dynamic2.h"
#include "dynamic1.h"

void sayHello() {
    hello();
}

然后编译打包动态库

> gcc -c -o dynamic2.o dynamic2.c # 编译为对象文件
> gcc -shared dynamic2.o -o libdynamic2.so
# 报错,显示 dynamic2.o 中没有 hello() 二进制代码
Undefined symbols for architecture x86_64:
  "_hello", referenced from:
      _sayHello in dynamic2.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
> gcc -shared dynamic2.o -o libdynamic2.so -L. -ldynamic1
# 成功,无报错

然后创建一个带有 main 函数的程序

hello.c
-------
#include "static.h"
int main(int argc, char *argv[]) {
    sayHello();
}

然后编译

> gcc -o hello hello.c -L. -ldynamic2

总结

从上面两个编译过程大家应该也能明白了,静态库实际上只是对象文件的打包,也就是说,只经过了编译过程,而没有链接过程,编译一个静态库甚至只需要满足所有函数的声明就行,而动态库虽然没有经过正规的链接,但是实际上还是通过 gcc 做了跟其他动态库的链接,动态库自身有了依赖的概念。所以不需要在工程中显式依赖了。
这里可能有朋友想要问,这种知识有什么用处?实际上这种知识在依赖管理中有用,前面说过 CocoaPods 对工程修改了很多内容,这是有原因的,因为 iOS8 之前是不存在动态库的,只存在静态库,CocoaPods 不得不对目标工程也作出修改来添加对其他库的依赖,因为静态库不知道自身依赖什么库。而 Carthage 则只支持 iOS8 和动态库,所以完全可以没有侵入性,只需要提供一个动态库,然后工程依赖这个动态库就行了。


山河永寂
2.4k 声望159 粉丝