[iOS研习记]——谈谈静态库与动态库

2021年11月26日 阅读数:3
这篇文章主要向大家介绍[iOS研习记]——谈谈静态库与动态库,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

[iOS研习记]——谈谈静态库与动态库

在iOS项目开发中,静态库和动态库咱们时刻都在使用,离开了库的支持,咱们将会举步维艰。好比,你要画界面,总离不开UIKit这个库吧,你要使用的各类基础数据结构,如NSString,NSArray等,也离不开Foundation这个基础库。除了官方的库外,开发中咱们也会从Github等开源社区下载第三方的开源库进行使用。通常咱们使用的第三方库或本身开发的库都采用静态库的方式使用,而系统提供的库大可能是动态库,方便多进程共享。虽然咱们每天在用库,但你对静态库和动态库真的了解么?静态库和动态库的结构是怎样的?静态库和动态库有什么区别?它们又是怎么应用的?本节博客,咱们就来聊一聊这些问题。数据结构

1. 引言

静态库与动态库有不少类似之处,固然也有不少差别。函数

从后缀名来讲,.a为后缀名的库文件是静态库,.dylib为后缀名的库文件是动态库。在iOS开发中,更多时候咱们使用的库是以.framework为后缀的。framework能够是静态库,也能够是动态库,framework自己是一种打包方式。咱们知道,咱们在编写代码时,编写的都是“源码”,而要让计算机理解这些源码,就须要编译器对源码进行编译,将其编译成计算机可理解的“机器码”,咱们每编写的一个源码文件都会被编译成一个二进制的.o文件,不管静态库仍是动态库,都是.o文件的合集。仅仅只有.o文件集合而成的库文件,对于开发者来讲是不够的,在开发时咱们不可能在没有头文件的状况下方便的调用库中的方法,所以还须要有头文件将库中提供的接口暴露出来,还有时候,可能还须要一些其余资源,好比和页面相关的库会有内置一些图片资源等,framework的功能就是将库文件,头文件,资源文件打包在一块儿,方便咱们进行使用。下图描述了framework文件与库文件的关系:工具

2. 建立一个静态库

更深刻的了解静态库以前,咱们能够先建立一个静态库体验下,首先使用Xcode建立一个新的工程,选择Framework,以下图所示:测试

建立好的framework工程模板,会生成一个和工程名相同的头文件,以及一个Resources资源文件夹,咱们能够建立新的功能类文件,例如能够新建一个命名为MyLog的类和一个MyTool的类,代码以下:优化

MyLog.hui

// MyLog.h
#import <Foundation/Foundation.h>

@interface MyLog : NSObject

+ (void)log:(NSString *)str;

@end

MyLog.m插件

#import "MyLog.h"

@implementation MyLog

+ (void)log:(NSString *)str {
    NSLog(@"MyLog:%@",str);
}

@end

MyTool.h3d

#import <Foundation/Foundation.h>

@interface MyTool : NSObject

+ (NSInteger)add:(NSInteger)a another:(NSInteger)b;

@end

MyTool.m调试

#import "MyTool.h"

@implementation MyTool

+ (NSInteger)add:(NSInteger)a another:(NSInteger)b {
    return a + b;
}

@end

在默认生成的库头文件中,引入这两个功能头文件,以下:code

#import <Foundation/Foundation.h>

//! Project version number for MyStatic.
FOUNDATION_EXPORT double MyStaticVersionNumber;

//! Project version string for MyStatic.
FOUNDATION_EXPORT const unsigned char MyStaticVersionString[];

#import "MyLog.h"
#import "MyTool.h"

在构建framewrok前,咱们能够设置此framework构建成动动态库仍是静态库,咱们先将其构建成静态库,设置编译选项的Mach-o Type为Static Library,以下:

以后,可让Xcode进行Build,以后在对应的Products文件夹中能够找到生成的framework文件,以下图所示:

若是你查看此framework文件的包内容,会发现其中有5类文件,以下:

其中,_CodeSignature中存放的是framework的签名文件。

Headers中存放的是头文件,须要注意,在编译framework工程时,要将须要暴露的头文件设置为public。

Info.plist文件是当前framework的配置文件。

Modules中的modulemap文件用来管理LLVM的module map,定义组件结构。

下面,咱们能够尝试使用下此静态库,使用Xcode新建一个名为LibDemo的iOS工程,将前面构建的MyStatic.framework文件直接拖入此工程中,在工程的编译选项中,找到Framework Search Paths和Header Search Paths中分别将此framework的路径与头文件的路径进行配置,以下图所示:

修改测试项目的ViewController.m文件以下:

#import "ViewController.h"
#import "MyStatic.framework/Headers/MyStatic.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSInteger a = 100;
    NSInteger b = 200;
    NSInteger c = [MyTool add:a another:b];
    [MyLog log:[NSString stringWithFormat:@"%ld", c]];
}


