OC面试题 六、内存管理

294 阅读9分钟

CADisplayLink、NSTimer使用注意

CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用 解决方案

  1. 使用block

image.png 2. 使用代理对象(NSProxy)进行消息转发

image.png

GCD定时器

  • NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时
  • 而GCD的定时器会更加准时

image.png

copy和mutableCopy

image.png

  • 浅拷贝就是对内存地址的复制让目标对象指针和源对象指针指向同一片内存空间

  • 深拷贝就是拷贝地址中的内容,让目标对象产生新的内存区域,且将源内存区域中的内容复制到目标内存区域中

面试题:nonatomic和atomic的区别

  • atomic是原子性的,nonatomic是非原子性的
  • atomic和nonatomic系统自动生成的setter和getter方法不一样,atomic生成的是有加锁的,而nonatomic是没有加锁的
  • atomic由于加锁,访问速度比nonatomic慢,但不一定是线程安全的,nonatomic访问速度快,线程不安全
  • atomic加锁只是保证setter和getter的有序性
atomicnonatomic
原子性
运行速度
线程安全

浅拷贝和深拷贝

浅拷贝:并不拷贝对象本身,只是对指向对象的指针进行拷贝,改变原对象的属性会影响新的对象,内存中本质上还是一个对象。 深拷贝:直接拷贝生成一个新对象,改变原对象的属性不会影响新的对象,在内存中出现了两个独立的对象本身。

1、浅拷贝(指针copy)

1)相当于对指向对象的指针进行复制,产生一个新的指向对象的指针;

2)就有两个指针指向同一个对象;

3)这个对象销毁后,两个指针都应置空;

4)对象引用计数+1

2、深拷贝Copy(内容copy)

深copy不仅会复制对象本身,而且会递归复制每个指针类型的实例变量,直到两个对象没有任何公共的部分。

1)相当于对对象进行复制,产生一个新的对象;

自旋锁、互斥锁比较?

自旋锁互斥锁
等待时间
处理器多核单核
CPU资源紧张不紧张
代码调用经常调用但竞争少竞争激烈
代码复杂度一般代码复杂,循环量大

weak实现原理?

  1. 初始化一个weak对象时,runtime会调用一个objc_initWeak函数初始化一个新的weak指针指向该对象的地址

  2. 在objc_initWeak函数中会继续调用objc_storeWeak函数,在这个过程是用来更新weak指针的指向,同时创建对应的弱引用表WeakTable

  3. 在对象释放时,会调用clearDeallocating函数,这个函数会根据对象地址获取所有weak指针数组,然后遍历这个数组置为nil。最后把该条对象的记录从weak表中删除

  4. weak指针指向对象,不会让对象的引用计数增加,所以block内部就不会持有self对象,破解循环引用

OC对象的内存管理

  • 在iOS中,使用引用计数来管理OC对象的内存
  • 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
  • 内存管理的经验总结
    • 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
    • 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
  • 可以通过以下私有函数来查看自动释放池的情况
    • extern void _objc_autoreleasePoolPrint(void);

自动释放池

自动释放池的底层数据结构

  • 自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage
  • 调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
  • 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
  • 调用pop方法时传入一个POOL_BOUNDARY的内存地址会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY

Runloop和Autorelease

  • iOS在主线程的Runloop中注册了2个Observer
  • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
  • 第2个Observer

监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()

监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

NSTimer循环引用的几种解决方案

  1. 循环引用产生的原因:

timer强引用target,即timer强引用vc,然而vc并没有强引用timer,哪来的vc与timer循环引用?但是,如果vc没有强引用timer,timer是如何存活的? 其实,默认将timer加入到currentRunLoop中,currentRunLoop会强引用timer,而currentRunLoop就是mainRunLoop,mainRunLoop一直存活,所以timer可以存活。

  1. 解决方案:

  • 调用invalidate的时候,会执行destroy操作 选择合适的时机手动释放timer
[self.timer invalidate];
  • timer使用block方式添加Target-Action

还需要注意block可能引起的循环引用,所以使用weakSelf

  • 通过proxy转发的形式解决

image.png

考虑到循环引用的原因,改方案就是需要打破这些相互引用关系,因此添加一个中间件,弱引用self,同时timer引用了中间件,这样通过弱引用来解决了相互引用

proxy弱引用vc,所以vc可以释放,当vc执行dealloc,在dealloc内部销毁timer即可

iOS内存泄漏检测方法

  1. 通过Xcode中Product->Analyze静态分析代码,找出潜在的内存泄漏。
  2. 使用Xcode自带工具Instruments来检测内存泄漏。

方案1的优点是不需要写代码,只需要运行一次,就能检测出潜在的内存泄漏;缺点是由于是静态代码检查,无法覆盖全部场景(比如动态运行场景),有可能误报。

方案2的优点是不需要写代码,直接运行Instruments工具进行检测,适用于开发、测试来使用;缺点是需要一个一个页面去点击。

  1. 微信读书开源的内存泄漏检测库MLeaksFinder

功能:

  • 能检测出内存泄漏和循环引用,并且弹框提醒。
  • 只在Debug模式下起作用,Release不起作用。
  • 支持检查VC和View里面任意对象的内存泄漏。

