iOS底层实现逻辑

2017-11-10 » iOS性能优化

一、AutoreleasePool 和 AutoreleasePoolPage

我们都知道 AutoreleasePool 是通过 AutoreleasePoolPage双向链表实现的,那具体的实现逻辑和自动释放时机是什么时候呢?

1、AutoreleasePool

AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage作为结点以双向链表的形式组合而成。

Runtime源码,整个链表以堆栈的形式运作。

void * objc_autoreleasePoolPush(void) {
		return AutoreleasePoolPage::push();
}

void * objc_autoreleasePoolPop(void *ctxt) {
		return AutoreleasePoolPage::pop(ctxt);
}

1、每一个指针代表一个加入到释放池的对象 或者是哨兵对象,哨兵对象是在 @autoreleasepool{} 构建的时候插入的

2、当自动释放池 pop的时候,所有哨兵对象之后的对象都会release

3、链表会在一个Page空间占满时进行增加,一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。

2、AutoreleasePoolPage

结构

class AutoreleasePoolPage
{
		magic_t const magic;
		id *next;
		pthread_t const thread;
		AutoreleasePoolPage * const parent;
		AutoreleasePoolPage *child;
		uint32_t const depth;
		uint32_t hiwat
}

1、id *next 指向了下一个能存放autorelease对象地址的区域

2、parent 父节点 指向前一个page

3、child 子节点 指向下一个page

4、POOL_BOUNDARY 是一个边界对象 nil,之前的源代码变量名是 POOL_SENTINEL哨兵对象,用来区别每个page即每个 AutoreleasePoolPage边界

AutoreleasePoolPage::push() 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址. push就是压栈的操作,先加入边界对象,然后添加person1对象,然后是person2对象…以此类推

AutoreleasePoolPage::pop(ctxt) 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY(因为是双向链表,所以可以向上寻找)

5、释放时机

关联runloop,可以看runloop的介绍,一般是即将进入休眠的时候会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()方法. 系统会根据情况从最新加入的对象一直往前清理直到遇到POOL_BOUNDARY标志 而在即将退出RunLoop时会调用objc_autoreleasePoolPop()方法释放自动释放池内对象。

了解iOS上的可执行文件和Mach-O格式

很多朋友都知道,在Windows上exe是可直接执行的文件扩展名,而在Linux(以及很多版本的Unix)系统上ELF是可直接执行的文件格式,那么在苹果的操作系统上又是怎样的呢?在iOS(和Mac OS X)上,主要的可执行文件格式是Mach-O格式。本文就关于iOS上的可执行文件和Mach-O格式做一个简要整理。

Mach-O格式是iOS系统上应用程序运行的基础,了解Mach-O的格式,对于调试、自动化测试、安全都有意义。在了解二进制文件的数据结构以后,一切就都显得没有秘密。

0. Mach与Mach-O

这里先提醒大家一下,Mach不是Mac,Mac是苹果电脑Macintosh的简称,而Mach则是一种操作系统内核。Mach内核被NeXT公司的NeXTSTEP操作系统使用。在Mach上,一种可执行的文件格是就是Mach-O(Mach Object file format)。1996年,乔布斯将NeXTSTEP带回苹果,成为了OS X的内核基础。所以虽然Mac OS X是Unix的“后代”,但所主要支持的可执行文件格式是Mach-O。

iOS是从OS X演变而来,所以同样是支持Mach-O格式的可执行文件。

1. iOS可执行文件初探

作为iOS客户端开发者,我们比较熟悉的一种文件是ipa包(iPhone Application)。但实际上这只是一个变相的zip压缩包,我们可以把一个ipa文件直接通过unzip命令解压。

解压之后,会有一个Payload目录,而Payload里则是一个.app文件,而这个实际上又是一个目录,或者说是一个完整的App Bundle。

在这个目录中,里面体积最大的文件通常就是和ipa包同名的一个二进制文件。找到它,我们用file命令来看一下这个文件的类型:

模拟器包
Mach-O 64-bit executable x86_64
真机包
Mach-O 64-bit executable arm64

可以看到不同环境根据处理器架构不一样,包支持的架构就不一样,可执行文件都是Mach-O格式。

对于一个二进制文件来讲,每个类型都可以在文件最初几个字节来标识出来,即“魔数”。比如PNG图片的最初几个字节是\211 P N G \r \n \032 \n (89 50 4E 47 0D 0A 1A 0A)。我们再来看下这个Mach-O universal binary的:

