IOS面试题——OC内存管理(7)

一 面试题汇总

  1. OC中内存分区从低到高是怎么样的?保留区,代码段,数据段,堆,栈,内核区
  2. 各个分区分吧存储哪些内容?
  3. OC内存管理方案有哪些?ARC,MRC的区别,Tagged Pointer是什么?自动释放池又是什么
  4. Tagged Pointer能够存储哪些类型,怎么区分iOS平台还是Mac平台
  5. 引用计数存储在什么位置?
  6. delloc方法会进行哪些操作?
  7. SideTable是什么,能够存储哪些数据,数据结构是怎么样的?
  8. 自动释放池的底层结构是什么样的,怎么实现的?
  9. Runloop和自动释放池的关系?
  10. Copy 和 mutableCopy的区别是什么?
  11. 属性关键字有哪些?什么情况下用copy?
  12. Block,NSTimer循环引用区别与解决方案?
  13. weakTable 弱引用表是怎么实现的

二 面试题解答(仅供参考)

2.1 OC中内存分区从低到高是怎么样的?保留区,代码段,数据段,堆,栈,内核区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
在 Objective-C(以及大多数现代编程语言)中,内存通常会被划分为以下几个区域,从低到高的顺序如下:

1-代码段(Text Segment):
代码段存储了程序的机器指令,即程序的可执行代码。
这部分内存是只读的,通常在程序加载时被操作系统加载到内存中,并保持不变。

2-数据段(Data Segment):
数据段包含了程序中已初始化的全局变量和静态变量。
这些变量在程序运行期间保持不变,通常位于代码段之后。

3-堆(Heap):
堆是动态分配的内存区域,用于存储程序运行期间动态分配的内存,
例如通过 malloc、calloc、realloc 等函数分配的内存。
堆的大小不固定,可以根据需要动态增长或收缩。

4-栈(Stack):
栈用于存储函数调用期间的局部变量、函数参数、函数返回地址等信息。
栈是一种后进先出(LIFO)的数据结构,每个线程都有自己的栈空间,栈的大小是固定的,通常较小。

5-内核区(Kernel Space):
内核区是操作系统内核的内存空间,包含了操作系统的核心代码和数据结构。
用户程序无法直接访问内核区,只能通过系统调用(Syscall)来与操作系统进行交互。

6-保留区:
保留区并不是内存分区的标准术语,但在某些情况下可能会被用来指代系统保留的一部分内存空间,
例如用于存储操作系统的保留区域。

这些内存分区的存在和组织方式可能会因操作系统和硬件平台而有所不同,但通常遵循类似的原则。
在 Objective-C 中,开发者通常更多地关注堆和栈的使用,以及全局变量和静态变量的存储。

2.2 各个分区分吧存储哪些内容?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
各个分区分别存储的内容可以因操作系统和文件系统类型而有所不同。
以下是一般情况下常见的分区及其存储内容:

1-系统分区(System Partition):
1.1-这通常是操作系统安装的位置。Windows系统中通常是C盘,而在Linux系统中可能是/(根)分区。
1.2-存储操作系统及其组件、程序文件、系统配置等。

2-数据分区(Data Partition):
2.1-这是用于存储用户创建的数据文件,如文档、照片、视频等。
2.2-通常在Windows系统中,D盘或其他盘符被分配为数据分区。
2.3-在Linux系统中,可能会有单独的数据分区,例如/home 分区。

3-恢复分区(Recovery Partition):
3.1-这是一些计算机制造商预先设置的分区,用于存储恢复系统到出厂设置的备份映像。
3.2-它通常包含恢复软件和系统镜像,以便在系统损坏或需要重置时恢复系统。

4-交换分区(Swap Partition)(在Linux系统中):
4.1-这是用于虚拟内存的分区,用于操作系统在物理内存不足时将数据临时存储在硬盘上。
4.2-通常用于辅助系统运行时的内存管理。

