iOS 开发中的『库』(一)

看文章之前,你可以看下下面几个问题,如果你都会了,或许可以不看。

  • .framework 是什么?怎么制作?
  • 谈一谈自己对动态库和静态库的理解。
  • 在项目中如何使用动态framework的 APP ?使用了动态framework 的 APP 能上架 Appstore 么?
  • 可以通过 framework 的方式实现 app 的热修复么?

我是前言

  • 最近发现很多人分不清 『.framework && .a 』、『动态库 && 静态库』、『.tbd && .dylib』这几个东西。甚至, 还有人一直以误为 framework 就是动态库!!鉴于网上许多文章都表述的含糊不清,再加上很多文章都比较老了,所以今天写点东西总结一下。

  • 首先,看文章之前,你稍微了解这么几个东西:编译过程、内存分区。下面开始!


理论篇

动态库 VS. 静态库

Static frameworks are linked at compile time. Dynamic frameworks are linked at runtime

  • 首先你得搞清楚,这两个东西都是编译好的二进制文件。就是用法不同而已。为什么要分为动态和静态两种库呢?先看下图:
静态库
动态库
  • 我们可以很清楚的看到:

    • 对于静态库而言,在编译链接的时候,会将静态库所有文件都添加到 目标 app 可执行文件中,并在程序运行之后,静态库app 可执行文件 一起被加载到同一块代码区中。
      • app 可执行文件: 这个目标 app 可执行文件就是 ipa解压缩后,再显示的包内容里面与app同名的文件。
    • 对于动态库而言,在编译链接的时候,只会将动态库被引用的头文件添加到目标** app 可执行文件,区别于静态库动态库** 是在程序运行的时候被添加另外一块内存区域。
  • 下面看下苹果的官方文档中有两句对动态库静态库的解释。
    - A better approach is for an app to load code into its address space when it’s actually needed, either at launch time or at runtime. The type of library that provides this flexibility is called dynamic library.

     - **动态库**:可以在 **运行 or 启动** 的时候加载到内存中,加载到一块**独立的于 app ** 的内存地址中
    
     - When an app is launched, the app’s code—which includes the code of the static libraries it was linked with—is loaded into the app’s address space.Applications with large executables suffer from slow launch times and large memory footprints
    
     - **静态库**:当程序在启动的时候,会将 app 的代码(包括静态库的代码)一起在加载到 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

Embedded frameworks are placed within an app’s sandbox and are only available to that app. System frameworks are stored at the system-level and are available to all apps.

  • OK,前面说了那么多,那么如果我们自己开发了一个动态framework 怎么把它复制到 dyld 的共享缓存 里面呢?

  • 一般来说,用正常的方式是不能滴,苹果也不允许你这么做。(当然不排除一些搞逆向的大神通过一些 hack 手段达到目的)

  • 那么,我们应该如何开发并使用我们自己开发的 动态framework 呢?

  • 那就是 Embedded Binaries。

  • Embedded 的意思是嵌入,但是这个嵌入并不是嵌入 app 可执行文件,而是嵌入 app 的 bundle 文件。当一个 app 通过 Embedded 的方式嵌入一个 app 后,在打包之后解压 ipa 可以在包内看到一个 framework 的文件夹,下面都是与这个应用相关的动态framework。在 Xcode 可以在这里设置,图中红色部分:

