内存管理系列—OC的内存管理方案

内存管理系列文章:数组

引言

苹果设备受欢迎的背后离不开iOS优秀的内存管理,不一样场景,系统提供了不一样的内存管理方案来节省内存和提升执行效率,大体有以下三种:bash

  • TaggedPointer (对于一些小对象,好比说NSNumber,NSString等)
  • NONPOINTER_ISA (不只仅是指针)
  • 散列表SideTables

TaggedPointer

为了节省内存和提升执行效率,苹果提出了Tagged Pointer的概念。对于 64 位程序,引入 Tagged Pointer 后,相关逻辑能减小一半的内存占用,苹果对于Tagged Pointer特色的介绍:数据结构

  • Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
  • Tagged Pointer指针的值再也不是地址了,而是真正的值。因此,实际上它再也不是一个对象了,它只是一个披着对象皮的普通变量而已。因此,它的内存并不存储在堆中,也不须要 malloc 和 free。
  • 在内存读取上有着 3 倍的效率,建立时比之前快 106 倍。

为何会出现TaggedPointer

假设咱们要存储一个 NSNumber 对象,其值是一个整数。正常状况下,若是这个整数只是一个 NSInteger 的普通变量,那么它所占用的内存是与 CPU 的位数有关,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下是占 8 个字节的。而指针类型的大小一般也是与 CPU 位数相关,一个指针所占用的内存在 32 位 CPU 下为 4 个字节,在 64 位 CPU 下也是 8 个字节。架构

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

为了存储和访问一个 NSNumber 对象,咱们须要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增长了额外的逻辑,形成运行效率上的损失,因此须要一种解决方案(TaggedPointer)来节省内存和提升执行效率。

TaggedPointer的原理

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

因此咱们能够将一个对象的指针拆成两部分,一部分直接保存数据,另外一部分做为特殊标记,表示这是一个特别的指针,不指向任何一个地址。因此,引入了Tagged Pointer对象以后,64 位 CPU 下 NSNumber 的内存图变成了如下这样: 布局

方案对比: 当NSNumber、NSDate、NSString存值很小的状况下post

  • 在没有使用TaggedPointer以前:性能

    • NSNumber等对象须要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值(须要建立OC对象)
  • 使用TaggedPointer以后:优化

    • NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中(不须要建立OC对象)
  • 当存值很大,指针不够存储数据时(超过64位),才会使用动态分配内存的方式来存储数据(建立OC对象)

  • 消息调用时,objc_msgSend 能识别TaggedPointer,好比NSNumber的intValue方法,直接从指针提取数据,节省了之前的调用开销(并且这不是真的OC对象,根本就没有isa去找方法)

demo

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSNumber *num1 = @3;
        NSNumber *num2 = @4;
        NSNumber *num3 = @5;
        // 数值太大,64位不够放,得alloc生成个对象来保存
        NSNumber *num4 = @(0xFFFFFFFFFFFFFFFF); 
        // 小数值的NSNumber对象,并非alloc出来放在堆中的对象,只是一个单纯的指针,目标值是存放在指针的地址值中
        NSLog(@"%p %p %p %p", num1, num2, num3, num4); 
        }
    }
// 打印日志
2020-03-23 16:10:30.888204+0800 04-内存管理-Tagged Pointer[6079:225288] 0x2027be5cc632c957 0x2027be5cc632ce57 0x2027be5cc632cf57 0x100512050
复制代码

说明: 猜想是iOS13以后底层多加了一层掩码,之前输出num1, num2, num3地址是0x327 0x427 0x527 ,直接能够从地址里面看到NSNumber的值

如何断定是不是TaggedPointer

断定规则:将某个对象和1进行位运算

  • iOS平台的断定位为最高有效位(第64位)
  • Mac平台的断定位为最低有效位(第1位)

断定为是【1】就是TaggedPointer,不然这就是分配到堆中的OC对象的内存地址(OC对象在内存中以16对齐,所以有效位确定是0,16 = 0x10 = 0b00010000)。

BOOL isTaggedPointer(id pointer) {
    return (long)(__bridge void *)pointer & (long)1; // Mac平台是最低有效位(第1位)
 }
int main(int argc, const char * argv[]) {
   @autoreleasepool {
    NSNumber *num3 = @5;
    NSNumber *num4 = @(0xFFFFFFFFFFFFFFFF); 
    NSLog(@"%d %d ", isTaggedPointer(num3), isTaggedPointer(num4));
 }
}
// 打印日志
2020-03-23 16:10:30.888286+0800 04-内存管理-Tagged Pointer[6079:225288] 1 0
复制代码

优势

TaggedPointer技术的好处:

  1. 存值:直接把值存到指针中,不须要再新建一个OC对象来保存(额外多分配至少16个字节)--- 省内存
  2. 取值:直接从指针中把目标值抽取出来,不须要像OC对象那样,先从类对象的方法列表中查找再调用来获取那么麻烦 --- 性能好、效率高

NONPOINTER_ISA

