iOS动态库、静态库及使用场景、方式

前面介绍过制作过程,这里不讲如何制作动态库、静态库。

静态库和动态库都是以二进制提供代码复用的代码库。

  • 静态库常见的是 .a
  • 动态库(共享库)常见的是 Windows 下的 .dll,Linux 下的 .so,Mac 下的 .dylib/.tbd。

特别注意平时我们经常说的Framework(in Apple) 是Cocoa/Cocoa Touch程序中使用的一种资源打包方式,可以将代码文件、头文件、资源文件、说明文档等集中在一起,方便开发者使用。也就是说我们的 Framework其实是资源打包的方式,和静态库动态库的本质是没有什么关系。

静态库和动态库的区别

首先来看什么是库,库(Library)说白了就是一段编译好的二进制代码,加上头文件就可以供别人使用。我们在和别人合作的时候,一种情况是某些代码需要给别人使用,但是我们不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件。另外一种情况是,对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要 Link 一下,不会浪费编译时间。

  • 静态库:链接时会被完整的复制到可执行文件中,所以如果两个程序都用了某个静态库,那么每个二进制可执行文件里面其实都含有这份静态库的代码。
  • 动态库: 链接时不复制,在程序启动后用动态加载,然后再决议符号,所以理论上动态库只用存在一份,好多个程序都可以动态链接到这个动态库上面,达到了节省内存(不是磁盘是内存中只有一份动态库),还有另外一个好处,由于动态库并不绑定到可执行程序上,所以我们想升级这个动态库就很容易,windows和linux上面一般插件和模块机制都是这样实现的。

动态库和静态库都是由*.o目标文件生成的。

对比一下静态和动态库的优缺点

库类型 优点 缺点
静态库 1. 目标程序没有外部依赖,直接就可以运行。2. 效率教动态库高。
1. 会使用目标程序的体积增大。
动态库 1. 不需要拷贝到目标程序中,不会影响目标程序的体积。
2. 同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库)。
3. 编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。实现动态更新
1. 动态载入会带来一部分性能损失(可以忽略不计)
2. 动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux lib not found 错误)。

iOS的动态库(被阉割的动态库)

iOS平台上规定不允许存在动态库,并且所有的 IPA 都需要经过Apple的私钥加密后才能用,基本你用了动态库也会因为签名不对无法加载,(越狱和非 APP store 除外)。于是就把开发者自己开发动态库成为了天方夜谭。

iOS8之前因为 iOS 应用都是运行在沙盒当中,不同的程序之间不能共享代码,并且iOS是单进程的,也就是某一时刻只有一个进程在运行,那么你写个共享库,给谁共享呢。同时动态下载代码又是被苹果明令禁止的,没办法发挥出动态库的优势,综上所以上动态库也就没有存在的必要了

但是后来iOS8之后,iOS有了App Extesion特性,而且Swift也诞生了。由于iOS主App需要和Extension共享代码,Swift语言机制也需要动态库,于是苹果后来提出了Embedded Framework,这种动态库允许APP和APP Extension共享代码,但是这份动态库的生命被限定在一个APP进程内。简单点可以理解为被阉割的动态库。

但是这种动态库(Embedded Framework) 和系统的 UIKit.Framework 还是有很大区别,传统的动态库是给多个进程用的,而这里的动态库(Embedded Framework)是给单个进程里面多个可执行文件用的。系统的 Framework 不需要拷贝到目标程序中,我们自己做出来的 动态库(Embedded Framework) 哪怕是动态的,最后也还是要拷贝到 App 中(App 和 Extension 的 Bundle 是共享的)。所以苹果没有直接把这种Embedded Framework称作动态库而是叫Embedded Framework。

上面提到跟Swift也有原因,在Swift的项目中如果要在项目中使用外部的代码,可选的方式只有两种,一种是把代码拷贝到工程中,另一种是用动态 Framework。使用静态库是不支持的。这个问题的根本原因主要是 Swift 的运行库没有被包含在 iOS 系统中,而是会打包进 App 中(这也是造成 Swift App 体积大的原因),静态库会导致最终的目标程序中包含重复的运行库这是苹果自家的解释)。原文如下:

The current runtime doesn't ship with the OS, so static libs would lead to multiple runtimes in the final executable. A statically linked runtime would be much more difficult to patch for compatibility with newer OS or Swift.

iOS中的Embedded Framework可以理解为独立的没有main函数的可执行文件。

基础知识

前面提到的静态库可以简单理解为一堆目标文件(.o/.obj)的打包体(并非二进制文件),而动态库可以简单理解为 一个没有main函数的可执行文件。

大学再讲编译原理的时候有两个非常重要的过程,编译和链接。编译可以理解为将源代码编译为目标文件,链接可以理解为将各种目标文件上加一些第三方库、并且和系统库链接起来为可执行文件。因为某个目标文件的符号(可以理解为变化、函数)可能来至其他目标文件,链接最为主要的就是决议符号的地址。

编译会生成目标文件,目标文件没有经过链接的过程,某些符号还没有调整过,Windows下的.obj文件,Linux下的.o文件,Unix的.out文件。

链接的过程可以简单描述如下:
假如主程序main.c 使用了 fun.c 模块的 foo函数,那么main.c在编译的过程,对于调用foo函数的指令,对于指令的目标地址暂时搁置;待到链接的时候,由链接器来填写foo函数的地址。

在决议符号的时候有如下规则:

  • 若符号来自静态库(本质就是.o 的集合包)或 .o,将其纳入链接产物,并确定符号地址。常见的符号冲突就出现在这一步。
  • 若符号来自动态库,打个标记,等启动的时候再说---交给dyld去加载和链接符号。也就是把链接的过程推迟到运行时再进行,上面讲到的静态库符号冲突就可以推迟到运行时在解决,而具体怎么解决由系统去决定。如果这两个符号表示的意思是一样(比如函数符号冲突但是函数的实现是一样的)的就没有问题。

