开发者社区 > 博文 > iOS内存管理布局及管理方案-理论篇
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

iOS内存管理布局及管理方案-理论篇

  • 京东零售技术
  • 2019-11-27
  • IP归属:北京
  • 88760浏览

    苹果设备备受欢迎的背后离不开iOS优秀的内存管理机制,那iOS的内存布局及管理方案是怎样的呢?我们一起研究下。

    内存管理分为五大块


    栈区(stack):线性结构,内存连续,系统自己管理内存,程序运行记录,每个线程,也就是每个执行序列各有一个(看crash log最容易理解),都是编译的时候能确定好的,还有一个特点就是这里面的数据可以不用指针,也不会丢。

    堆区(heap):链式结构,内存不连续,最灵活的内存区,用途多多,动态分配和释放,编译时不能提前确定,我们的Objective-C对象都是这么来的,都存在这里,通常堆中的对象都是以指针来访问的,指针从线程栈中来,但不独属于某个线程,堆也是对复杂的运行时处理的基础支持,还有就是ARC还是MRC、“谁分配谁释放”说的都是堆上对象的管理。

    静态区(全局区)(bss):初始化数据,简单理解就是有初始值的变量、常量。

    常量区(data):未初始化数据,只声明未给值的变量,运行前统统为0,之所以单独分出来,是出于性能的考虑,因为这些东西都是0,没必要放在程序包里,也不用copy。

    代码区(text):最静态的,就是只读的东西,存储代码。


    iOS内存管理方案有三种


    我们详细看下每种方案的实现及存在的意义。


    一.tagged pointer

    没有这种管理机制会引起内存浪费,为什么呢?我们来看下,假设我们要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,那么它所占用的内存是与CPU的位数有关,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。

    所以一个普通的iOS程序,如果没有Tagged Pointer对象,从32位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber、NSDate一类的对象所占用的内存会翻倍。如下图所示: 


    1.png


    我们再来看看效率上的问题,为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。

    为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。

    所以我们可以将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer对象之后,64位CPU下NSNumber的内存图变成了以下这样:


    1.png


    当8字节可以承载用于表示的数值时,系统就会以Tagged Pointer的方式生成指针,如果8字节承载不了时,则又用以前的方式来生成普通的指针。以上是关于Tag Pointer的存储细节。


    Tagged Pointer的特点:

    1. 我们也可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate, 当然NSString小于60字节的也可以运用了该手段

    2. Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已,因为他没有isa指针。所以,它的内存并不存储在堆中,也不需要malloc和free。

    3. 在内存读取上有着3倍的效率,创建时比以前快106倍。

    由此可见,苹果引入Tagged Pointer,不但减少了64位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。


    二、Non-pointer iSA--非指针型iSA

    在64位系统上只需要32位来储存内存地址,而剩下的32位就可以用来做其他的内存管理

    non_pointer iSA 的判断条件:

    1 : 包含swift代码;

    2:sdk版本低于10.11;

    3:runtime读取image时发现这个image包含__objc_rawi sa段;

    4:开发者自己添加了OBJC_DISABLE_NONPOINTER_ISA=YES到环境变量中;

    5:某些不能使用Non-pointer的类,GCD等;

    6:父类关闭。


    三、SideTables,RefcountMap,weak_table_t 

    为了管理所有对象的引用计数和weak指针,苹果创建了一个全局的SideTables,虽然名字后面有个"s"不过他其实是一个全局的Hash表,里面的内容装的都是SideTable结构体而已。它使用对象的内存地址当它的key。管理引用计数和weak指针就靠它了。

    因为对象引用计数相关操作应该是原子性的。不然如果多个线程同时去写一个对象的引用计数,那就会造成数据错乱,失去了内存管理的意义。同时又因为内存中对象的数量是非常非常庞大的需要非常频繁的操作SideTables,所以不能对整个Hash表加锁。苹果采用了分离锁技术。

    下边是SideTabel的定义:

    SideTable

       struct SideTable {

         //锁

         spinlock_t slock;

         //强引用相关

         RefcountMap refcnts;

         //弱引用相关

         weak_table_t weak_table;

         ...

         }

    当我们通过SideTables[key]来得到SideTable的时候,SideTable的结构如下:

     1、一把自旋锁。spinlock_t slock;

    自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。

    它的作用是在操作引用技术的时候对SideTable加锁,避免数据错误。

    苹果在对锁的选择上可以说是精益求精。苹果知道对于引用计数的操作其实是非常快的。所以选择了虽然不是那么高级但是确实效率高的自旋锁


    2、引用计数器 RefcountMap *refcnts;

    对象具体的引用计数数量是记录在这里的。

    这里注意RefcountMap其实是个C++的Map。为什么Hash以后还需要个Map呢?因为内存中对象的数量实在是太庞大了我们通过第一个Hash表只是过滤了第一次,然后我们还需要再通过这个Map才能精确的定位到我们要找的对象的引用计数器。

    引用计数器的数据类型是:

    typedef __darwin_size_t        size_t;

    再进一步看它的定义其实是unsigned long,在32位和64位操作系统中,它分别占用32和64个bit。

    苹果经常使用bit mask技术。这里也不例外。拿32位系统为例的话,可以理解成有32个盒子排成一排横着放在你面前。盒子里可以装0或者1两个数字。我们规定最后边的盒子是低位,左边的盒子是高位。

    (1UL<<0)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动0位(就是原地不动):0b0000 0000 0000 0000 0000 0000 0000 0001

     (1UL<<1)的意思是将一个"1"放到最右侧的盒子里,然后将这个"1"向左移动1位:0b0000 0000 0000 0000 0000 0000 0000 0010

    下面来分析引用计数器(图中右侧)的结构,从低位到高位。

     (1UL<<0)????WEAKLY_REFERENCED

    表示是否有弱引用指向这个对象,如果有的话(值为1)在对象释放的时候需要把所有指向它的弱引用都变成nil(相当于其他语言的NULL),避免野指针错误。

     (1UL<<1)????DEALLOCATING

    表示对象是否正在被释放。1正在释放,0没有

     (1UL<<(WORD_BITS-1))????SIDE_TABLE_RC_PINNED

    其中WORD_BITS在32位和64位系统的时候分别等于32和64。其实这一位没啥具体意义,就是随着对象的引用计数不断变大。如果这一位都变成1了,就表示引用计数已经最大了不能再增加了。


    3、维护weak指针的结构体 weak_table_t  *weak_table;

    第一层结构体中包含两个元素。

    第一个元素weak_entry_t *weak_entries;是一个数组,上面RefcountMap是要通过find(key)来找到精确的元素的。weak_entries则是通过循环遍历来找到对应的entry。

     (上面管理引用计数器苹果使用的是Map,这里管理weak指针苹果使用的是数组,有兴趣的朋友可以思考一下为什么苹果会分别采用这两种不同的结构)

    这个是因为weak的显著的特征来决定的: 当weak对象被销毁的时候,要把所有指向该对象的指针都设为nil。

    第二个元素num_entries是用来维护保证数组始终有一个合适的size。比如数组中元素的数量超过3/4的时候将数组的大小乘以2。

    第二层weak_entry_t的结构包含3个部分

    1、referent:被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。

    2、referrers:可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。

    3、inline_referrers只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。

    上面我们介绍了苹果为了更好的内存管理使用的三种不同的内存管理方案,在内部采用了不同的数据结构以达到更高效内存检索。


    参考链接: 
    https://www.jianshu.com/p/dcbf48a733f9
    http://www.cocoachina.com/articles/13449
    http://www.cocoachina.com/articles/24119
    参考书籍:Objective-C高级编程:iOS与OS X多线程和内存管理


    共0条评论