本文主讲内存管理相关面试问题,包括内存布局、内存管理方案、数据结构、ARC&MRC、引用计数管理、弱引用管理、自动释放池、循环引用。
一、内存布局
1、stack(栈区):方法调用。
2、heap(堆区):通过alloc等分配的对象。
3、bss:未初始化的全局变量等。
4、Data:已初始化的全局变量等。
5、text:程序代码。
二、内存管理方案
问题:iOS操作系统是怎样对内存进行管理的?(iOS会根据不同场景会采取不同内存方案)
1、TaggedPoint(小对象)。 如NSNumber,NSData类型。
2、NONPOINTER_ISA
(非指针型的isa)。 在arm64位架构下使用的一种方案,这种方案主要是高效利用64位架构下isa指针的剩余内存空间。
3、散列表
(散列表是复杂的数据结构,其中包含引用计数表、弱引用表)。 在32位架构下使用以及64位架构下isa指针存放不下的场景下使用,也就是我们常说的信引用计数表
1、NONPOINTER_ISA
NONPOINTER_ISA 64个比特位分析:
- indexed(1位): 0 代表纯isa指针,内容代表当前对象的类对象的地址 . 1代表isa指针不仅存储类对象的地址, 还有内存管理数据也就是
NONPOINTER_ISA
(非指针型的isa). - has_assoc(2位):表示当前对象是否有关联对象, 0 没有, 1有.
- has_cxx_dtor(3位): 当前对象是否有使用C++语言相关内容.当前对象是否使用ARC管理内存
- shiftcls(4~37位): 共33位比特位 表示当前对象类对象的指针地址.
- magic(38~43位):
- weakly_referenced(44位): 标识当前对象是否有弱引用指针
- deallocating(45位): 当前对象是否正在进行dealloc操作
- has_sidetable_rc(46位): 当前isa指针中所存储引用计数达到上限, 需要外挂一个sidetable数据结构用来存储相关的引用内容,也就是
散列表
- extra_rc(47~64位): 额外的引用计数, 当引用计数在很小值范围就会存储在isa指针中.
2、散列表方式
SideTables()(非嵌入式系统中包含64个SideTable),实际是一个哈希表,通过对象的指针找到对应的引用计数表或弱引用表,在哪一个SideTable中
SideTable结构
包含自旋锁 引用计数表 弱引用表
问题:为什么不是一个SideTable?
存在效率问题,如果多个对象在对同一张表进行引用计数时,就会等待前一个对象操作结束才能操作。引用分离锁的方案,可以提高访问效率。
问题:怎样实现快速分流?(哈希查找的过程)
- SideTables的本质是一张
Hash表
。 - 根据对象的地址,通过一个均匀散列函数的计算就可以得到数组下标索引值。
Hash查找过程
例: 给定值是对象内存地址,目标值是数组下标索引。
f(ptr) = (uintptr_t)ptr% array.count
三、散列表中数据结构
1)自旋锁(Spinlock_t)
- 是一种忙等的锁(当前锁已被其他线程获取,当前线程就会不断的探测这个锁是否被释放,如果释放自己第一时间获取锁)
- 适用于轻量访问。 例如引用计数+1、-1操作
补充:信号量 如果获取不到锁,自己会等待休眠,等他其他线程释放锁时,唤醒当前线程.
2)引用计数表(RefcountMap)
ptr ——> DisguisedPtr(obj) ——>size_t
提高查找效率,插入和获取都是通过同一个哈希算法,避免了数组遍历
3)弱引用表(weal_table_t)
ptr ——> Hash函数——>value
四、ARC&MRC
1、MRC:手动引用计数。alloc、retain、release、retainCount、autorelease、dealloc。
2、ARC:自动引用计数。
(1)、ARC是LLVM(编译器)和RunTime协作的结果。
(2)、ARC中禁止手动调用retain/release/retainCount/dealloc。
(3)、ARC中新增weak、strong属性关键字。
五、引用计数管理
实现原理分析,包括alloc、retain、release、retainCount、dealloc。
1、alloc实现
经过一系列调用,最终调用的C函数calloc,此时并没有设置引用计数为1(但是通过retainCount得知是1,在后面会讲到)
2、retain实现
经过两次Hash查找,找到对应的引用计数值,然后进行+1的操作
SideTable& table = SideTables()[this];
size_t& refcntStorage = table.refcnts[this];
refcntStorage += SIDE_TABLE_RC_ONE;
3、release实现
经过两次Hash查找,找到对应的引用计数值,然后进行-1的操作
SideTable& table = SideTables()[this];
RefcountMap::iterator it = table.refcnts.find(this);
it ->second -= SIDE_TABLE_RC_ONE;
4、retainCount实现
经过两次Hash查找,找到对应的引用计数值,然后与1相加(因此刚alloc的对象,在对应的引用计数表中实际是没有这个映射的)
SideTable& table = SideTables()[this];
size_t refcnt_result = 1;
RefcountMap::iterator it = table.refcnts.find(this);
refcnt_result += it->secont >> SIDE_TABLE_RC_SHIFT;
5、dealloc实现
判断对象时候可以释放的条件(五个条件缺一不可)
- 没有使用nonpointer_isa
- 没有weak指针指向
- 没有有关联对象
- 没有使用ARC或者涉及C++
- 当前对象的引用计数没有通过SideTable中的引用计数表来存储的
object_dispose() 实现
objc_destructInstance()实现
clearDeallocating()实现
六、弱引用管理
问题:如何添加weak变量的?(系统是怎样把一个weak变量添加到弱引用表中?)
答:对象指针在经过编译器的编译之后调用objc_initweak(),然后storeweak()方法,经过一系列的函数调用栈,最终在weak_register_no_lock()进行弱引用变量的添加,(具体添加位置是)通过hash算法位置查找,如果已经存在当前对象对应的弱引用数组,则直接加进去,如果没有则创建新个新的弱引用数组,然后把第0个位置存放新的weak指针,后面的都初始化nil或者0。
问题: 系统如何实现将废弃对象的weak指针置为nil?
答:当对象被dealloc废弃之后,dealloc内部实现中会调用弱引用清除的相关函数(weak_clear_no_lock()
)。然后在函数实现中,根据当前对象指针,哈希查找弱引用表,把当前对象对应的弱引用都拿出来是一个数组,然后遍历这个数组中所有的弱引用指针置分别置为nil。
七、自动释放池
1、自动释放池数据结构
编译器会将@autoreleasepool{}
改写为:
void *ctx = objc_autoreleasePoolPush();
{}中的代码
objc_autoreleasePoolPop(ctx);
下面对上面的主要函数进行一个简单的说明:
1)objc_autoreleasePoolPush内部实现
2)objc_autoreleasePoolPop内部实现
实际objc_autoreleasePoolPop
函数在内部做了pop操作,批量将autoreleasepool中的所有的对象都会做一次release操作.
下面对上面的主要函数进行一个简单的说明:
3)AutoreleasePool的结构
- 是以栈为结点通过双向链表的形式组合而成
- 是和线程一一对应的
问题:AutoreleasePool的实现结构是怎么样的?(什么是自动释放池?实现原理)
答:
AutoreleasePool是以栈
为结点,通过双向链表
的形式组合而成的数据结构。 AutoreleasePool是和线程
一一对应的。
2、双向链表
AutoreleasePoolPage
1)AutoreleasePoolPage::push实现流程(释放池多层嵌套)
- 插入哨兵对象
2)AutoreleasePoolPage::pop实现流程(与push相反)
- 根据传入的哨兵对象找到对应的位置
- 给上次push操作之后添加的对象依次发送release消息
- 回退next指针到正确的位置
[obj autorelease]的实现(对象加入自动释放池)
先判断当前next指针是否指向栈顶,如果没有指向栈顶直接将对象加入到next指针位置,结束流程;如果next已经位于栈顶,则增加一个栈结点到链表上,在新的栈添加对象,结束流程
问题: array是什么时候释放的呢?
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableArray *arrar =[NSMutableArray array];
NSLog(@"%@",arrar);
}
答:在当次runloop将要结束的时候调用AutoreleasePoolPage:pop(),对array对象执行release操作
问题:AutoreleasePool为何可以嵌套使用?
答: 多次插入哨兵对象,也就是对一个新的autoreleasePool
的创建,如果当前栈没有满,则不需要创建新的page,如果满了,新增一个栈节点
问题: AutoreleasePool的使用场景?
答: 在for循环中,alloc图片数据等内存消耗较大的场景手动插入autoreleasePool,每一次for循环都进行一次内存的释放,降低内存消耗
八、循环引用
1、三种循环引用:自循环引用、相互循环引用、多循环引用。
2、如何破除循环引用?
- 避免产生循环引用 (如使用代理两个对象,一个是强引用,一个是弱引用)
- 在合适的时机手动断环
3、 常见的循环引用场景:
代理(delegate)、block 、 NSTimer 、大环引用
4、具体的解决方案都有哪些?
- __weak
- __block
- __unsafe_unretained(与weak等效)
__block破解 (__block在ARC和MRC条件下的区别?)
-
MRC
下,__block修饰对象不会增加其引用计数,避免
了循环引用 -
ARC
下,__block修饰对象会被强引用,无法避免
循环引用,需手动解环
__unsafe_unretained破解
- 修饰对象不会增加其引用计数,
避免
了循环引用。 - 如果被修饰对象在某一时机被释放,会产生
悬空指针
。
循环引用的示例?(平时开发时是否有遇到循环引用,又是怎么解决的?)
- Block使用示例(在后面block讲解时)
- NSTimer 的循环引用问题
//NSTimer+WeakTimer.h
@interface NSTimer (WeakTimer)
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;
@end
//NSTimer+WeakTimer.m
@interface TimerWeakObject : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;
- (void)fire:(NSTimer *)timer;
@end
@implementation TimerWeakObject
- (void)fire:(NSTimer *)timer {
if (self.target) {
if ([self.target respondsToSelector:self.selector]) {
[self.target performSelector:self.selector withObject:timer.userInfo];
}
}else{
[self.timer invalidate];
}
}
@end
@implementation NSTimer (WeakTimer)
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
TimerWeakObject *object = [[TimerWeakObject alloc] init];
object.target = aTarget;
object.selector = aSelector;
object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];
return object.timer;
}
@end
内存管理面试总结:
问题:什么是ARC?
答:自动引用计数。ARC是由LLVM(编译器)和RunTime共同协作来为我们实现自动引用计数的管理。
问题:为什么weak指针指向的对象在废弃之后会被自动置为nil?
答:当对象被废弃之后,dealloc内部实现中会调用清除弱引用的相关函数(weak_clear_no_lock()
)。然后在清除弱引用函数实现中,会通过哈希算法查找被废弃对象在弱引用表中位置,来提取它所对应的弱引用指针的列表数组,然后进行for循环遍历, 把所有的弱引用指针置分别置为nil。
问题:苹果是如何实现AutoreleasePool的?
答:AutoreleasePool是以栈
为结点,通过双向链表
的形式组合而成的数据结构。
问题:什么是循环引用?你遇到过哪些循环引用,是怎样解决的?
答: NSTimer循环引用
//__weak修饰,弱应用,对象引用计数不会加1
__weak NSArray *weakArr1;
__weak NSArray *weakArr2;
{
//arr1指向的数组对象没有被注册到autorelease pool
NSArray *arr1 = [[NSArray alloc] initWithObjects:@"123", nil];
weakArr1 = arr1;
//arr2指向的数组对象已被注册到autorelease pool
NSArray *arr2 = [NSArray arrayWithObjects:@"123", nil];
weakArr2 = arr2;
}
//局部变量arr1和arr2的作用域结束,
//此时arr1指向的对象不再被强引用,因此被回收;
//而arr2指向的对象仍然在autorelease pool中
NSLog(@"%@", weakArr1);//输出null
NSLog(@"%@", weakArr2);//输出arr2,因为此刻arr2在autorelease pool中,不会因为arr2作用域的结束而被回收
__weak NSObject *weakObj1;
__weak NSObject *weakObj2;
{
__autoreleasing NSObject *obj1 = [[NSObject alloc] init];
//weakObj1指向的对象已被注册到autorelease pool
weakObj1 = obj1;
__strong NSObject *obj2 = [[NSObject alloc] init];
//weakObj2指向的对象没有被注册到autorelease pool
weakObj2 = obj2;
}
//局部变量obj1和obj2的作用域结束,
//此时weakObj2指向的对象不再被强引用,因此被回收;
//而weakObj1指向的对象仍然在autorelease pool中
NSLog(@"%@", weakObj1);//输出<NSObject: 0x100206030>,因为此刻weakObj1在autorelease pool中,不会因为obj1作用域的结束而被回收
NSLog(@"%@", weakObj2);//输出null
Runloop每次循环都是被一个AutoReleasePool包围着的,具体说每次Runloop循环将要结束的时候会释放当前runloop的内存占用。再创建好一个AutoReleasePool给下一次Runloop循环使用。在该方法中创建的array会加入到当次RunLoop的AutoReleasePool中,array会在当前RunLoop将要结束的时候调用AutoreleasePoolPage:pop(),得到内存释放。