0000000 ca fe ba be 00 00 00 02 00 00 00 0c 00 00 00 09

没错,开始的4个字节是cafe babe,即“Cafe baby”。了解Java或者说class文件格式的同学可能会很熟悉,这也是.class文件开头的“魔数”,但貌似是Mach-O在更早的时候就是用了它。在OS X上,可执行文件的标识有这样几个魔数(也就是文件格式):

cafebabe

feedface

feadfacf

还有一个格式,就是以#!开头的脚本

cafebabe就是跨处理器架构的通用格式,feedface和feedfacf则分别是某一处理器架构下的Mach-O格式,脚本的就很常见了,比如#!/bin/bash开头的shell脚本。

这里注意一点是,feedface和cafebabe的字节顺序不同,我们可以用lipo把上面cafebabe的文件拆出armv7架构的,看一下开头的几个字节:

0000000 ce fa ed fe 0c 00 00 00 09 00 00 00 02 00 00 00

2. Mach-O格式

接下来我们再来看看这个Mach-O格式到底是什么样的格式。我们可以通过二进制查看工具查看这个文件的数据,结果发现,不是所有数据都是相连的,而是被分成了几个段落。

在一位叫做JOE SAVAGE的老兄发布的图片上来看,Mach-O的文件数据显现出来是这个样子的:

Hello-World-Hilbert-Visualisation-Structure.png

(图形化的Mach-O文件数据)

大家可以对数据的分布感受下。

虽然被五颜六色的标记出来,可能这还不是特别直接。再来引用苹果官方文档的示意图:

mach_o_segments.gif

(Mach-O文件格式基本结构)

从这张图上来看,Mach-O文件的数据主体可分为三大部分,分别是头部(Header)、加载命令(Load commands)、和最终的数据(Data)。

回过头来,我们再看上面那张图,也许就都明白了。黄色部分是头部、红色是加载命令、而其它部分则是被分割成Segments的数据。

3. Mach-O头部

这里,我们用otool来看下Mach-O的头部信息,得到:

      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedface      12          9  0x00          2    45       4788 0x00218085

更详细的,我们可以通过otool的V参数得到翻译版:

Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
   MH_MAGIC     ARM         V7  0x00     EXECUTE    45       4788   NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE

前面几个字段的意义,上下对比就能看懂,我这里主要说下这样几个字段:

filetype,这个可以有很多类型,静态库(.a)、单个目标文件(.o)都可以通过这个类型标识来区分。

ncmdssizeofcmds,这个cmd就是加载命令,ncmds就是加载命令的个数,而sizeofcmds就是所占的大小。

flags里包含的标记很多,比如TWOLEVEL是指符号都是两级格式的,符号自身+加上自己所在的单元,PIE标识是位置无关的。

4. 加载命令

上面头部中的数据已经说明了整个Mach-O文件的基本信息,但整个Mach-O中最重要的还要数加载命令。它说明了操作系统应当如何加载文件中的数据,对系统内核加载器和动态链接器起指导作用。一来它描述了文件中数据的具体组织结构,二来它也说明了进程启动后,对应的内存空间结构是如何组织的。

我们可以用otool -l xxx来看一个Mach-O文件的加载命令:

Load command 0
      cmd LC_SEGMENT
  cmdsize 56
  segname __PAGEZERO
   vmaddr 0x00000000
   vmsize 0x00004000
  fileoff 0
 filesize 0
  maxprot ---
 initprot ---
   nsects 0
    flags (none)
Load command 1
      cmd LC_SEGMENT
  cmdsize 736
  segname __TEXT
   vmaddr 0x00004000
   vmsize 0x00390000
  fileoff 0
 filesize 3735552
  maxprot r-x
 initprot r-x
   nsects 10
    flags (none)
Section
  sectname __text
   segname __TEXT
      addr 0x0000b0d0
      size 0x0030e7f4

上面这段是执行结果的一部分,是加载PAGE_ZERO和TEXT两个segment的load command。PAGE_ZERO是一段“空白”数据区,这段数据没有任何读写运行权限,方便捕捉总线错误(SIGBUS)。TEXT则是主体代码段,我们注意到其中的r-x,不包含w写权限,这是为了避免代码逻辑被肆意篡改。