在arm64位下iOS操做系统,Objective-C对象的isa区域再也不只是一个指针,在64位架构下的isa指针是64bit位,实际上33位就可以表示类对象(或元类对象)的地址,为了提供内存的利用率,在剩余的bit位当中添加了内存管理的数据内容

位域简介

有些数据在存储时并不须要占用一个完整的字节,只须要占用一个或几个二进制位就能够了。

正是基于这种考虑,C语言又提供了一种叫作位域的数据结构。在结构体定义时,咱们能够指定某个成员变量所占用的二进制位数(Bit),这就是位域。

上个demo

struct {
        char name : 1;
        char number : 1;
        char sex : 1;
 } Person;
复制代码

简单总结:

  • “:1”表明只占1位的意思,这里声明的这3个成员就各占1bit,共3bit,因此这个结构体只须要用到3bit的内存,这样系统只须要分配1个字节就够用了(内存分配至少也得1个字节)
  • 结构体定义的顺序,在内存里面对应的字节顺序是从右往左的
// 0b00000 0    0    0
           ↓    ↓    ↓
          sex number name
复制代码

共同体union简介

  • union和struct区别
  1. 内存占用方式:与结构体不一样的是,共用体的全部成员占用同一段内存,修改一个成员会影响其他成员。可是结构体的各个成员会占不一样的内存
  2. 内存大小:结构体占用的内存大于等于全部成员占用的内存总和(成员之间可能存在缝隙),共用体占用的内存等于最长的成员占用的内存
  • **union的使用 **
union {
     int  number; // 占4字节
     float age;  // 占8字节
   } person; // 以最大的那一个成员的内存来分配,因此共同体占8字节
    
   test.number = 3;
   test.age = 20;
复制代码

猜想下number的结果:

此时再次访问 test.number 就再也不是3,而是20了,由于这两个成员共用一块内存,以前的3被覆盖了

  • union的变体
union {
        char content;
        //【这个结构体纯属摆设】自始至终只操做content,不会用到这个结构体,不影响存储
        struct {  
             char name : 1;
             char number : 1;
             char sex : 1;
        };
     
    }person
复制代码

简单总结

  1. union中的struct主要是为了提升可读性,用来讲明content里面存放的是这3个成员信息,而且每个成员占1位,描述性做用。
  2. 外部操做的是content,不会对struct进行操做

isa结构

  • arm64架构以前,isa是一个普通的指针,存储着Class、MetaClass对象的地址
  • 从arm64架构以后,苹果对isa进行了优化,变成了一个公用体
# 只看arm64状况下
union isa_t {
    Class cls;
    uintptr_t bits;
    struct {
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
    };
};

复制代码

字段含义解释

  1. nonpointer:0,表明普通的指针,存储着Class、Meta-Class对象的内存地址。 1,表明优化过,使用位域存储更多的信息
  2. has_assoc:是否有设置过关联对象,若是没有,释放时会更快
  3. has_cxx_dtor:是否有C++的析构函数(.cxx_destruct),若是没有,释放时会更快
  4. shiftcls:存储着Class、Meta-Class对象的内存地址信息
  5. magic: 用于在调试时分辨对象是否未完成初始化
  6. weakly_referenced:是否有被弱引用指向过,若是没有,释放时会更快
  7. deallocating:对象是否正在释放
  8. extra_rc:里面存储的值是引用计数器减1
  9. has_sidetable_rc:引用计数器是否过大没法存储在isa中,若是为1,那么引用计数会存储在一个叫SideTable的类的属性中。

可是若是 extar_rc不够存储的话,就须要将引用计数存入一个叫 Side Table 的数据结构中。

散列表(SideTables)

SideTables()实际是一个哈希表,咱们能够经过对象指针,找到所对应的引用计数表或弱引用表位于哪一个SideTable表中。也就是有多个sideTable表

思考:为何不是一个大表,而是多个表

回答:若是只有一张表,全部对象的引用计数都放到一张表中,则若是在修改某个对象的引用计数的时候,因为对象可能在不一样线程中被操做,则须要对表进行加锁,这样一来,效率就会极地。

什么是哈希表

是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它经过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,赋值和获取都避免了遍历,提升了效率

SideTable结构

底层源码结构以下:

struct SideTable {
    spinlock_t slock;//自旋锁
    RefcountMap refcnts;//引用计数表
    weak_table_t weak_table;//弱引用表
   }
复制代码

能够看到SideTable是由三部分组成

Spinlock_t自旋锁

  • 自旋锁来用来防止操做表结构时可能的竞态条件,适用于轻量访问。好比引用计数的修改
  • Spinlock_t是“忙等”的锁,对SideTable加锁,避免数据错误

引用计数表RefcountMap

引用计数表也是一个hash表,经过hash函数找到指针对应的引用计数的位置。

弱引用表weak_table_t

弱引用表也是一个hash表,经过hash函数找到对象对应的弱引用数组

底层结构

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
};
复制代码

参考文章:

相关文章
相关标签/搜索