Embedded && Link
  • 那么问题又来了,下面的 linded feameworks and libraries 又是什么呢?
  • 首先在 linded feameworks and libraries 这个下面我们可以连接系统的动态库、自己开发的静态库、自己开发的动态库。对于这里的静态库而言,会在编译链接阶段连接到app可执行文件中,而对这里的动态库而言,虽然不会链接到app可执行文件中,但是会在启动的时候就去加载这里设置的所有动态库。(ps.理论上应该是这样,但是在我实际测试中似乎加载不加载都和这个没关系。可能我的姿势不对。😂)
  • 如果你不想在启动的时候加载动态库,可以在 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 的热修复

  • 由于 Apple 不希望开发者绕过 App Store 来更新 app,因此只有对于不需要上架的应用,才能以 framework 的方式实现 app 的更新。
  • 但是理论上只要保持签名一致,在 dlopen 没有被禁止的情况下应该是行的通的。(因为没有去实践,只能这样 YY 了。)
  • 但是不论是哪种方式都得保证 服务器上的 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;
    
    // Check if new bundle exists
    if (![manager fileExistsAtPath:bundlePath] && loadDocument) {
        bundlePath = [documentDirectory stringByAppendingPathComponent:[fileName stringByAppendingString:@".framework"]];
    }

  • 再加载 framework
    // Load bundle
    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);
        
    }
  • 加载类并做事情
    // Load class
    Class PublicAPIClass = NSClassFromString(@"PublicAPI");
    if (!PublicAPIClass) {
        NSLog(@"Unable to load class");
        
    }
    
    NSObject *publicAPIObject = [PublicAPIClass new];
    [publicAPIObject performSelector:@selector(mainViewController)];

番外篇

关于lipo

<a name="lipo"/>

$ lipo -info /Debug-iphoneos/Someframework.framwork/Someframework
# Architectures in the fat file: Someframework are: armv7 armv7s arm64 
# 合并
$ lipo –create a.framework b.framework –output output.framework
#拆分
$ lipo –create a.framework -thin armv7 -output a-output-armv7.framework

<a name="build"/>

从源代码到app

当我们点击了 build 之后,做了什么事情呢?

  • 预处理(Pre-process):把宏替换,删除注释,展开头文件,产生 .i 文件。
  • 编译(Compliling):把之前的 .i 文件转换成汇编语言,产生 .s文件。
  • 汇编(Asembly):把汇编语言文件转换为机器码文件,产生 .o 文件。
  • 链接(Link):对.o文件中的对于其他的库的引用的地方进行引用,生成最后的可执行文件(同时也包括多个 .o 文件进行 link)。

ld && libtool

  • ld :用于产生可执行文件。
  • libtool:产生 lib 的工具。

Build phases && Build rules && Build settings

  • Build phases: 主要是用来控制从源文件到可执行文件的整个过程的,所以应该说是面向源文件的,包括编译哪些文件,以及在编译过程中执行一些自定义的脚本什么的。
  • Build rules: 主要是用来控制如何编译某种类型的源文件的,假如说想对某种类型的原文件进行特定的编译,那么就应该在这里进行编辑了。同时这里也会大量的运用一些 xcode 中的环境变量,完整的官方文档在这里:Build Settings Reference
  • Build settings:则是对编译工作的细节进行设定,在这个窗口里可以看见大量的设置选项,从编译到打包再到代码签名都有,这里要注意 settings 的 section 分类,同时一般通过右侧的 inspector 就可以很好的理解选项的意义了。

谈谈 Mach-O

Mach-O
  • 在制作 framework 的时候需要选择这个 Mach-O Type.
  • 为Mach Object文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性,并提升了符号表中信息的访问速度。

参考资料


后记

@我就叫Sunny怎么了 提出的问题。

问题

更多

工作之余,写了点笔记,如果需要可以在我的 GitHub 看。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • .framework是什么?怎么制作? ...
    King_Liu阅读 306评论 0 1
  • 静态库与动态库的区别 首先来看什么是库,库(Library)说白了就是一段编译好的二进制代码,加上头文件就可以供别...
    吃瓜群众呀阅读 11,882评论 3 42
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,520评论 25 707
  • 仅以方便自己查阅记录前言1.静态库和动态库有什么异同?静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗...
    190CM阅读 4,178评论 0 4
  • 说好的台风到现在都不见,也许台风也是一个路痴(迷路了)。 晚饭买了两碗饭,一个菜。阿姨还送了一个青瓜。——好饱啊。...
    逆风追梦人阅读 260评论 0 0