5-引导分区(Boot Partition):
5.1-这是存储引导加载程序(如GRUB或Windows Boot Manager)的分区。
5.2-它包含引导加载程序所需的文件,以便在启动时加载操作系统。

6-EFI系统分区(EFI System Partition,ESP)(在UEFI系统中):
6.1-这是存储引导加载程序和相关文件的特殊分区,用于启动操作系统。
6.2-它包含EFI标准所需的引导器和配置文件。

7-备份分区:
7.1-有些用户可能会创建专门的分区来存储系统或数据备份,以便在需要时恢复数据或系统。

需要注意的是,实际上可能不会在每台计算机上都找到所有这些分区,
而且某些分区可能会根据用户需求和系统配置而有所不同。

2.3 OC内存管理方案有哪些?ARC,MRC的区别,Tagged Pointer是什么?自动释放池又是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
在Objective-C(OC)中,内存管理主要通过两种方式来实现:
手动引用计数(MRC)和自动引用计数(ARC)。

1-MRC(Manual Reference Counting,手动引用计数):

1.1-在MRC中,开发者需要手动管理对象的引用计数。
这意味着在创建一个对象时,需要调用retain方法来增加其引用计数,在不再需要该对象时,
需要调用release方法来减少其引用计数,并在引用计数减为0时释放该对象。
1.2-MRC需要开发者手动管理内存,因此容易出错,例如内存泄漏或过早释放对象。

2-ARC(Automatic Reference Counting,自动引用计数):
2.1-ARC是由编译器在编译时自动生成引用计数管理代码的技术。
开发者不需要手动插入retain、release或autorelease调用,编译器会在适当的时候插入这些调用。
2.2-ARC简化了内存管理,减少了出错的机会,提高了开发效率。

区别:

主要区别在于是否需要开发者手动管理对象的引用计数。
MRC需要手动管理,而ARC则由编译器自动管理。

1-Tagged Pointer:

1.1-Tagged Pointer是一种优化技术,用于在一些特定情况下,不使用传统的指针来表示对象,
而是将对象的信息直接存储在指针中,以节省内存和提高性能。
1.2-在Tagged Pointer中,指针的某些位被用作标记位,用于存储对象的类型或其他信息,
而不是指向实际对象的内存地址。

2-自动释放池(Autorelease Pool):

2.1-自动释放池是用于管理自动释放对象的一种机制。
自动释放对象是使用autorelease方法创建的对象,在当前的自动释放池被销毁时,这些对象会被释放。
2.2-在ARC中,编译器会自动生成自动释放池管理代码,而在MRC中,开发者需要手动创建和管理自动释放池。

总的来说,ARC简化了内存管理,减少了开发者的工作量,并且有助于减少内存泄漏和过早释放对象的问题。
而MRC则更加灵活,但需要开发者花费更多的精力来管理内存。
Tagged Pointer 和 自动释放池是 Objective-C 内存管理的补充技术,用于优化内存使用和对象生命周期管理。

2.4 Tagged Pointer能够存储哪些类型,怎么区分iOS平台还是Mac平台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Tagged Pointer 是一种特殊的指针,用于直接存储简单数据值,而不是指向对象的内存地址。
它通常用于存储小整数值和一些简单的数据结构,从而节省内存并提高性能。
Tagged Pointer 能够存储的类型包括:

1-小整数值(Small Integer Values):
通常情况下,Tagged Pointer 可以存储较小范围的整数值,而不需要分配额外的内存
例如,一些平台可以使用 Tagged Pointer 存储小于 64 位的整数值。

2-简单数据结构(Simple Data Structures):
有些平台可以使用 Tagged Pointer 存储简单的数据结构,如 NSDate、NSNumber 等。

Tagged Pointer 的具体实现方式可能因平台而异。
在iOS和macOS平台上,通常有一些差异来区分不同的 Tagged Pointer 类型。
在iOS平台和macOS平台上,通常使用了不同的位模式来表示 Tagged Pointer,
这样就可以在运行时准确地区分不同的 Tagged Pointer 类型。

