看文章之前,你可以看下下面几个问题,如果你都会了,或许可以不看。
- .framework 是什么?怎么制作?
- 谈一谈自己对动态库和静态库的理解。
- 在项目中如何使用动态framework的 APP ?使用了动态framework 的 APP 能上架 Appstore 么?
- 可以通过 framework 的方式实现 app 的热修复么?
动态库 VS. 静态库
首先你得搞清楚,这两个东西都是编译好的二进制文件。就是用法不同而已。**为什么要分为动态和静态两种库呢?**先看下图:
app 可执行文件
这个目标 app 可执行文件就是 ipa解压缩后,再显示的包内容里面与app同名的文件
静态库
对于静态库而言,在编译链接的时候,会将静态库的所有文件都添加到 目标 app 可执行文件中,并在程序运行之后,静态库与 app 可执行文件 一起被加载到同一块代码区中。
苹果的官方文档中有静态库的解释
- 静态库:当程序在启动的时候,会将 app 的代码(包括静态库的代码)一起在加载到 app 所处的内存地址上。
- 相比于静态库 的方案,使用动态库将花费更多的启动时间和内存消耗。还会增加可执行文件的大小。
动态库
在编译链接的时候,只会将动态库被引用的头文件添加到目标 app 可执行文件
苹果的官方文档中有对动态库的解释
- 动态库:可以在 运行 or 启动 的时候加载到内存中,加载到一块**独立的于 app ** 的内存地址中
举个例子
假设 UIKit 编译成静态库和动态库的大小都看成 1M , 加载到内存中花销 1s现在又 app1 和 app2 两个 app
app1 启动的时候, 需要花销 2s 同时内存有 2M 分配给了 app1
同样的道理 加上 app2 的启动时间和内存消耗
采用静态库的方案,一共需要花销 4s 启动时间、4M 内存大小、4M 安装包大小
对于启动和 app1 可能花费一样的时间
但是在启动 app2 的时候 不用再加载 UIKit 动态库 了。减少了 UIKit 的重复 使用问题,
一共花销 3s启动时间、3M 内存大小、4M 安装包大小。
动态库 和静态库的总结
动态库的形式 来优化系统
而很多 app 都会使用很多相同的库,如 **UIKit **、 CFNetwork 等
苹果为了加快 app 启动速度、 减少内存消耗、 减少安装包体积大小, 采用了大量 动态库的形式 来优化系统
dyld 的共享缓存
在 OS X 和 iOS 上的动态链接器使用了共享缓存,共享缓存存于 /var/db/dyld/。对于每一种架构,操作系统都有一个单独的文件,文件中包含了绝大多数的动态库,这些库都已经链接为一个文件,并且已经处理好了它们之间的符号关系
- 当加载一个 Mach-O 文件 (一个可执行文件或者一个库) 时,动态链接器首先会检查 共享缓存
看看是否存在其中,如果存在,那么就直接从共享缓存中拿出来使用 - 每一个进程都把这个共享缓存映射到了自己的地址空间中。这个方法大大优化了 OS X 和 iOS 上程序的启动时间。
两者都是由*.o目标文件链接而成。都是二进制文件,闭源。
.framework VS .a
- .a是一个纯二进制文件,不能直接拿来使用,需要配合头文件、资源文件一起使用。在 iOS 中是作为静态库的文件名后缀。
- .framework中除了有二进制文件之外还有资源文件,可以拿来直接使用。
- 在不能开发动态库的时候,其实 『.framework = .a + .h + bundle』。而当 Xcode 6
出来以后,我们可以开发动态库后**『.framework = 静态库/动态库 + .h + bundle』**
.tbd VS .dylib
- 对于静态库的后缀名是**.a**,那么动态库的后缀名是什么呢?
- 可以从 libsqlite3.dylib 这里我们可以知道 .dylib 就是动态库的文件的后缀名。
- 那么 .tbd 又是什么东西呢?其实,细心的朋友都早已发现了从 Xcode7 我们再导入系统提供的动态库的时候,不再有**.dylib了,取而代之的是.tbd**。而 .tbd 其实是一个YAML本文文件,描述了需要链接的动态库的信息。主要目的是为了减少app 的下载大小。具体细节可以看这里
小总结
- 首先,相比较与静态库和动态库,动态库在包体积、启动时间还有内存占比上都是很有优势的。
- 为了解决 .a 的文件不能直接用,还要配备 .h 和资源文件,苹果推出了一个叫做 .framework 的东西,而且还支持动态库。
Embedded VS. Linked
OK,前面说了那么多,那么如果我们自己开发了一个动态framework 怎么把它复制到 dyld 的共享缓存 里面呢?
- 一般来说,用正常的方式是不能滴,苹果也不允许你这么做。(当然不排除一些搞逆向的大神通过一些 hack 手段达到目的)
- 那么,我们应该如何开发并使用我们自己开发的 动态framework 呢?
- 那就是 Embedded Binaries。
Embedded Binaries
Embedded 的意思是嵌入,但是这个嵌入并不是嵌入 app 可执行文件,而是嵌入 app 的 bundle 文件。当一个 app 通过 Embedded 的方式嵌入一个 app 后,在打包之后解压 ipa 可以在包内看到一个 framework 的文件夹,下面都是与这个应用相关的动态framework。在 Xcode 可以在这里设置,图中红色部分:
那么问题又来了,下面的 linked feameworks and libraries 又是什么呢?
首先在 linded feameworks and libraries 这个下面我们可以连接系统的动态库、自己开发的静态库、自己开发的动态库。
- 对于这里的静态库而言,会在编译链接阶段连接到app可执行文件中,
- 而对这里的动态库而言,虽然不会链接到app可执行文件中,但是会在启动的时候就去加载这里设置的所有动态库
如果你不想在启动的时候加载动态库,可以在 linded feameworks and libraries 删除,并使用dlopen加载动态库。(dlopen 不是私有 api。)
- (void)dlopenLoad{
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.");
}
}
关于制作过程
关于如何制作,大家可以看下raywenderlich家的经典教程《How to Create a Framework for iOS 》,中文可以看这里《创建你自己的Framework》
阅读完这篇教程,我补充几点。
- 首先,framework 分为Thin and Fat Frameworks。Thin 的意思就是瘦,指的是单个架构。而 Fat
是胖,指的是多个架构。 - 要开发一个真机和模拟器都可以调试的 Frameworks 需要对Frameworks进行合并。合并命令是 lipolipo。 如果 app
要上架 appstore 在提交审核之前需要把 Frameworks 中模拟器的架构给去除掉。 - 个人理解,项目组件化或者做 SDK 的时候,最好以 framework 的形式来做。
framework 的方式实现 app 的热修复
实现大致思路
- 下载新版的 framework
- 先到 document 下寻找 framework。然后根据条件加载 bundle or document 里的 framework。
NSString *fileName = @"remote";
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = nil;
if ([paths count] != 0) {
documentDirectory = [paths objectAtIndex:0];
}
NSFileManager *manager = [NSFileManager defaultManager];
NSString *bundlePath = [[NSBundle mainBundle]
pathForResource:fileName ofType:@"framework"];
BOOL loadDocument = YES;
if (![manager fileExistsAtPath:bundlePath] && loadDocument) {
bundlePath = [documentDirectory stringByAppendingPathComponent:[fileName stringByAppendingString:@".framework"]];
}
NSError *error = nil;
NSBundle *frameworkBundle = [NSBundle bundleWithPath:bundlePath];
if (frameworkBundle && [frameworkBundle loadAndReturnError:&error]) {
NSLog(@"Load framework successfully");
}else {
NSLog(@"Failed to load framework with err: %@",error);
}
Class PublicAPIClass = NSClassFromString(@"PublicAPI");
if (!PublicAPIClass) {
NSLog(@"Unable to load class");
}
NSObject *publicAPIObject = [PublicAPIClass new];
[publicAPIObject performSelector:@selector(mainViewController)];
番外篇
从源代码到app
当我们点击了 build 之后,做了什么事情呢?
- 预处理(Pre-process):把宏替换,删除注释,展开头文件,产生 .i 文件。
- 编译(Compliling):把之前的 .i 文件转换成汇编语言,产生 .s文件。
- 汇编(Asembly):把汇编语言文件转换为机器码文件,产生 .o 文件。
- 链接(Link):对.o文件中的对于其他的库的引用的地方进行引用,生成最后的可执行文件(同时也包括多个 .o 文件进行 link)。
|