如果要深入了解一下相关知识,建议看一下《程序员自我修养》这本书,我也只懂皮毛。

静态库和动态库依赖关系

  • 第一种静态库互相依赖,这种情况非常常见,制作静态库的时候只需要有被依赖的静态库头文件在就能编译出来。但是这就意味者你要收到告诉使用者你的依赖关系。
  • 第二种动态库依赖动态库,两个动态库是相互隔离的具有隔离性。在制作的静态库的时候需要被依赖动态库参与链接,最终具体的符号决议交给dyld来做。
  • 第三种,静态库依赖动态库,也很常见,静态库制作的时候也需要动态库参与链接,但是符号的决议交给dyld来做。
  • 第四种,动态库依赖静态库,这种情况就有点特殊。首先我们设想动态库编译的时候需要静态库参与编译,但是静态库交由dyld来做符号决议,这和我们前面说的就矛盾了啊。静态库本质是一堆.o 的打包体,首先并不是二进制可执行文件,再者你无法保证主程序把静态库参与链接共同生成二进制可执行文件。

对于第四种情况解决办法如下:

目前的编译器的解决办法是,首先我无法保证主程序是否包含静态库,再者静态库也无法被dyld加载,那么我直接把你静态库的.o 偷过来,共同组成一个新的二进制。也被称做吸附性

如果有多个动态库依赖这个静态库就会,每个动态库为了保证自己的正确性会把静态库吸附进来。然后两个库包含了同样的静态库,于是问题就出现了。

利用动态库解决相关问题

有了上面的知识就可以解决一些平时遇到的疑难杂症。

处理多个动态库依赖一个静态库问题

通过前面我们知道可执文件(主程序或者动态库)在构建的链接阶段,遇到静态库,吸附进来;遇到动态库,打标记,彼此保持独立。

正因为动态库是保持独立的,那么我们可以自定义一个动态库把依赖的静态库吸附进来。对外整体呈现的是动态库特性。其他的组件依赖我们自定义的动态库,由于隔离性的存在,不会出现问题。

这个思路在处理项目组件化的时候非常有用,尤其是在使用Swift的项目中。

利用动态库处理静态库与静态库的符号冲突问题

需要知道,在打包IPA的时候,最终静态库会被连接到最终的那个可执行文件中。所以如果多个静态库拥有了相同的符号必定会产生符号冲突。

前面讲过可以把动态库看成一个独立的没有main函数入口的可执行文件,在iOS打包中直接copy到应用程序.app目录下的Frameworks目录。既然是可执行文件那么内部编译连接过程已经完成了,要处理的连接也只有在加载的时候由操作系统的dyld自动load + link。

所以最终系统在加载动态库的时候和静态库的符号根本没有丝毫关系,进而避免了链接时产生的符号冲突。

这一点在处理一些由于底层三方库源码不能手动修改(比如boringssl与openssl)的时候,非常有用。

动态库的动态装载

目前iOS中动态更新方案有如下几种:

  • HTML 5
  • lua(wax)hotpatch
  • react native
  • framework

使用 framework 的方式来更新可以不依赖第三方库,使用原生的 OC/Swift 来开发,体验更好。由于 Apple 不希望开发者绕过 App Store 来更新 app,因此只有对于不需要上架的应用,才能以 framework 的方式实现 app 的更新。

使用framework实现动态更新常用用到的一些函数如下:

  • dlfcn.h中的的方法:用于处理动态库的装载、卸载。

    • dlopen打开动态链接库;
    • dlerror返回错误;
    • dlsym获取函数名或者变量名;
    • dlclose关闭动态库;
  • Objective-C的方法: 用于动态库中对象的具体使用。

    • NSClassFromString根据名字返回类;
    • NSSelectorFromString根据名字返回方法;
    • performSelector执行方法;

注意:没有在在Linked的设置里面设置的动态库,通过dlopen的形式来打开。如果动态库在Link Framwokrs and Libraries中设置了会在应用启动的时候就会被加载。

在使用动态库对象的时候必须使用NSClassFromString的方式,使用常见对象创建的方式是不可以的。在使用dlopen打开动态库的时候注意在build settings里面设置对应的路径,其中的@executable_path/表示可执行文件所在路径,即沙盒中的.app目录,注意不要漏掉最后的/。如果你将动态库放到了沙盒中的其他目录,只需要添加对应路径的依赖就可以了。

image.png

实例代码的代码如下:

打开动态库

- (IBAction)onDlopenLoadAtPathAction1:(id)sender
{
    NSString *documentsPath = [NSString stringWithFormat:@"%@/Documents/Dylib.framework/Dylib",NSHomeDirectory()];
    [self dlopenLoadDylibWithPath:documentsPath];
}

- (void)dlopenLoadDylibWithPath:(NSString *)path
{
    libHandle = NULL;
    libHandle = dlopen([path cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);
    if (libHandle == NULL) {
        char *error = dlerror();
        NSLog(@"dlopen error: %s", error);
    } else {
        NSLog(@"dlopen load framework success.");
    }
}

使用动态库中的内容

- (IBAction)onTriggerButtonAction:(id)sender
{
    Class rootClass = NSClassFromString(@"Person");
    if (rootClass) {
        id object = [[rootClass alloc] init];
        [(Person *)object run];
    }
}

扩展阅读

编译器的工作过程
编译器
Mach-O 可执行文件
Linux下的静态库、动态库和动态加载库

推荐阅读更多精彩内容