具体来说,在iOS平台和macOS平台上,Tagged Pointer 可以用于存储不同的对象类型,
包括NSNumber、NSDate、NSValue等。
区分iOS和macOS平台的方式可能会依赖于内部的位模式约定,以及操作系统和平台的特定实现。
因此,具体的区分方式可能会有所不同,并且可能受到未来版本的更新和变化的影响

2.5 引用计数存储在什么位置?

1
2
3
4
5
6
7
8
9
10
11
12
在Objective-C中,对象的引用计数是存储在对象的内部的。
具体来说,每个Objective-C对象都有一个isa指针指向其类对象,并且在类对象中有一个字段用于存储引用计数。
这个字段通常称为 retainCount,它记录了对象当前被引用的次数。

当一个对象被创建时,它的引用计数会被初始化为1。
每当有新的对象持有(retain)这个对象时,它的引用计数就会增加1;
每当有对象释放(release)对它的持有时,引用计数就会减少1。
当引用计数减少为0时,系统会自动释放这个对象的内存。

虽然在MRC(Manual Reference Counting)中开发者需要手动管理对象的引用计数,
但是在ARC(Automatic Reference Counting)中,
编译器会自动生成适当的引用计数管理代码,以减少开发者的工作量

2.6 delloc方法会进行哪些操作?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在Objective-C中,dealloc 方法是一个特殊的方法,用于在一个对象被释放(deallocating)时执行一些清理操作。
当对象的引用计数减为0时,系统会调用对象的 dealloc 方法,释放对象的内存。

通常情况下,dealloc 方法会执行以下操作:

1-释放资源:
dealloc 方法通常用于释放对象所持有的资源,例如释放分配的内存、关闭打开的文件或释放其他资源。
这样可以确保在对象被销毁时,相关资源被正确释放,以避免内存泄漏和资源泄漏。

2-取消注册观察者:
如果对象注册了观察者(KVO或通知中心),通常会在 dealloc 方法中取消注册,以避免悬挂引用。

3-断开连接:
在某些情况下,可能需要在 dealloc 方法中断开对象与其他对象之间的连接,以防止悬挂引用或循环引用。

4-调用父类的 dealloc 方法:
通常会在 dealloc 方法中调用父类的 dealloc 方法,以确保父类所需的清理操作也得以执行。

需要注意的是,ARC(Automatic Reference Counting)下,
开发者不需要手动实现 dealloc 方法来管理对象的内存释放,因为编译器会自动生成适当的引用计数管理代码。
但是,在需要执行其他清理操作时,仍然可以重写 dealloc 方法来实现这些逻辑。

2.7 SideTable是什么,能够存储哪些数据,数据结构是怎么样的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SideTable 是 Objective-C runtime 中的一个数据结构,用于存储 Objective-C 对象的引用计数信息。
它在内部用于跟踪对象的引用计数,以及处理对象的内存管理。

具体来说,SideTable 存储了每个对象的引用计数信息,以及与对象相关联的一些其他数据。
这些数据通常包括:

1-引用计数(Reference Count):
记录了对象当前被引用的次数。当引用计数为0时,系统会释放对象的内存。

2-自旋锁(Spin Lock):
用于在多线程环境中保护引用计数的访问,以确保线程安全性。

3-引用计数溢出位(Overflow Count):
当引用计数溢出时,会在 SideTable 中的溢出位中记录溢出次数,用于处理引用计数溢出的情况。

4-弱引用计数(Weak Reference Count):
用于跟踪对象的弱引用数量,以支持弱引用的内存管理。

5-弱引用表(Weak Reference Table):
用于存储对象的弱引用列表,以便在对象被释放时通知相关的弱引用。

2.8 自动释放池的底层结构是什么样的,怎么实现的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
自动释放池(Autorelease Pool)是 Objective-C 中用于管理自动释放对象的一种机制。
其底层结构可以简单描述为一个栈(stack)或者链表(list),其中每个节点表示一个自动释放池。
当创建一个自动释放池时,会将该自动释放池压入栈或链表中,当自动释放池被销毁时,会从栈或链表中移除该自动释放池。
在某个自动释放池范围结束时,其中的所有自动释放对象都会被释放。

