在接触iOS开发的时候,我们都知道“引用计数”的概念,也知道ARC和MRR,但其实这仅仅是对堆内存上对象的内存管理。用WWDC某Session里的话说,这其实只是内存管理的冰山一角。
在内存管理方面,其实iOS和其它操作系统总体上来说是大同小异的,大的框架原理基本相似,小的细节有所创新和不同。
和其它操作系统上运行的进程类似,iOS App进程的地址空间也分为代码区、数据区、栈区和堆区等。进程开始时,会把mach-o文件中的各部分,按需加载到内存当中。
而对于一般的iPhone,实际物理内存都在1G左右,对于超大的内存需求怎么办呢?其实这也是和其它操作系统一样的道理,都由系统内核维护一套虚拟内存系统。但这里需要注意的是iOS的虚存系统原则略有不同,最截然不同的地方就是当物理内存紧张情况时的处理。
当物理内存紧张时,iOS会把可以通过重新映射来加载的内容直接清理出内存,对于不可再生的数据,iOS需要App进程配合处理,向各进程发送内存警告要求配合释放内存。对于不能及时释放足够内存的,直接Kill掉进程,必要时时甚至是前台运行的App。
如上所述,iOS在外存没有交换区,没有内存页换出的过程。
在iOS App进程地址空间的各个区域中,最灵活的就要属堆区了,它为进程动态分配内存,也是我们经常和内存打交道的地方。
通常,我们会在需要新对象的时候,进行 [NSObject alloc]调用,而释放对象时需要release(ARC会自动帮你做到这些)。
而这些alloc、release方法的调用,通常最终都会走到libsystem_malloc.dylib的malloc()和free()函数这里。libsystem_malloc.dylib是iOS内核之外的一个内存库,我们App进程需要的内存,先回请求到这里,但最终libsystem_malloc.dylib也都会向iOS的系统内核发起申请,映射实际内存到App进程的地址空间上。
从苹果公开的malloc源码上来看,malloc的原理大致如下:
malloc内存分配基于malloc zone,并将内存分配按大小分为nano、tiny、small、large几种类型,申请时按需进行最适分配
malloc在首次调用时,初始化default zone,在64位情况下,会初始化default zone为nano zone,同时初始化一个scalable zone作为helper zone,nano zone负责nano大小的分配,scalable zone则负责tiny、small和large内存的分配
每次malloc时,根据传入的size参数,优先交给nano zone做分配处理,如果大小不在nano范围,则转交给helper zone处理。
(截图自http://www.tinylab.org/memory-allocation-mystery-%C2%B7-malloc-in-os-x-ios/)
下面分别对nano zone和scalable zone上分配内存的源码做简要解读(由于苹果Open source的代码是针对OS X的特定版本,具体细节可能与iOS上有所不同,如地址空间分布)。
在支持64位的条件按下,malloc优先考虑nano malloc,负责对256B以下小内存分配,单位是16B。
nano zone分配内存的地址空间范围是0x00006nnnnnnnnnnn(OSX上64位情况),将地址空间从大到小一次分为Magazine、Band和Slot几个级别。
分配过程:
当App通过free()释放内存时:malloc库会检查指针地址,如果没有问题,则以链表的形式将这些区块按大小存储起来。这些链表的头部放在meta_data数组中对应的[mag][slot]元素中。
其实从缓存获取空余内存和释放内存时都会对指向这篇内存区域的指针进行检查,如果有类似地址不对齐、未释放/多次释放、所属地址与预期的mag、slot不匹配等情况都会以报错结束。
下图是我根据个人理解梳理出来的一个关系图,图中标出了nanozone_t、meta_data_t等相关结构的关键字段画了出来(OSX)。
除了分配和释放,系统内存吃紧时,nano zone需将cache的内存区块还给系统,这主要是通过对各个slot对应的meta data上挂着的空闲链表上内存区块回收来完成。
对于超出nano大小范围或者不支持nano分配的,直接会在scalable zone(下文简称szone)上分配内存。由于szone上的内存分配比起nano分配要较为复杂,细节繁多,下面仅作简要介绍,感兴趣的同学可以直接阅读源码。
在szone上分配的内存包括tiny、small和large三大类,其中tiny和small的分配、释放过程大致相同,large类型有自己的方式管理。
而tiny、small的方式也依然遵循nano分配中的原则,新内存从系统申请并分配,free后按照大小以特定的形式缓存起来,供后续分配使用。这里的分配在region上进行,region和nano malloc里的band概念极为相似,但不同的是地址空间未必连续,而且每个region都有自己的位图等描述信息。和nano,一样每个cpu有一个magazine,除此之外还分配了一个index为-1的magazine作为后备之用。
下面是一个简图。
以tiny的情况为例,分配时:
free时:
而large的情况,malloc以页为单位申请和分配内存,不区分magazine,szone统一维护一个hash table管理已申请的内存。而且由于内存区域都比较庞大,只缓存总量2G的区块,分为16个元素,每个最大为128M。large相关的结构相对简单,就不特意画图了。
综上,iOS内存管理和malloc库的源码整理到此。如果发现分析得有纰缪的地方或者描述不完整的请不吝指出,欢迎随时交流。