如何检测内存泄漏?

在viewDidDisappear调用之后,过2s后,VC、它的childViewControllers、presentedViewController、VC的view及view的子view肯定要立即销毁。通过hook掉UIViewController的viewDidDisappear、viewWillAppear、dismissViewControllerAnimated方法以及UINavigationController的pushViewController、popViewControllerAnimated、popToViewController、popToRootViewControllerAnimated方法来实现。

Tagged Pointer

内存优化之Tagged Pointer

  1. TaggerPointer: 并不是一个类,它是适用于64位处理器的一个内存优化机制,专门用来存储小对象,当存储不下时,则转为对象!例如NSString、NSNumber和NSDate等对象进行优化。

  2. 指针不再是地址了,而是经过标识过的值。它不再是一个对象了,只是普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free

  3. 在内存读取上有着3倍的效率,创建时比以前快106倍。

注意:Tagged Pointer并不是真正的对象,而是一个伪对象,对象都有isa指针,而Tagged Pointer是没有的,因为它不是真正的对象,不能直接访问Tagged Pointer的isa!

内存泄漏问题

  1. 循环引用(Retain Cycle)

在作为self的property的block中,使用self指针导致self被block强引用,形成引用循环。

  1. Delegate/NSNotification 我们在使用代理设计模式的时候,一定要注意将 delegate 变量声明为 weak 类型,像这样如使用strong或别的类型修饰的话将会导致循环引用,导致dealloc()不会被调用NSNotification没有移除通知等都会触发一些意想不到的后果

  2. Block

目前在项目中出现的内存泄漏大部分是因为block的问题。

在 ARC 下,当 block 获取到外部变量时,由于编译器无法预测获取到的变量何时会被突然释放,为了保证程序能够正确运行,让 block 持有获取到的变量,向系统声明:我要用它,你们千万别把它回收了!然而,也正因 block 持有了变量,容易导致变量和 block 的循环引用,造成内存泄露。

  1. NSTimer NSTimer在释放前,一定要调用[timer invalidate],不调用的后果就是NSTimer无法释放其target,如果target正好是self,则会导致引用循环。
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay

系统依靠一个timer来保证延时触发,但是只有在runloopdefault mode的时候才会执行成功,否则selector会一直等待runloop切换到default mode。根据我们之前关于timer的说法,在这里其实调用performSelector:afterDelay:同样会造成系统对target强引用,也即retain住。这样子,如果selector一直无法执行的话(比如runloop不是运行在default model下),这样子同样会造成target一直无法被释放掉,发生内存泄露。怎么解决这个问题呢?其实很简单,我们在适当的时候取消掉该调用就行了,系统提供了接口:

+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget
  1. Image内存过大
  2. Foundation与CoreFoundation的相互引用也会造成内存泄漏
  3. AFN 的NSURLSession不能释放
//解决办法:
//修改AFHTTPSessionManager 的manager方法,替换manager;
//或继承其,自己写个manager方法
//另一种写法,两个单例:
+ (AFHTTPSessionManager *)sharedHTTPSession{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    manager = [AFHTTPSessionManager manager];
    manager.requestSerializer.timeoutInterval = 30;
    [manager.requestSerializer  setValue:@"XMLHttpRequest" 
forHTTPHeaderField:@"X-Requested-With"];
});
return manager;
}
+ (AFURLSessionManager *)sharedURLSession{
static dispatch_once_t onceToken2;
dispatch_once(&onceToken2, ^{
    urlsession = [[AFURLSessionManager alloc] initWithSessionConfiguration:
[NSURLSessionConfiguration defaultSessionConfiguration]];
});
return urlsession;
}
  1. 大次数循环内存暴涨问题 有道比较经典的面试题,查看如下代码有何问题:
for (int i = 0; i < 100000; i++) {
    
    NSString *string = @``"Abc"``;
    
    string = [string lowercaseString];
    
    string = [string stringByAppendingString:@``"xyz"``];
    
    NSLog(@``"%@"``, string);
    
}

该循环内产生大量的临时对象,直至当前runloop休眠前才释放掉,可能导致内存泄漏,解决方法为在循环中创建自己的autoReleasePool,及时释放占用内存大的临时变量,减少内存占用峰值

 for (int i = 0; i < 100000; i++) {
        
        @autoreleasepool {
            
            NSString *string = @"Abc";
            
            string = [string lowercaseString];
            
            string = [string stringByAppendingString:@"xyz"];
            
            NSLog(@"%@", string);
            
        }
        
    }

内存泄漏的查询

  1. Analyze 静态分析 (command + shift + b)也就是编译

    • 逻辑错误:访问空指针或未初始化的变量等
    • 内存管理错误:如内存泄漏等
    • 声明错误:从未使用过的变量
    • Api调用错误:未包含使用的库和框架
  2. Instruments中的Leak动态分析内存泄漏,product->profile ->leaks 打开工具主窗口

  3. Facebook早已开源了一款检测内存问题的三方库FBRetainCycleDetector

为什么AFNetWorking中的block不会造成循环引用问题?

系统的block或者AFN等block的调用并不在当前控制器中调用,那么这个self就不代表当前控制器,那自然也就没有循环引用的问题。