自动释放池的实现可以通过以下步骤完成:

1-创建自动释放池:
当进入一个新的自动释放池范围时,系统会创建一个新的自动释放池对象。

2-将自动释放池压入栈或链表:
创建的自动释放池会被压入一个栈或链表的顶部,以便在之后进行管理。

3-添加自动释放对象:
在自动释放池内部,每当调用 autorelease 方法时,该对象就会被添加到当前自动释放池中。

4-自动释放对象:
当自动释放池范围结束时,会触发自动释放池对象的销毁操作。在销毁操作中,会释放自动释放池中的所有对象。

5-移除自动释放池:销毁操作完成后,自动释放池会被从栈或链表中移除。

这种基于栈或链表的自动释放池实现方式,能够保证在一个自动释放池范围内,
所有添加到该自动释放池的对象都会在该范围结束时被释放,从而实现了自动内存管理的目的。

2.9 Runloop和自动释放池的关系?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Runloop(运行循环)和自动释放池在iOS和macOS应用程序中密切相关,它们之间有以下关系:

1-Runloop 负责管理自动释放池的释放时机:
在每个 Runloop 循环中,系统会在适当的时机处理自动释放池中的对象释放操作。
具体来说,当 Runloop 进入休眠状态或者切换模式时,会执行自动释放池中待释放对象的释放操作。

2-自动释放池的作用域通常与 Runloop 的循环一致:
通常情况下,自动释放池的作用域(scope)与 Runloop 的循环一致。
在每次 Runloop 迭代中,都会创建一个新的自动释放池,并在本次迭代结束时释放该自动释放池中的对象。

3-在 Runloop 中手动创建自动释放池:
开发者可以通过在特定代码块中手动创建自动释放池来控制对象的释放时机。
在某些需要大量临时对象的代码段中,手动创建自动释放池可以帮助控制内存占用,并在合适的时机释放内存。

4-Runloop 的工作原理与自动释放池密切相关:
Runloop 在每次迭代中会处理输入源、定时器和观察者等事件,而其中的处理过程可能会创建临时对象。
这些临时对象通常会被添加到自动释放池中,以便在适当的时机释放内存,从而避免内存泄漏。

总的来说,Runloop 和自动释放池的配合使用,
确保了在应用程序运行过程中及时释放不再需要的临时对象,有效地管理内存资源。

2.10 Copy 和 mutableCopy的区别是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
copy 和 mutableCopy 是 Objective-C 中用于创建对象副本的方法,它们之间的区别在于:

1-copy:
1.1-copy 方法用于创建不可变对象的副本。当调用 copy 方法时,
会生成原始对象的一个新的不可变副本,新副本的内容与原始对象相同。
1.2-对于不可变对象,copy 方法实际上只是返回原始对象本身,
因为不可变对象不需要创建副本来保证内容的不可变性。

2-mutableCopy:
2.1-mutableCopy 方法用于创建可变对象的副本。
当调用 mutableCopy 方法时,会生成原始对象的一个新的可变副本,新副本的内容与原始对象相同。
2.2-对于可变对象,mutableCopy 方法会创建一个新的可变副本,允许修改副本的内容而不影响原始对象。

总的来说,copy 方法用于复制不可变对象,而 mutableCopy 方法用于复制可变对象。
这两个方法的返回类型分别是不可变类型和可变类型,因此在使用时需要根据实际需求来选择使用哪个方法。

2.11 属性关键字有哪些?什么情况下用copy?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
在Objective-C中,定义属性时可以使用不同的关键字来指定属性的特性。常用的属性关键字包括:

1-atomic:
保证在多线程环境下对属性的读取和写入是安全的。
当设置为 atomic 时,系统会使用锁来确保对属性的操作是原子性的。这是默认的行为。

