iOS
中内存管理机制是开发中一项很重要的知识,了解iOS中内存管理的规则不管是在开发中还是在学习中都能很大程度的帮助我们提升效率。下面我就根据自己的理解,详细梳理一下内存管理相关的知识。
关于内存:
在说内存管理
之前,我们首先要了解什么内存
。首先了解一些计算机的基本知识。
1. 硬件内存区分:
我们的手机、电脑、或者智能设备都有
RAM
(运行内存)和ROM
(硬盘)。
RAM
是内部存储,ROM
是外部存储。我们的CPU
直接访问的是RAM
,如果想访问外部存储,则数据须先放到RAM
中才能被CPU
访问。CPU
不能直接从内存卡里面读取指令(需要Flash
驱动等等)。
2. RAM和ROM的特点和区别:
- RAM:运行内存,CPU可以直接访问,访问速度快,价格高,不能够掉电存储-断电会失去数据-不稳定。
- ROM:存储型内存,CPU不可以直接访问,访问速度慢,价格低,可以掉电存储-稳定。
3. RAM和ROM的协同工作:
由于
RAM
不支持掉电存储,所以App
程序一般存储在ROM
中。
手机里面使用的ROM
基本都是NandFlash
(闪存),CPU
是不能直接访问的,而是需要文件系统/驱动程序(嵌入式中的EMC
)将其读到RAM
里面,CPU
才可以访问。另外,RAM
的速度也比NandFlash
快。
4、内存分区:
说到内存分区,内存即指的是RAM。一块内存条,是一个从下至上地址依次递增的结构,内存条中主要分为几大类:栈区(stack)、堆区(heap)、常量区、代码区(.text)、保留区。常量区分为未初始化区域(.bss)和已初始化区域(.data)
程序员操作的主要是栈区与堆区还有常量区。
栈区(heap)
:由系统去管理。地址从高到低分配。先进后出。会存一些局部变量,函数跳转跳转时现场保护(寄存器值保存于恢复),这些系统都会帮我们自动实现,无需我们干预。所以大量的局部变量,深递归,函数循环调用都可能耗尽栈内存而造成程序崩溃 。堆区(stack)
:需要我们自己管理内存,alloc
申请内存release
释放内存。创建的对象也都放在这里。 地址是从低到高分配。堆是所有程序共享的内存,当N个这样的内存得不到释放,堆区会被挤爆,程序立马瘫痪。这就是内存泄漏。全局区/静态区(staic)
:全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放。常量区
:常量字符串就是放在这里的,还有const
常量。代码区
:存放App
代码,App
程序会拷贝到这里。
4. App程序在内存中的运行:
当我们点击手机icon启动一个App时(例如微信): 拓展:iOS程序生命周期详解、
- 操作系统会为微信开辟
4G
的虚拟内存空间。 - 操作系统会把存储在
ROM
里面的微信部分代码copy
到上一步开辟的4G
内存空间中。 -
CPU
可以访问RAM
来运行微信程序了。
假设我们下载了一段视频,那么会从 Server
一点点下载到 RAM
,然后再从RAM写入到 ROM
,这样保证关闭微信再次打开时,视频还在。假设隔一段时间,我们要看视频,程序会将它从 ROM
读到 RAM
然后解码播放。数据本地化时候,频繁进行数据读取,可能会涉及到性能优化。
内存管理
移动设备的内存大小是有限的, 内存申请一直不释放就会导致内存不足,所以需要内存管理。OC
的对象在内存中是以堆的方式分配空间的,堆内存是由我们自己释放的。非 OC
对象一般是放在栈中,系统会自动回收。
1. 引用计数
- 引用计数(Reference counting)是一个简单有效管理对象生命周期的方式。
-
当我们新建一个新对象时候,它的引用计数+1,当一个新指针指向该对象,将引用计数+1。当指针不再指向这个对象时候,引用计数-1,当引用计数为0时,说明该对象不再被任何指针引用,将对象销毁,进而回收内存。
2. TaggedPointer
对于一个 NSNumber
对象,如果存储 NSInteger
的普通变量,那么它所占用的内存是与 CPU
的位数有关,在 32 位 CPU
下占4个字节。而指针类型的大小通常也是与 CPU
位数相关,一个指针所占用的内存在32位 CPU
下为4个字节。但是迁移至64位系统中后,其占用空间达到了8字节,以此类推,所有在64位系统中占用空间会翻倍的对象,在迁移后会导致系统内存剧增,即时他们根本用不到这么多的空间。在2013年9月,苹果推出了iPhone 5s,该款机型首次采用64位架构的A7双核处理器。所以苹果对于一些小型数据(NSNumber
、NSDate
、NSString
等),采用了 taggedPointer
这种方式管理内存。
TaggedPointer
是一种为内存高效节省空间的方法,Tagged Pointer是一个特别的指针,它分为两部分:
- 一部分直接保存数据 ;
- 另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址;
在一个程序中运行下述代码,获取输出日志:
NSNumber *number = @(0);
NSNumber *number1 = @(1);
NSNumber *number2 = @(2);
NSNumber *number3 = @(9999999999999999999);
NSString *string = [[@"a" mutableCopy] copy];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
NSLog(@"number ---- %@, %p", [number class], number);
NSLog(@"number1 --- %@, %p", [number1 class], number1);
NSLog(@"number2 --- %@, %p", [number2 class], number2);
NSLog(@"number3 --- %@, %p", [number3 class], number3);
NSLog(@"NSString -- %@, %p", [string class], string);
NSLog(@"indexPath - %@, %p", indexPath.class,indexPath);
/********************* 输出日志 *********************
number ---- __NSCFNumber, 0xb000000000000002
number1 --- __NSCFNumber, 0xb000000000000012
number2 --- __NSCFNumber, 0xb000000000000022
number3 --- __NSCFNumber, 0x600003b791c0
NSString -- NSTaggedPointerString, 0xa000000000000611
indexPath - NSIndexPath, 0xc000000000000016
*/
分析日志:
-
NSNumber
存储的数据不大时,NSNumber *
指针是伪指针Tagged Pointer
; - NSNumber存储的数据很大时,
NSNumber *
指针一般指针,指向NSNumber
实例的地址,如number3
; -
NSTaggedPointerString
经常遇见,它就是Tagged Pointer
对象;
对于Tagged Pointer
,是系统实现的,无需开发者操心!但是作为开发者,也要知道NSTaggedPointerString
等是什么东西!
objc_objcet
对象中 isa
指针分为指针型 isa
与非指针型isa(NONPOINTER_ISA)
,运用的便是类似这种技术。下面详细解读一下NONPOINTER_ISA
:
在一个64位的指针内存中
- 第0位存储的是
indexed
标识符,它代表一个指针是否为NONPOINTER
型,0代表不是,1代表是。- 第1位
has_assoc
,顾名思义,1代表其指向的实例变量含有关联对象,0则为否。- 第2位为
has_cxx_dtor
,表明该对象是否包含C++
相关的内容或者该对象是否使用ARC
来管理内存,如果含有C++
相关内容或者使用了ARC
来管理对象,这一块都表示为YES
,- 第3-35位
shiftcls
存储的就是这个指针的地址。- 第42位为
weakly_referenced
,表明该指针对象是否有弱引用的指针指向。- 第43位为
deallocing
,表明该对象是否正在被回收。- 第44位为
has_sidetable_rc
,顾名思义,该指针是否引用了sidetable
散列表。第- 45-63位
extra_rc
装的就是这个实例变量的引用计数,当对象被引用时,其引用计数+1,但少量的引用计数是不会直接存放在sideTables
表中的,对象的引用计数会先存在NONPOINTER_ISA
的指针中的45-63位,当其被存满后,才会相应存入sideTables
散列表中。
4. 散列表(sideTables
)
散列表在系统中的体现是一个 sideTables
的哈希映射表,其中所有对象的引用计数(除上述存在 NONPOINTER_ISA
中的外)都存在这个 sideTables
散列表中,而一个散列表中又包含众多 sideTable
结构体。每个 SideTable
中又包含了三个元素,spinlock_t
自旋锁,RefcountMap
引用计数表,weak_table_t
弱引用表。
它使用对象的内存地址当它的 key
。管理引用计数和 weak
指针就靠它了。
既然 SideTables
是一个哈希映射的表,为什么不用 SideTables
直接包含自旋锁,引用技术表和弱引用表呢?因为在众多线程同时访问这个 SideTables
表的时候,为了保证数据安全,需要给其加上自旋锁,如果只有一张 SideTable
的表,那么所有数据访问都会出一个进一个,单线程进行,非常影响效率,而且会带来不好的用户体验,针对这种情况,将一张 SideTables
分为多张表的 SideTable
,再各自加锁保证数据的安全,这样就增加了并发量,提高了数据访问的效率,所以这就是一张 SideTables
表下涵盖众多 SideTable
表的原因。
因为是使用对象的内存地址当 key
所以 Hash
的分部也很平均。假设 Hash
表有n
个元素,则可以将 Hash
的冲突减少到n
分之一,支持n
路的并发写操作。
自旋锁
自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。
对于引用计数的操作其实是非常快的。所以选择了虽然不是那么高级但是确实效率高的自旋锁
引用计数器(RefcountMap)
对象具体的引用计数数量是记录在这里的。
这里注意RefcountMap其实是个C++的Map。为什么Hash以后还需要个Map?其实苹果采用的是分块化的方法。
举个例子
假设现在内存中有16个对象。
0x0000、0x0001、...... 0x000e、0x000f
咱们创建一个SideTables[8]来存放这16个对象,那么查找的时候发生Hash冲突的概率就是八分之一。
假设SideTables[0x0000]和SideTables[0x0x000f]冲突,映射到相同的结果。
SideTables[0x0000] == SideTables[0x0x000f] ==> 都指向同一个SideTable
苹果把两个对象的内存管理都放到里同一个SideTable中。你在这个SideTable中需要再次调用table.refcnts.find(0x0000)或者table.refcnts.find(0x000f)来找到他们真正的引用计数器。
这里是一个分流。内存中对象的数量实在是太庞大了我们通过第一个Hash表只是过滤了第一次,然后我们还需要再通过这个Map才能精确的定位到我们要找的对象的引用计数器。
引用计数器的存储结构如下
维护weak指针的结构体(weak_table_t)
上面的
RefcountMap
是一个一层结构,可以通过 find(key)
直接找到对应的值。而 weak_entries
是一个两层结构。第一个元素
weak_entry_t *weak_entries
是一个数组,上面的 RefcountMap
是要通过 find(key)
来找到精确的元素的。weak_entries
则是通过循环遍历来找到对应的 entry
。(上面管理引用计数器苹果使用的是
Map
,这里管理 weak
指针苹果使用的是数组,有兴趣的朋友可以思考一下为什么苹果会分别采用这两种不同的结构)
- referent: 被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。
- referrers 可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,
referrers
里的所有指针都会被设置成nil
。 - inline_referrers 只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用
referrers
来存储指针。
第二个元素 num_entries
是用来维护保证数组始终有一个合适的 size
。比如数组中元素的数量超过3/4的时候将数组的大小乘以2。