iOS 进阶 - 内存管理(二) -- 内存管理方案

思维导图

iOS 进阶 - 内存管理(二) -- 内存管理方案

概述

iOS 进阶 - 内存管理(二) -- 内存管理方案

  • 内存布局(iOS 系统下的内存布局是什么样的?)
  • 内存管理方案(iOS操作系统是怎么对内存进行管理的?)
  • 数据结构(更好的讲述内存管理方案相关的问题,就要明白他们的数据结构)
  • ARC&MRC(什么是ARC,什么是MRC,他们的区别以及各自实现的机制、原理)
  • 引用计数机制(什么是引用计数机制?内存是怎样管理的?)
  • 弱引用表(我们声明weak的一个变量,为什么在内存释放的时候,weak指针会自动置为nil?弱引用变量内存是怎么管理的?)
  • 自动释放池(AutoReleasePool)的实现机制和原理是怎样的?
  • 循环引用(常考点和易错点,考察对内存管理理解的深度)

内存管理方案

iOS操作系统是怎么对内存进行管理的?

iOS操作系统是针对不同场景,会提供不同的内存管理方案,有以下几种方案

  1. TaggedPointer
    对一些小对象,如NSNumber等,采用的是TaggedPointer这种内存管理方案
  2. NONPOINTER_ISA
    对于64位架构下的iOS应用程序采用的是NONPOINTER_ISA这种内存管理方案。在64位架构下,ISA这个指针本身是占64个bit位的,但其实有32位或者40位就够用了,剩余的bit位其实是浪费的,苹果为了提高内存的利用率,在ISA剩余的这些bit位当中,存储了一些关于内存管理方面的相关内容,这个叫非指针型的ISA
  3. 散列表
    是一种很复杂的结构,其中包含了引用计数表和弱引用表
NONPOINTER_ISA (非指针型的ISA)这种内存管理方案)iOS 进阶 - 内存管理(二) -- 内存管理方案

iOS 进阶 - 内存管理(二) -- 内存管理方案
在arm64架构下,ISA指针一共有64个bit位,我们逐一分析这64个bit位分别都存储了什么内容

  • 首先看0-15位:
    • 第一位是(indexed)标志位,如果这个位置是0,代表我们使用的这个ISA指针只是一个纯的ISA指针,里面内容代表当前对象的类对象的地址。若这个标志是1,代表ISA指针里面不仅存储类对象的地址,还有一些内存管理方面的数据
    • 第二位(has_assoc)表示当前对象是否有关联对象,若是0则没有,若是1代表有
    • 第三位(has_cxx_dtor)表示当前对象是否有使用到C++相关的代码
  • 剩下的33位(shiftcls)0,1的一个bit位表示当前对象的类对象的指针地址
  • 再后6位(magic),不影响内存管理的解答
  • 再后一位(weakly_referenced)标识了这个对象是否有相应的一个弱引用指针
  • 再后一位(deallocating)标识当前对象是否正在进行dealloc操作
  • 再后一位(has_siderable_rc)标识当前ISA指针中存储的引用计数是否达到了上线,若达到了,需要外挂一个sidetable这样的数据结构来存储相关的引用计数内容,也就是我们接下来要了解的散列表。
  • 剩余的(extra_rc)代表的就是额外的引用计数,当引用计数值很小的时候,会存在ISA指针中,当大的时候,会有单独的引用计数表去存储。

通过NONPOINTER_ISA 64个bit位的分析,可以看出,关于内存管理不仅仅是散列表,其实还有ISA部分的extra_rc来存储相关的引用计数值。

散列表方式(关于散列表这种内存管理方案)

散列表的方案在源码中是通过Side Tables()结构来实现,Side Tables()结构是:
iOS 进阶 - 内存管理(二) -- 内存管理方案Side Tables()结构下挂了很多Side Table这样的数据结构,这些结构在不同的架构下有不同的个数,例如在非嵌入式系统下面,一共有64个Side Table表
Side Tables()实际上是一个哈希表。可以通过一个对象指针来具体找到对应的引用计数标或弱引用表在哪一张Side Table中。

iOS 进阶 - 内存管理(二) -- 内存管理方案Side Table结构下包含了三个元素

  1. 自旋锁
  2. 引用计数表
  3. 弱引用表

面试过程中会针对引用计数表和弱引用表提出一些相关技术问题。也会有一些涉及到自旋锁的相关面试问题,不过涉及到一些多线程的和资源竞争方面相关的问题。

为什么不是一个Side Table来实现,而是由多个Side Table共同组成一个Side Tables()这样一个数据结构?iOS 进阶 - 内存管理(二) -- 内存管理方案

假如只有一张Side Table,相当于我们在内存当中分配的所有对象的引用计数或者说弱引用都放到了一张大表中,如果要操作某个对象的引用计数值进行修改(进行+1或者-1的操作),由于所有的对象可能是在不同的线程中分配创建的(包括调用他们的return或者release等方法,也可能是在不同线程里面进行操作的),那么对这张表操作时就需要进行加锁处理,来保证数据访问的安全,这样就存在了效率问题。
假如用户的内存空间一共有4GB,我们可能分配出成千上百万个内存对象,如果每一个对象在进行引用计数改变时,都操作这张表,很显然就存在了效率问题。当对象A操作时,因为加了锁,下一个对象就要等当前对象操作完之后,将锁释放后,B才能操作。

系统为了解决这样的效率问题,引用了分离锁的技术方案。
iOS 进阶 - 内存管理(二) -- 内存管理方案分离锁:可以把内存对象所对应的引用计数表分拆成多个部分,假设分拆成8个,需要对8个表分别加锁。
假如对象A在表1中,对象B在表2中,当A和B同时进行引用计数操作时,可以并发操作。
但如果只有一张表就只能按顺序操作,分离锁可以提高访问效率。

如何实现快速分流

如何通过一个对象指针,如何快速定位到它属于哪张Side Table表?
快速分流:Side Tables本质是张Hash表,这张Hash表中,可能有64张具体的Side Table,存储不同对象的引用计数表和弱引用表。
Hash表:哈希查找、哈希算法
iOS 进阶 - 内存管理(二) -- 内存管理方案对象指针可以作为一个key,经过Hash函数的一个运算,会计算出一个值Value,来决定出这个对象所对应的Side Table是哪张,或者说在数组的位置索引是哪个。

Hash查找的过程
iOS 进阶 - 内存管理(二) -- 内存管理方案
假如给定的值是对象内存地址,目标值是Side Table结构(数组)下标索引。

  • ptr是对象内存指针地址
  • 通过哈希函数f,把指针ptr作为函数f的参数
  • 经过函数f的运算,可以得出数组的下标索引值index

哈希函数f对于Side Table具体的情况来讲,实际表达式如图所示f(ptr) = (unitptr_t)ptr % array.count

  • 通过对象的内存地址,来和Side Table这个数组的个数进行取余运算
  • 计算出对象指针所对应的引用技术表或者弱引用表是在哪一张Side Table中

为什么要通过Hash查找

  • 可以提高查找效率,存储时通过Hash进行存储,假如数组是8,假如内存地址是1,取余就是1,我们就把对象存储在第一个位置,当访问对象时,也不用对数组遍历并比较指针值,只需要也通过这个函数进行运算,找到第一个索引位置,直接取出内容。
  • 这样就不涉及遍历操作了,查找效率比较高
  • 内存地址的分布是均匀分布,我们可以称这个hash函数为均匀散列函数