2-nonatomic:
不保证在多线程环境下对属性的读取和写入是安全的。
使用 nonatomic 可以提高性能,因为它不会添加额外的锁来确保原子性操作。

3-strong:
指定属性为强引用,即该属性持有对象的所有权。当对象的强引用计数变为0时,对象会被释放。

4-weak:
指定属性为弱引用,即该属性不持有对象的所有权。当对象的弱引用计数变为0时,对象会被释放。
使用弱引用可以避免循环引用。

5-copy:
指定属性在设置时进行对象的拷贝操作。当使用 copy 关键字修饰属性时,
属性的 setter 方法会对传入的对象进行拷贝操作,从而使得原始对象和属性的值相互独立,
避免了由于原始对象变化而导致属性值也发生变化的情况。

6-readonly:
指定属性为只读,即只生成 getter 方法而不生成 setter 方法。
只读属性只能在初始化时被赋值,之后不能被修改。

7-readwrite:
指定属性为读写,即生成 getter 和 setter 方法。

8-assign:
用于基本数据类型的属性,指定属性直接赋值,不进行内存管理。通常用于非对象类型的属性。

9-unsafe_unretained:
与 assign 类似,但用于指定属性为非强引用的对象类型,不进行引用计数管理。
在对象被释放后,指向该对象的指针不会被置为 nil,容易导致野指针访问问题。

这些属性关键字可以根据需要进行组合使用,以满足具体的需求。一般来说,使用 copy 关键字的情况包括:

1-当属性是一个可变对象类型时,为了避免外部修改对象导致内部数据被篡改,通常会使用 copy 关键字。
2-当属性是一个遵循了 NSCopying 协议的自定义对象时,
为了在赋值时进行深拷贝而不是浅拷贝,也会使用 copy 关键字。
3-当属性的对象可能在设置后被修改,而你希望保持属性的值不受影响时,也可以使用 copy 关键字。

2.12 Block,NSTimer循环引用区别与解决方案?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
循环引用是指两个或多个对象之间相互持有对方的强引用,导致它们无法被释放,从而产生内存泄漏。
在使用 Block 和 NSTimer 时,循环引用是比较常见的问题。以下是它们的区别以及解决方案:

1-Block 循环引用:

区别:

Block 可以捕获外部对象(包括 self),如果 Block 内部持有了外部对象的强引用,
而外部对象又持有 Block,则会导致循环引用。

解决方案:

1-使用弱引用(weak reference)或者非持有引用(__unsafe_unretained)来避免循环引用。
例如,使用 __weak typeof(self) weakSelf = self;
来捕获 self,并在 Block 内部使用 weakSelf 来访问 self。
2-在 Block 执行完后手动将 Block 置为 nil,以打破循环引用。
可以在 Block 结束时,将持有该 Block 的对象的属性置为 nil,
或者将 Block 赋值为 nil,以释放对外部对象的强引用。

NSTimer 循环引用:

区别:

当 NSTimer 被添加到 Runloop 中时,Runloop 会持有 NSTimer,
而 NSTimer 又会持有其 target(通常是调用 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
方法时传入的对象)。如果在 target 中引用了 NSTimer,就会形成循环引用。

解决方案:

1-使用弱引用(weak reference)来避免循环引用。
可以在创建 NSTimer 时使用 [NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]
方法的 target 参数传入一个弱引用的对象,或者在 NSTimer 的执行方法中使用 weakSelf。
2-在适当的时候手动使 NSTimer 失效(invalidate)。
可以在需要 NSTimer 停止时调用其 invalidate 方法,以释放对其 target 的强引用,从而打破循环引用。

总的来说,避免循环引用的关键是合理使用弱引用,并在合适的时机打破循环引用。

2.13 weakTable 弱引用表是怎么实现的

1
2
weakTable(弱引用表)是一种数据结构,用于在 Objective-C runtime 中管理弱引用。
它通常被用于实现自动释放池、ARC 中的弱引用以及 Objective-C 中的一些特性,如 KVO、通知中心等

三 参考

  • 简书—OC内存管理