@end

运行代码,从控制台能够看到,咱们的静态库已经能够正常工做了。你可能会以为上面的头文件引入方式很是的丑陋,你彻底能够在工程中新建一个文件夹,将framework包内的头文件拷贝过来,以下图:

这样你就能够像引用工程内的头文件同样的使用framework中的功能了:

#import "ViewController.h"
#import "MyStatic.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSInteger a = 100;
    NSInteger b = 200;
    NSInteger c = [MyTool add:a another:b];
    [MyLog log:[NSString stringWithFormat:@"%ld", c]];
}

@end

3.试试动态库吧

静态库的构建和使用过程看上去很是容易,动态库应该也与其相似。咱们如今就来试试吧,使用Xcode新建一个命名为MyDylib的framework工程,将编译选项中的Mach-O Type 改成Dynamic Library,建立一些简单的测试类以下:

MyObjectOne.h

#import <Foundation/Foundation.h>

@interface MyObjectOne : NSObject

@property(copy) NSString *name;

@end

MyObjectOne.m

#import "MyObjectOne.h"

@implementation MyObjectOne

@end

MyObjectTwo.h

#import <Foundation/Foundation.h>

@interface MyObjectTwo : NSObject

@property(copy) NSString *title;

@end

MyObjectTwo.m

#import "MyObjectTwo.h"

@implementation MyObjectTwo

@end

按照一样的方式,将构建好的framework文件拖入到测试工程中,配置头文件路径,添加测试代码以下:

#import "ViewController.h"
#import "MyStatic.h"
#import "MyDylib.framework/Headers/MyDylib.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSInteger a = 100;
    NSInteger b = 200;
    NSInteger c = [MyTool add:a another:b];
    [MyLog log:[NSString stringWithFormat:@"%ld", c]];
    
    MyObjectOne *one = [[MyObjectOne alloc] init];
    one.name = @"Hello";
    [MyLog log:one.name];
}

试下编译运行,目前为止,看上去一切正常,可是当程序运行起来后会崩溃,控制台会输出以下信息:

dyld[72035]: Library not loaded: @rpath/MyDylib.framework/MyDylib

产生这个异常的缘由是没有找到动态库文件,静态库的动态库的区别出现了,怎么解决这个问题呢,其实很简单,咱们找到当前测试工程编译的产出可执行文件,点击显示包内容,在其中新建一个Frameworks的文件夹,将MyDylib.framework文件拷贝进入,以下图所示:

如今再运行工程,你会发现程序已经能够正常执行了。可是手动拷贝动态库到可执行文件的操做很是不优雅,若是真的要在项目中使用动态库,咱们更多时候会经过自动化的脚原本实现复制库文件这一步操做。

经过这些实践,咱们好像能感受到静态库和动态库之间有些什么不一样,但究竟哪里不一样呢?咱们带着疑问继续探索。

4.静态库和动态库的不一样之处

Ⅰ. 载入的方式

出现前面动态库没法找到的缘由实际上是动态库与静态库的载入方式不一样。

静态库:静态库在连接时,会被完整的复制到可执行文件中,若是有多个应用使用了相同的静态库,每一个应用的二进制文件中都会有一份完整的静态库代码。

动态库:程序在连接时,动态库并不会被复制进二进制文件,而是在程序运行时由系统动态加载到内存供程序进行调用。因为这种特性,动态库系统能够只加载一次,应用程序共享。

对于静态库动态库载入方式来讲,咱们能够再深一步。首先,静态库会被完整复制进可执行文件中,这里的完整实际上是不精准的,咱们在引入第三方库时,每每须要在工程的Other Linker Flags中配置-Objc选项,这一项的做用是对连接优化作设置。

默认状况下,静态库在连接的时候,并不会把全部代码都复制到可执行文件,其只会复制使用到的代码,这样能够减小最终应用包的体积,可是OC语言的动态性决定了并不是代码直接引用才算使用,这种链接方式常常会产生运行时的问题。

设置-Objc选项后,连接器无论代码中有没有使用,都会将OC类和其对应的Category所有加载进来。

设置-all_load选项后,连接器会把全部目标文件都加载进来,不止局限与OC文件。

设置-force_load参数能够指定强制加载某个静态库的全部目标文件,对这个静态库来讲,做用与-all_load同样。

对于动态库来讲,连接器就没有办法作这样的优化动做了,由于动态库是运行时加载的,连接器不知道哪些代码会被用到,所以从这一个方面来讲,静态库对包大小的优化貌似会比动态库更加优异,可是真的是这样么?咱们先留下个伏笔,后面再分析。

Ⅱ.文件结构不一样

静态库和动态库的本质区别仍是在于构建出的文件结构彻底不一样。可使用MachOView工具来查看库文件。

咱们先说静态库,MachOView打开的静态库结构以下:

