2022iOS面试题总结之内存管理
在 iOS 中,我们通常将内存分为五大部分:
* 代码区:用于存放程序的代码,即 CPU 执行的机器指令,并且是只读的。
* 全局区 / 静态区:它主要存放静态数据、全局数据和常量。分为未初始化全局区(BSS 段)、初始化全局区:(数据段)。程序结束后由系统释放。
* 数据段:用于存放可执行文件中已经初始化的全局变量,也就是用来存放静态分配的变量和全局变量。
* BSS 段:用于存放程序中未初始化的全局变量。
* 常量区:用于存储已经初始化的常量。程序结束后由系统释放。
* 栈区(Stack):用于存放程序 临时创建的变量、存放函数的参数值、局部变量等。从上往下,地址是连续的,由编译器自动分配释放。
* 堆区(Heap):用于存放alloc分配的对象,copy之后的block变量(copy后其实是一个对象)等,从下往上,是链表结构,地址不连续的,由程序员分配和释放。
从上边内存的各个部分说明可以看出:只有堆区存放的数据需要由程序员分配和释放。
堆区存放的,主要是继承了 NSObject 的对象,需要由程序员进行分配和释放。其他非对象类型(int、char、float、double、struct、enum 等)则存放在栈区,由系统进行分配和释放,对象的成员变量进行赋值后,会从栈区复制一份到堆区。
内存管理方案的三种:
1.Tagged Pointer
2.NONPOINTER_ISA(非指针isa)
3.散列表(引用计数表和弱引用计数表)
1.iOS的寻址空间扩大到了64位。我们可以用63位来表示一个数字(一位做符号位)。那么这个数字的范围是2^63 ,很明显我们一般不会用到这么大的数字,那么在我们定义一个数字时NSNumber *num = @100,实际上内存中浪费了很多的内存空间。
Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。
Tagged Pointer是一种特殊的“指针”,指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
在内存读取上有着3倍的效率,创建时比以前快106倍。
2.NONPOINTER_ISA,nonpointer:表示是否对isa开启指针优化 。index位是0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等
sildetable是多张表组合到一个组里,不然又成千上万个对象都放一张表就会有效率问题。
引用计数表是hash表,存储和取值都是通过同一个函数计算得到,避免了循环遍历,提高查找效率。
引用计数机制,一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程,分为mrc(手动) arc(自动)
01-当你通过new、alloc或copy方法创建一个对象时,它的引用计数为1,当不再使用该对象时,应该向对象发送release或者autorelease消息释放对象。
02-当你通过其他方法获得一个对象时,如果对象引用计数为1且被设置为autorelease,则不需要执行任何释放对象的操作;
03-如果你打算取得对象所有权,就需要保留对象并在操作完成之后释放,且必须保证retain和release的次数对等。
retain:持有,对原对象引用计数加1,强引用。ARC中使用strong。 copy:拷贝,复制一个对象并创建strong关联,引用计数为1 ,原来对象计数不变。 assign:赋值,不涉及引用计数的变化,弱引用。ARC中对象不使用assign,但原始类型(BOOL、int、float)仍然可以使用,assign修饰的对象,当对象释放之后,即引用计数为0时,指针变量并不会同时置为nil,全局变量就是变为野指针,不知道指向哪,再向该对象发消息,非常容易崩溃。
因此,当属性类型是对象时,不要使用assign,会带来一些风险。
weak:赋值(ARC),比assign多了一个功能,对象释放后把指针置为nil,避免了野指针,weak只能用来修饰对象,不能用来修饰基本数据类型。
strong:持有(ARC),等同于retain。
在你打开ARC时,你是不能使用retain、release、autorelease 操作的,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了,但是你需要在对象属性上使用weak 和strong, 其中strong就相当于retain属性,而weak相当于assign,基础类型只需声明非原子锁即可。
僵尸对象:内存已经被回收的对象。
野指针:指向僵尸对象的指针,向野指针发送消息会导致崩溃。
alloc会调用c函数的calloc,此时并没有设置引用计数器为1.
retain是怎么将对象的引用计数器+1?经过2次hash表的查找,找到size_t加1实现的。
release的查找和retain一样,后面是做-1操作。
对象创建alloc的时候是没有修改引用计数表的,但调用reatanCout后会加1返回。
关联对象释放原理,
weak_clear_no_lock()函数会根据hash算法查询到当前对象的弱引用表,遍历表置为nil.
weak 实现原理的概括
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址,就是地址的地址)集合(当weak指针的数量小于等于4时,是数组, 超过时,会变成hash表)。
weak 的实现原理可以概括以下三步:
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表或者加入已创建的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,清理对象的记录。
autorelease实现机制
看出AutoreleasePoolPage是一个双向链表结构,同时内部是一个栈结构,和线程一一对应
自动释放池,系统有一个现成的自动内存管理池,他会随着每一个mainRunloop的结束而释放其中的对像;自动释放池也可以手动创建,他可以让pool中的对象在执行完代码后马上被释放,可以起到优化内存,防止内存溢出的效果(如视频针图片的切换时、创建大量临时对象时等)
使用:@autoreleasepool {}代码块(ARC和MRC下均可以使用)
该循环内产生大量的临时对象,直至循环结束才释放,可能导致内存泄漏,解决方法和上文中提到的自动释放池常见问题类似:在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值。
for (int i = 0; i < 10000; i ++) { @autoreleasepool { Person* soldier = [[Person alloc]init]; [soldier fight]; } }
在ARC有效的情况下编译源代码,必须遵守一定的规则。
不能使用retain/release/retainCount/autorelease
ARC有效时,实现retain/release/retainCount/autorelease会引起编译错误。代码会标红,编译不通过。
不能使用NSAllocateObject/NSDeallocateObject
——————————————————
NSTimer的循环引用
01-引起原因
当你在 ViewController (简称 VC )中使用 timer 属性,由于 VC 强引用 timer,timer 的target 又是 VC 造成循环引用。当你在 VC 的 dealloc 方法中销毁 timer,发现 VC 被 pop,VC 的 dealloc 方法没走,VC 在等 timer 释放才走 dealloc,timer 释放在 dealloc 中,所以引起循环引用。
解决01-苹果 API 接口解决方案(iOS 10.0 以上可用)苹果官方新增了关于 NSTimer 的三个 API:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats: (BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats: (BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); - (instancetype)initWithFireDate:(NSDate *)date interval: (NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
定时器在执行时,将自身作为参数传递给 block,来帮助避免循环引用。使用很简单,但是要注意两点:
1. 避免 block 的循环引用,使用 __weak 和 __strong 来避免
2. 在持用 NSTimer 对象的类的方法中 -(void)dealloc 调用 NSTimer 的- (void)invalidate 方法;
解决02-对定时器 NSTimer 封装PFTimer,封装多一层,让self去强引用PFTimer,PFTimer强引用time
代码如下:PFTime.h
//PFTimer.h文件 #import <Foundation/Foundation.h>@interface PFTimer : NSObject //开启定时器- (void)startTimer; //暂停定时器- (void)stopTimer;@end
PFTime.m
#import "PFTimer.h" @implementation PFTimer { NSTimer *_timer; } - (void)stopTimer{ if (_timer == nil) { return; } [_timer invalidate]; _timer = nil; } - (void)startTimer{ _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(work) userInfo:nil repeats:YES]; } - (void)work{ NSLog(@"正在计时中。。。。。。"); } - (void)dealloc{ NSLog(@"%s",__func__); [_timer invalidate]; _timer = nil; } @end
外部使用:
#import "ViewController1.h" #import "PFTimer.h" @interface ViewController1 () @property (nonatomic, strong) PFTimer *timer; @end @implementation ViewController1 - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; } - (void)viewDidLoad { [super viewDidLoad]; self.title = @"VC1"; self.view.backgroundColor = [UIColor whiteColor]; //自定义timer PFTimer *timer = [[PFTimer alloc] init]; self.timer = timer; [timer startTimer]; } - (void)dealloc { [self.timer stopTimer]; NSLog(@"%s",__func__); }
其他:使用 NSProxy 来解决循环引用,伪造一个控制器类,来避免循环引用。
代码如下:
//PFProxy.h #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface PFProxy : NSProxy //通过创建对象 - (instancetype)initWithObjc:(id)object; //通过类方法创建创建 + (instancetype)proxyWithObjc:(id)object; @end NS_ASSUME_NONNULL_END
#import "PFProxy.h" @interface PFProxy() @property (nonatomic, weak) id object; @end @implementation PFProxy - (instancetype)initWithObjc:(id)object { self.object = object; return self; } + (instancetype)proxyWithObjc:(id)object { return [[self alloc] initWithObjc:object]; } - (void)forwardInvocation:(NSInvocation *)invocation { if ([self.object respondsToSelector:invocation.selector]) { [invocation invokeWithTarget:self.object]; } } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.object methodSignatureForSelector:sel]; } @end
使用
#import "ViewController1.h" #import "PFProxy.h" @interface ViewController1 () //使用NSProxy @property (nonatomic, strong) NSTimer *timer2; @end @implementation ViewController1 - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; } - (void)viewDidLoad { [super viewDidLoad]; self.title = @"VC1"; self.view.backgroundColor = [UIColor whiteColor]; PFProxy *proxy = [[PFProxy alloc] initWithObjc:self]; self.timer2 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:proxy selector:@selector(timerHandle) userInfo:nil repeats:YES]; } //定时触发的事件 - (void)timerHandle { NSLog(@"正在计时中。。。。。。"); } - (void)dealloc { [self.timer2 invalidate]; self.timer2 = nil; NSLog(@"%s",__func__); } @end
面试题:在viewDidLoad方法中创建一个数组,问,数组对象什么时候被释放?
答:在单次runloop将要结束的时候调用AutoreleasePoolPage::pop()释放。
面试题:Autorelease为何可以嵌套调用?
答:多层嵌套就是多次插入哨兵对象,以区分不同的Autorelease
面试题:什么时候需要手动创建AutoreleasePool?
答:在for循环中alloc图片数据等内存消耗较大的场景下。