iOS 开发中的『库』(二)

620 阅读10分钟
原文链接: www.jianshu.com

其实这是一篇纠(da)错(lian)篇

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

  • 再谈一谈动态库和静态库。你真的知道 XXXX 和 XXX 系列。
  • 为什么使用动态库的方式来动态更新只能用在 in housedevelop 模式却不能在使用到 AppStore 上呢?
  • 动态库到底会添加到内存中几次?

我是前言

  • 其实这篇文章准备明天再写的,但是看到 @我就叫Sunny怎么了 大半夜还帮我指出问题,睡觉这个东西,感觉还是先放一放吧。
  • 像我这种严重拖延症患者来说,关于 iOS 开发中的『库』(二) ,更新速度已经史无前例了。(我好像也没什么系列啊?哈哈哈哈)
  • 主要是 iOS 开发中的『库』(一) 这篇文章确实有不少错误,需要弥补一下。
  • 当然,最需要感谢的还是 @Casa Taloyum 大神 在我写完第一篇当天晚上给我提出了一些文中的问题,并耐心的解答了我的一些小疑问。
  • ok,废话不多说,我们先开始吧。

纠错篇

静态库的处理方式

  • 对于一个静态库而言,其实已经是编译好的了,类似一个 .o 的集合(这里并没有连接,在iOS 开发中的『库』(一) 所描述的链接其实不对)。在 build 的过程中只会参与链接的过程,而这个链接的过程简单的讲就是合并,并且链接器只会将静态库中被使用的部分合并到可执行文件中去。相比较于动态库,静态库的处理起来要简单的多,具体如下图:

静态库链接过程


  • 链接器会将所有.o用到的 global symbolunresolved symbol 放入一个临时表,而且是 global symbol 是不能重复的。
  • 对于静态库的 .o , 连接器会将没有任何 symbolunresolved symbol table 的给忽略。
  • unresolved symbol 类似 extern int test(); --- .h 的 声明?
  • global symbol 类似 void test() { print("test")} -- .m 的 实现?

  • 最后,链接器会用函数的实际地址来代替函数引用。

所以,在 iOS 开发中的『库』(一) 所提到,将头文件添加到可执行文件是不正确的。

动态库的处理方式

  • 首先,对于动态库而言其实分 动态链接库动态加载库 两种的,这两个最本质的区别还是加载时间。
    • 动态链接库:在没有被加载到内存的前提下,当可执行文件被加载,动态库也随着被加载到内存中。在 Linked Framework and Libraries 设置的一些 share libraries。【随着程序启动而启动】
    • 动态加载库:当需要的时候再使用 dlopen 等通过代码或者命令的方式来加载。【在程序启动之后】
  • 但是不论是哪种动态库,相比较与静态库,动态库处理起来要棘手的多。由于动态库是动态的,所以你事先不知道某个函数的具体地址。因此动态链接器在链接函数的时候需要做大量的工作。

因为动态库在链接函数需要做大量的工作,而静态库已经实现处理好了。(“静态库的加载速度会更快一点”的原因是因为编译器预先实现了链接,而不是静态库实现的链接。- by Casa Taloyum)所以单纯的在所有都没有加载的情况下,静态库的加载速度会更快一点。而在 iOS 开发中的『库』(一) 提到的有所不妥,正确应该是,虽然动态库更加耗时,但是对于在加载过的share libraries不需要再加载的这个前提下,使用动态库可以节省一些启动时间。

  • 而实现这个动态链接是使用了 Procedure Linkage Table (PLT)。首先这个 PLT 列出了程序中每一个函数的调用,当程序开始运行,如果动态库被加载到内存中,PLT 会去寻找动态的地址并记录下来,如果每个函数都被调用过的话,下一次调用就可以通过 PLT 直接跳转了,但是和静态库还是有点区别的是,每一个函数的调用还是需要通过一张 PLT。这也正是 sunny 所说的所有静态链接做的事情都搬到运行时来做了,会导致更慢 的原因。