能够看到,静态库的结构实际上是比较简单的,除了库自己的一些描述文件,符号表外,基本就是其余可执行文件的集合了,在图中能够看到,每一个可执行文件都会有一些头数据,这些头数据记录了可执行未见的名字,大小等信息。能够点开任意一个可执行文件,其中就是咱们熟悉的各类代码段,数据段等数据了:

咱们再来看动态库,其结构以下:

能够看到动态库自己就是一个可执行文件,其并非将内部的全部.o文件作简单的集合,而是一个最终连接完成的镜像文件。因为动态库是运行时进行连接的,其没法作编译时的优化,看上去可能会增长应用包的大小,可是实际应用中,咱们大多会采用-Objc参数来强制静态库连接全部OC文件,而且静态库中每个.o文件都会有一个头信息,而动态库则省略了这部分信息,所以最终对影响应用包大小这一方面来讲,并不必定静态库更优。可是有一点是肯定了,静态库是编译时连接,会节省应用启动时间。每每在作优化类的项目时,没有固定的方案,咱们要根据实际状况,选择最合适本身的方案。

5.动态库与运行时

Ⅰ. 动态库的加载

只要说到运行时,对开发者来讲就大有可为之处。首先,咱们先思考下,前面的测试工程,若是咱们不拷贝动态库文件到IPA包内的时候,为何程序运行会找不到这个库文件?又为何咱们须要将动态库拷贝进IPA包的Frameworks文件夹才行?别的文件夹不行么?

要解释上面的问题,咱们仍是要从动态库的加载原理上来看,能够用MachOView打开测试应用包的可执行文件,找到其中的Load Commands段,以下图所示:

能够看到,其中有一些动态库的加载指令,Foundation,UIKit等都是系统的动态库,咱们能够在其详情中看到详细的加载路径,以下:

对于咱们本身的MyDylib库,其加载路径以下:

能够看到,这个动态库是从@rpath/MyDylib.framework/MyDylib这个路径来加载的,这个加载路径的设置在动态库编译时就已经肯定,咱们能够看下MyDylib这个工程,在Xcode的编译配置选项中,找到Dynamic Library Install Name选项,以下所示:

这里的@rpath其实是一个环境变量,在应用工程中能够配置@rpath的值,在LibDemo工程的编译选项中搜rpath,能够看到这个环境变量的配置:

如今咱们清楚了,其实动态库文件不必定要放入Frameworks文件夹下,修改@rpath变量的路径便可修改动态库的加载路径。

对于动态库的这种加载方式,原则上,咱们能够修改此二进制文件的加载路径,也能够直接替换包内的动态库文件,实现一些逆向注入的功能,很是酷。

Ⅱ. 代码载入动态库

动态库是在运行时被加载的,咱们也能够在运行时使用代码动态的控制动态库的载入。能够将测试工程中引用MyDylib的地方所有删掉,将配置的头文件路径也去掉,咱们将这个动态库拷贝进工程的Bundle中,以下:

修改ViewController类的代码以下:

#import "ViewController.h"
#import "MyStatic.h"
#import <dlfcn.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSInteger a = 100;
    NSInteger b = 200;
    NSInteger c = [MyTool add:a another:b];
    [MyLog log:[NSString stringWithFormat:@"%ld", c]];
    
    NSString *path = [[[NSBundle mainBundle] pathForResource:@"MyDylib" ofType:@"framework"] stringByAppendingString:@"/MyDylib"];
    // 载入动态库
    void * p = dlopen([path cStringUsingEncoding:NSUTF8StringEncoding], RTLD_LAZY);
    if (p) {
        // 加载动态库成功 直接使用
        Class cls = NSClassFromString(@"MyObjectOne");
        NSObject *obj = [[cls alloc] init];
        [obj performSelector:@selector(setName:) withObject:@"Hello"];
        [MyLog log:[obj performSelector:@selector(name)]];
    }
}

@end

此时,再次编译运行此工程,若是你观察测试项目的二进制文件,里面的加载命令中已经没有了MyDylib的加载,可是程序依然能够正常的执行,dlopen函数的做用就是在运行时载入动态连接库,载入成功后,咱们能够借助OC的运行时方法,直接调用到动态库中的代码。经过这种方式,咱们实际上能够实现插件动态下载与使用,使得应用有很是高的热更新能力,可是须要注意,动态下载动态库的方式并不容许在AppStore上架,咱们只能在测试的App或企业的App中使用。

再进一步说,其实动态库的读取并不必定是从本地沙盒中,在本地调试时,你能够从任何位置读取动态库文件进行加载,这能够在本地实现不少很是酷的功能,好比Injection工具,它经过一个服务监听代码文件的变化,以后将其打包成动态库注入到程序中,再经过运行时替换类和方法,从而实现本地开发iOS项目的热更新效果,很是好用。

专一技术,懂的热爱,愿意分享,作个朋友

QQ:316045346