[iOS面试]第4章 内存管理相关面试问题

本文主讲内存管理相关面试问题,包括内存布局、内存管理方案、数据结构、ARC&MRC、引用计数管理、弱引用管理、自动释放池、循环引用。

一、内存布局

1、stack(栈区):方法调用。
2、heap(堆区):通过alloc等分配的对象。
3、bss:未初始化的全局变量等。
4、Data:已初始化的全局变量等。
5、text:程序代码。

二、内存管理方案

问题:iOS操作系统是怎样对内存进行管理的?(iOS会根据不同场景会采取不同内存方案)

1、TaggedPoint(小对象)。 如NSNumber,DSData类型。
2、NONPOINTER_ISA(非指针型的isa)。 在arm64位架构下使用的一种方案,这种方案主要是高效利用64位架构下isa指针的剩余内存空间。
3、散列表(散列表是复杂的数据结构,其中包含引用计数表、弱引用表)。 在32位架构下使用以及64位架构下isa指针存放不下的场景下使用,也就是我们常说的信引用计数表

1.NONPOINTER_ISA

NONPOINTER_ISA 64个比特位分析:

NONPOINTER_ISA_1

  • indexed(1位): 0 代表纯isa指针,内容代表当前对象的类对象的地址 . 1代表isa指针不仅存储类对象的地址, 还有内存管理数据也就是NONPOINTER_ISA(非指针型的isa).
  • has_assoc(2位):表示当前对象是否有关联对象, 0 没有, 1有.
  • has_cxx_dtor(3位): 当前对象是否有使用C++语言相关内容.当前对象是否使用ARC管理内存
  • shiftcls(4~37位): 共33位比特位 表示当前对象类对象的指针地址.

NONPOINTER_ISA_2

  • magic(38~43位):
  • weakly_referenced(44位): 标识当前对象是否有弱引用指针
  • deallocating(45位): 当前对象是否正在进行dealloc操作
  • has_sidetable_rc(46位): 当前isa指针中所存储引用计数达到上限, 需要外挂一个sidetable数据结构用来存储相关的引用内容,也就是散列表
  • extra_rc(47~64位): 额外的引用计数, 当引用计数在很小值范围就会存储在isa指针中.

2.散列表方式

SideTables()结构

SideTables()(非嵌入式系统中包含64个SideTable),实际是一个哈希表,通过对象的指针找到对应的引用计数表或弱引用表,在哪一个SideTable中

SideTable结构

SideTable结构
包含自旋锁 引用计数表 弱引用表

问题:为什么不是一个SideTable?
存在效率问题,如果多个对象在对同一张表进行引用计数时,就会等待前一个对象操作结束才能操作。引用分离锁的方案,可以提高访问效率。

问题:怎样实现快速分流?(哈希查找的过程)

  • SideTables的本质是一张Hash表
  • 根据对象的地址,通过一个均匀散列函数的计算就可以得到数组下标索引值。

Hash查找过程
例: 给定值是对象内存地址,目标值是数组下标索引。

hash查找

f(ptr) = (uintptr_t)ptr% array.count

三、散列表中数据结构

1)自旋锁(Spinlock_t)

  • 是一种忙等的锁(当前锁已被其他线程获取,当前线程就会不断的探测这个锁是否被释放,如果释放自己第一时间获取锁)
  • 适用于轻量访问。 例如引用计数+1、-1操作

补充:信号量 如果获取不到锁,自己会等待休眠,等他其他线程释放锁时,唤醒当前线程.

2)引用计数表(RefcountMap)
ptr ——> DisguisedPtr(obj) ——>size_t
提高查找效率,插入和获取都是通过同一个哈希算法,避免了数组遍历

RefcountMap

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实现

dealloc实现

判断对象时候可以释放的条件(五个条件缺一不可)

  • 没有使用nonpointer_isa
  • 没有weak指针指向
  • 没有有关联对象
  • 没有使用ARC或者涉及C++
  • 当前对象的引用计数没有通过SideTable中的引用计数表来存储的
object_dispose() 实现

object_diapose内部实现

objc_destructInstance()实现

objc_destructInstance内部实现

clearDeallocating()实现

clearDeallocating内部实现

六、弱引用管理

weak对象编译

objc_initWeak调用栈

问题:如何添加weak变量的?(系统是怎样把一个weak变量添加到弱引用表中?)
答:对象指针在经过编译器的编译之后调用objc_initweak(),然后storeweak()方法,经过一系列的函数调用栈,最终在weak_register_no_lock()进行弱引用变量的添加,(具体添加位置是)通过hash算法位置查找,如果已经存在当前对象对应的弱引用数组,则直接加进去,如果没有则创建新个新的弱引用数组,然后把第0个位置存放新的weak指针,后面的都初始化nil或者0。

系统如何实现将废弃的weak指针置为nil

问题: 系统如何实现将废弃对象的weak指针置为nil?
答:当对象被dealloc废弃之后,dealloc内部实现中会调用弱引用清除的相关函数(weak_clear_no_lock())。然后在函数实现中,根据当前对象指针,哈希查找弱引用表,把当前对象对应的弱引用都拿出来是一个数组,然后遍历这个数组中所有的弱引用指针置分别置为nil。

七、自动释放池

1 自动释放池数据结构

编译器会将@autoreleasepool{}改写为:

void *ctx = objc_autoreleasePoolPush();
{}中的代码
objc_autoreleasePoolPop(ctx);

下面对上面的主要函数进行一个简单的说明:

1)objc_autoreleasePoolPush内部实现

objc_autoreleasePoolPush内部实现

2)objc_autoreleasePoolPop内部实现

objc_autoreleasePoolPop内部实现

实际objc_autoreleasePoolPop函数在内部做了pop操作,批量将autoreleasepool中的所有的对象都会做一次release操作.

下面对上面的主要函数进行一个简单的说明:

3)AutoreleasePool的结构
  • 是以栈为结点通过双向链表的形式组合而成
  • 是和线程一一对应的

问题:AutoreleasePool的实现结构是怎么样的?(什么是自动释放池?实现原理)
答:
AutoreleasePool是以为结点,通过双向链表的形式组合而成的数据结构。 AutoreleasePool是和线程一一对应的。

2 双向链表

双向链表数据结构

栈数据结构

AutoreleasePoolPage

AutoreleasePoolPage数据结构

1)AutoreleasePoolPage::push实现流程(释放池多层嵌套)

  • 插入哨兵对象

AutoreleasePoolPage push实现流程

2)AutoreleasePoolPage::pop实现流程(与push相反)

  • 根据传入的哨兵对象找到对应的位置
  • 给上次push操作之后添加的对象依次发送release消息
  • 回退next指针到正确的位置
[obj autorelease]的实现(对象加入自动释放池)

先判断当前next指针是否指向栈顶,如果没有指向栈顶直接将对象加入到next指针位置,结束流程;如果next已经位于栈顶,则增加一个栈结点到链表上,在新的栈添加对象,结束流程

obj autorelease实现

问题: 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、如何破除循环引用?

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循环引用

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(),得到内存释放。