我再提一个加载命令,LC_MAIN。这个加载指令,会声明整个程序的入口地址,保证进程启动后能够正常的开始整个应用程序的运行。

除此之外,Mach-O里还有LC_SYMTAB、LC_LOAD_DYLIB、LC_CODE_SIGNATURE等加载命令,大家可以去官方文档查找其含义。

至于Data部分,在了解了头部和加载命令后,就没什么特别可说的了。Data是最原始的编译数据,里面包含了Objective-C的类信息、常量等。

本文是对Mach-O文件格式的一个理解小结,希望能够抛砖引玉,帮助各位朋友把握可执行文件的主题脉络,进而解决各类问题。

Mac OS 及 iOS 支持的文件类型有三种

1、以#!开头的脚本文件

2、通用二进制文件 universal binary(胖二进制文件)

3、Mach-o格式文件。

胖二进制文件的结构

胖二进制文件可以看作是多个mach-o文件的聚合体。我们APP打包所得到的.app文件中就包含一个通用的胖二进制文件。其结构如图。

img

/usr/include/mach-o/fat.h,定义了各字段的含义。

img

Magic字段是我们所说的魔数(文件结构),加载器通过这个数值来判断当前文件是什么样的文件。主要是区分32位与64位。 32位是0xcafebabe,64位是0xcafebabf

nfat_arch 字段表明当前二进制文件包含了多少种不同架构的Mach-o 文件

fat_header后面跟进的是 fat_arch文件。有多少个不同架构的mach-o文件就会有多少fat__arch文件。用于说明mach-o文件的大小支持的 cpu架构及偏移等。即fat_arch和mach-o是一一对应的。

fat_arch 字段含义

cputype cpu 类型 cpusubtype 机器 标示符 offset 当前架构在这个文件的偏移 size 当前架构在文件中的大小 align 对齐方式

文件结构图

img

WX20190327-153510@2x.png

由结构图可知,apple只是将不同架构的文件并排放在一起。然后在头部添加相关描述信息而已,简单粗暴。

Mach-o 文件的结构

Mach-o文件主要三部分组成 1、header 2、loadcommands 3、data数据区 结构图如下:

img

Mach-o header

文件结构字段如图

img

相关字段含义如下

magic 魔数,用于类型判断

cputype cpu 类型

cpusubtype 机器标示符

filetype 文件类型

ncmds loadcommands的数量

sizeofcmds loadcommands的总大小

flags 动态连接器标志

reserved 保留

Mach-o load commands

该部分是mach-o文件中最重要的一个部分,紧跟header之后。

img

cmd 为command的类型

cmdsize 为所有command的大小

/usr/include/mach-o/loader.h中同时说明了cmd所包含的类型,如下图所示

img

每个类型都有对应的说明,这里就不一一翻译了。我们以LC___SEGMENT 为例举例说明。想要了解更多可以参考这篇文章. 对于加载命令是LC__SEGMENT而言,它指定了内核是如何设置新运行的进程的内存空间,在/usr/include/mach-o/loader.h也可以找到头文件.如图所示

img

由于有了LC_SEGMENT命令。对于每一个Segment,将文件中偏移量为fileOff长度为filesize的文件内容加载到虚拟地址为vmaddr的位置,长度为vmsize, 页面的权限通过initprot来初始化(比如设定读/写/执行, 段的保护级别可以动态设置最大不超过maxprot。 常见的segment有以下几个

1、__TEXT 代码段

2、__PAGEZERO 空指针陷阱

3、DATA 数据段

4、__LINKEDIT 包含需要被动态连接器使用的信息,包括符号表、字符串表、重定位项表等。

section介绍

img

img

我们可以使用otool -v -l test.out | open -f命令对mach-o文件进行转换输出的文档如下

img

所以可以发现整体的文件结构如图所示

img

一点小知识

1、在/usr/include/mach-o/loader.h文件我们可以发现32位和64位的魔法值宏定义不是理所当然的一个,而是两个例如32位的宏定义是0xfeedface0xcefaedfe。第一个是我们所熟知的,为什么会有第二个呢?这实际上数据大小端模式的体现。大端是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中;小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。之所以会有这种区别是跟计算机系统有关,具体我们就不深入,有兴趣的同学可以找相关资料看一看。

参考文章:

loadcommand 介绍

大小端模式

Mach-o

https://www.jianshu.com/p/221894fcb5d0