动态库到底在内存哪块区域的问题

  • 其实这个命题最开始就跑偏了,在和@酷酷的哀殿 等几个小伙伴讨论未果之后,在老司机@Casa Taloyum 大神 的点拨下,明白了问题出在了哪里。
  • 首先,不管是静态库、动态库,两者的区别和在内存哪个区域没有关系,最本质的区别是,一个的函数调用等在编译时候就已经确定,而动态库是动态加载的。换句话说,静态库修改了东西,整个程序需要重新编译,而对于动态库的修改而言,只需要重启 app(重置 PLT )。
  • 至于在内存的哪个区域,和是 静态库 or 动态库 没有关系。代码段、数据段这些,都是程序加载时就进入的。堆一般是文件buffer分配、对象初始化等时候用的。栈是函数出入口指针,局部常规变量用的。只要 malloc 都在堆里。具体的可以参照这里
  • 还需要提一下的是,如果是动态加载库,那么在没有加载的时候,代码段、数据段这些也是不会加载进去的。lazy load。

.a 的使用

.a是一个纯二进制文件,不能直接拿来使用,需要配合头文件、资源文件一起使用。

  • 可能他没有理解我的意思,我所说的资源文件,是如图片这类的。而他说可以不使用头文件的形式链接一个.a,好吧,我这还真不知道,但是常规使用下,使用.a 还是需要配合 头文件和资源文件一起的,所以相比之下使用 framework 更方便。

关于动态 framework 是否需要去除模拟器架构问题

  • 首先,这个理论上没有问题的,但是我自己遇到过这个坑,再加上在这里也提到了这个问题,所以保险条件下最好去掉。(感觉这是个苹果的 bug)

拓展篇

动态库动态更新问题

能否动态库的方式来动态更新AppStore上的版本呢?

  • 原本是打算国庆的时候试一试 AppStore 上到底行不行的,结果还是托@Casa Taloyum 大神 老司机的福,他已经踩过这个坑了,他的结论是:使用动态库的方式来动态更新只能用在 in housedevelop 模式却但不能在使用到 AppStore
  • 因为在上传打包的时候,苹果会对我们的代码进行一次 Code Singing,包括 app 可执行文件和所有Embedded 的动态库。因此,只要你修改了某个动态库的代码,并重新签名,那么 MD5 的哈希值就会不一样,在加载动态库的时候,苹果会检验这个 hash 值,当苹果监测到这个动态库非法时,就会造成 Crash。

所以在 iOS 开发中的『库』(一) 提到理论上是可行的这点也是不对的。

动态库到底添加几次问题

app1 和 app2 都有一个相同的 动态framework 以 Embedded 方式放入到各自的 app 中,问这个 动态framework 会加载几次?

  • 当然,不是一次,是两次。但是这不是和前面说的相违背了么,其实并不是违背,只是前面说的一次不妥当,最妥当的应该这么说:对于相同路径的动态库,系统只会加载一次。

动态库加载


关于。。。好了我先睡了。还有问题,明天再写吧。。希望写的这篇对得起 sunny 和 Casa 老司机 的指点。

番外篇

摘抄自个人笔记。

关于内存五大分区

  • BSS段:

    • BSS段( bss segment )通常是指用来存放程序中未初始化的全局变量和静态变量 的一块内存区域。
    • 这里注意一个问题:一般的书上都会说全局变量和静态变量是会自动初始化的,那么哪来的未初始化的变量呢?变量的初始化可以分为显示初始化和隐式初始化,全局变量和静态变量如果程序员自己不初始化的话的确也会被初始化,那就是不管什么类型都初始化为0,这种没有显示初始化的就 是我们这里所说的未初始化。既然都是0那么就没必要把每个0都存储起来,从而节省磁盘空间,这是BSS的主要作用
    • BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。 BSS节不包含任何数据,只是简单的维护开始和结束的地址,即总大小。以便内存区能在运行时分配并被有效地清零。BSS节在应用程序的二进制映象文件中并不存在,即不占用 磁盘空间 而只在运行的时候占用内存空间 ,所以如果全局变量和静态变量未初始化那么其可执行文件要小很多
  • 数据段(data segment)

    • 通常是指用来存放程序中已经初始化的全局变量和静态变量的一块内存区域。数据段属于静态内存分配,可以分为只读数据段读写数据段。字符串常量等,但一般都是放在只读数据段中。
  • 代码段(code segment/text segment)

    • 通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等,但一般都是放在只读数据段中 。
  • 堆(heap)

    • 堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或 缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张); 当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
  • 栈 (stack heap)

    • 栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}” 中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外, 在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值 也会被存放回栈中。由于栈的先进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

内存分区


小 Tips

  • 栈区中的变量不需要程序员管理
  • 堆区的需要程序员管理
  • 在 iOS 中堆区的内存是所有应用程序共享
  • 系统使用表级别结构来分配内存空间,所以逻辑地址和物理地址可能不一样

参考文献