内存管理

190 阅读8分钟

MRC 手动内存管理 和 ARC 自动内存管理

MRC

11年以前,系统是通过对象的引用计数来判断是否要销毁,需要程序员手动 retain +1 或者 release -1:创建时引用计数都为 1,被其他指针引用时,需要手动调用 [objc retain] 使对象的引用计数+1,当指针变量不再使用对象时,需要手动调用 [objc release] 来释放对象,使对象的引用计数 -1,当引用计数为 0 时,系统会销毁这个对象

总结:谁创建 谁释放 谁引用 谁管理

ARC

11年之后和iOS5引用的自动管理机制,即自动引用计数,是编译器的一种特性,和 MRC 一致 都是通过引用计数判断对象是否需要销毁,而ARC模式下不需要手动 retain release autorelease,编译器会在适当的位置插入 release autorelease

内存布局

五大分区

栈区,堆区,全局区,常量区,代码区,其实除了五大区还有内核区 和 保留区,比如一部 内存4gb 手机,3gb 给了 五大区和保留区,还有 1gb 给了内核区。内核区:系统用来进行内核处理操作的区域,保留区:预留给系统处理nil等 image.png

栈区 stack

栈是系统数据结构,对应的进程/线程是唯一的,从高地址到底地址的数据结构,地址空间以 0x7开头,栈区一般在运行时分配空间,空间大小较小,存储不够灵活,但是读写效率高,用来存储局部变量,函数的参数,由编译器来自动分配和释放。不会产生内存碎片

堆区 heap

堆是从低地址向高地址扩展的数据结构,不连续的内存区域,类似于链表,先进先出,地址空间以 0x6开头 一班在运行时进行分配,由开发者自己分配和释放,同时系统也会堆存储的内容进行回收和释放,方便灵活 数据使用广泛,但是内存需要手动管理,容易产生碎片,读取速度比栈区慢,访问堆区内存时,一般是先通过对象读取到对象所在的栈区的指针地址,然后通过指针地址访问堆区

全局区/静态区/bbs段

全局区是编译期分配的内存空间,以0x1开头,在程序运行的过程中钙数据就一直存在 ,程序结束才释放,存储未初始化的全局变量/静态变量,一旦被初始化 就会被回收,并将数据存储到常量区

常量区/data

常量区是专门存放常量的,只有程序结束才会被回收,也是编译时分配,只有程序结束才被释放,存储初始化后的全局变量/静态变量

代码区/text

由编译期分配,存放程序运行的代码。代码会被编译成二进制存进内存中该程序中的代码区,程序结束时系统会自动释放并回收存储在代码段中的数据。

如果字符串直接赋值是在常量区的,而alloc/new/格式化 得到的字符串是放在堆区的,alloc对象在堆区,但是对象指针地址在栈区 image.png

内存管理方案

除了 mrc arc 还有三种

  • tagged pointer:处理小对象,比如 NSNumber NSDate 小 String等
  • Nonpointer_isa:非指针类型的isa,主要是用来优化 64 位地址 (一个指针 8 字节,把剩余的位存放类相关的信息 比如是否有关联对象,weak表等)
  • sideTabels:散列表,主要有两个表:引用计数和弱引用表

Tagged Pointer

self.name = [NSString stringWithFormate:@"rhq"];

self.taggedPointName = [NSString stringWithFormate:@"我是一个优秀的计算机行业搬砖人"];

taggedPointName 是一个小对象,存储在常量区,原本 name 是 在alloc分配到堆区,但是由于string较小,经过iOS 优化,就成了NSTaggedPointerString 类型,存在常量区,不会对他进行内存管理 retain 和 release

小对象是苹果在 64位环境下对 NSString NSNumber 等对象做的优化,

  • 对于 NSString 来说 如果字符串由数字、英文字母组合且长度小于等于 9 则会被视为小对象类型 存储在常量区,只有app运行结束才会释放;
  • 当有中文或其他特殊符号时会被认为 NSCFString 类型,存储在堆区,_NSCFString 是运行时创建的 NSString 子类,创建后引用计数➕1 存储在堆上;
  • _NSCFConstantString :字符串常量,是编译时确定的常量,retaincount值很大,对其操作不会引起引用计数变化,存储在字符串常量区。

Tagged Pointer 底层原理

一个对象的alloc 是对新值的 retain,对旧值的 release,而小对象在执行底层 objc_retain()时 如果是小对象则不执行retain,同样,执行 objc_release()时也进行小对象的判断,如果是小对象也不进行release

小对象指针不再是简单的地址,而是 地址+值,即真正的值,一个普通变量,可以直接读取,不进行retain和release,直接被系统释放和回收,不需要ARC进行管理,这样占用空间小,节省内存。小对象的64位地址中,前四位代表类型,后四位用于系统做一些处理,中间 56 位用于存储值。字符串常量直接赋值 @“”,存在常量区,直接读取,比withFormate 初始化方式更加快速

strong 和 copy

copy

实例变量:如果一个成员变量的数据类型是个类,能被实例化,那它就是实例变量

name 被 copy修饰的属性 赋值流程-> 在set方法中会有objc_setProperty-> reallySetProperty- >copyWithZone 赋新值,不走objc_retain ->最后释放旧值;

对属性进行 copy,namecopy = [name copy]; 执行copy后并不走上面的流程,而是:执行objc_storeStrong->objc_reatain 存在返回不存在就创建 ->objc_release

strong

strong 修饰的属性 和对属性copy一样也是:执行objc_storeStrong->objc_reatain 存在返回不存在就创建 ->objc_release

objc_retain

首先判断是否是小对象 是则直接返回,不是则进行retain:通过函数检测类包括父类中是否含有自定义的的retain方法,如果有就进行消息转发,去调用自定义的retain方法,如果没有调用 rootRetain

rootRetain

  • 如果是小类型直接返回
  • 如果是不是小对象被优化过的nonpointer,直接进行操作散列表 进行+1操作,如果是小对象则还需要判断是否正在释放,如果正在释放则执行dealloc流程,释放弱引用表和引用计数表,左后free释放对象内存,
  • 如果不是正在释放,则对nonpointer isa进行常规引用计数+1.
  • copy属性时,只是调用copywithzone,而 strong强引用所以+1 而对对象进行copy时 是对地址进行copy,此时也指向该内存,+1

散列表

extra_rc 在真机上只有8位用于存储引用计数的值(一个字节) 当引用计数过大溢出,会将引用计数一半存在isa的extra_rc中,另一半存在sideTable中,如果把引用计数都存在sideTable中,那每次对散列表都需要开解锁,操作耗时,消耗性能大,所以对半分的操作目的是为了提高性能。散列表是多张的,真机中只能有 8 张,而其他有 64 张,sideTable 包含:互斥锁,引用计数,弱引用表

为什么使用散列表而不是数组链表:

  • 数组方便查询,取下标即可,但是增删麻烦,数组读取快,但是存储不方便
  • 链表:增删方便,但是查询慢(从节头遍历查询),链表存储快,读取慢
  • 散列表:本质是一张哈希表,集合了数组和链表的额优点,增删改查都比较方便,比如拉链哈希表 是最常用的链表

release 分析

  • reallysetproperty->objc_release->release->root_release
  • 首先判断是否是小对象 是小对象则return
  • 如果不是 nonpointer isa,直接对散列表进行-1操作
  • 如果是 nonpointer isa,则对 extra_rc 中的引用计数进行-1,并存储此时的 extra_rc 状态到carry中
  • 如果此时 carry为0,则走underflow流程
  • 判断散列表中是否存储了一半的引用计数
  • 如果是则从散列表中取出存储的一半引用计数,进行 -1 然后存储到 extra_rc 中
  • 如果此时extra_rc没有值,散列表中也是空的,则直接进行析构 即dealloc 属于自动触发

dealloc 分析

  • _objc_rootDealloc->rootDealloc ->objc_dispose->objc_destructInstance(销毁实例不释放内存)->free(释放内存)
  • 如果是小对象则return
  • 如果是nonpointer,有弱引用表、关联对象、c++析构函数,散列表(引用计数表),则进入 objc_dispose ->objc_destructInstance 调用c++析构函数,删除关联引用->clearDeallocating 清空弱引用表 + 散列表->free
  • 如果没有则free释放内存。

retainCount 分析

retainCount->_objc_rootRetainCount->rootRetainCount alloc创建的对象实际上引用计数为0,其引用计数打印结果为1,是因为在获取retaincount时,在底层做了 +1的处理

alloc之后 extra_rc中的引用计数仍然是 0,bits.extra_rc(引用计数) 此时为0,只是return时 对 bits.extra_rc 进行了 + 1,所以在读取时引用计数哦才是 1

所以 alloc 创建的对象并没有 retain 和 release,只是在读取retainCount 的时候 进行了 bits.extra_rc +1的返回值