经验篇iOS 收藏面试疯子

iOS 内存优化

2018-02-23  本文已影响344人  iOS_肖晨

简述:

本应释放的内存没有释放,导致可用空间减少的现象。
举个例子:你dismiss了一个视图控制器,但是最终却没有执行这个视图控制器的dealloc方法,就会导致内存泄露。
目前遇到的导致内存泄漏比较严重的有这几个地方:

1. Timer

NSTimer经常会被作为某个类的成员变量,而NSTimer初始化时要指定self为target,容易造成循环引用。 另一方面,若timer一直处于validate的状态,则其引用计数将始终大于0。

- (instancetype)init {
    self = [super init];
    if (self) {
        _timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"%@ called!", [self class]);
        }];
        [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor yellowColor];
    
    [_timer fire];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    // 控制器视图将要消失的时候清除 timer 不失为一个好时机。
    [self cleanTimer];
}

- (void)cleanTimer {
    [_timer invalidate];
    _timer = nil;
}

- (void)dealloc {
    // 应该在更合适的地方释放掉timer,否则会造成循环引用,导致控制器无法释放
//    [self cleanTimer];
    NSLog(@"%@ dealloc!!!", [self class]);
}

这个例子中控制器无法释放,造成内存泄漏,原因如下:
从timer的角度,timer认为调用方(控制器)被析构时会进入 dealloc,在 dealloc 可以顺便将 timer 的计时停掉并且释放内存;
但是从控制器的角度,他认为 timer 不停止计时不析构,那我永远没机会进入 dealloc。循环引用,互相等待,子子孙孙无穷尽也。
问题的症结在于-(void)cleanTimer函数的调用时机不对,显然不能想当然地放在调用者的 dealloc 中。一个比较好的解决方法是开放这个函数,在更合适的位置(比如在- (void)viewWillDisappear:(BOOL)animated;中)调用来清理现场。

2. Delegate

开发过程中使用retain修饰符或无修饰符(无修饰符默认strong),导致很多应该释放的视图控制器都没释放。这个修改很简单:将修饰符改成weak即可。
注:为什么不用assign, 如果用assign声明的变量在栈中可能不会自动赋值为nil,就会造成野指针错误!
weak声明的变量在栈中就会自动清空,赋值为nil。

// 如果此处用 retain 修饰,则添加这个代理方法的控制器就会由于 delegate 没有清空而无法释放,造成内存泄露。
//@property (retain, nonatomic) DelegateViewDelegate delegate;
@property (weak, nonatomic) DelegateViewDelegate delegate;

3. Block

block容易出现内存泄露,根本原因是存在对象间的循环引用问题(对象a强引用对象b,对象b强引用对象a)。

举例说明:
创建一个对象并为对象添加一个block属性

@interface BlockObject : NSObject

@property (copy, nonatomic) dispatch_block_t block;

@end

为控制器添加三个属性,其中包括新创建的对象属性

@interface BlockViewController ()

// self 对 object 对象进行强引用
@property (strong, nonatomic) BlockObject *object;
@property (assign, nonatomic) NSInteger index;
@property (copy, nonatomic) dispatch_block_t block;

@end

造成内存泄露写法一:

_object = [[BlockObject alloc] init];

[_object setBlock:^{
    // object 对象对 self (成员变量或属性)进行强引用,就会造成循环引用
    self.index = 1; // _index = 1;
}];

解决方式:

_object = [[BlockObject alloc] init];

// 先将 self 转成 weak,之后在 block 内部转成 strong 使用,是常见的解决方案。
__weak typeof(self)weakSelf = self;
[_object setBlock:^{
    __strong typeof(self)strongSelf = weakSelf;
    strongSelf.index = 1;
}];

用全局变量的写法也会造成内存泄露:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 此处会发生内存泄露,因为 self 添加了全局 block,self 对此 block 存在强引用。
    [self executeBlock2:^{
        self.index = 1;
    }];
}

- (void)executeBlock2:(dispatch_block_t)block {
    // 这个 _block 全局变量就是内存泄露的原因,如果 block 内部使用weakSelf就会打破这个循环了。
    _block = block;
    if (block) {
        block();
    }
}

4. Image

关于图片加载占用内存问题:
imageNamed: 方法会在内存中缓存图片,用于常用的图片。
imageWithContentsOfFile: 方法在视图销毁的时候会释放图片占用的内存,适合不常用的大图等。

#pragma mark - 图片加载内存占用问题 -
// 初始化时内存占用为 42M
// 加载之后为 56M,控制器dealloc 之后内存并没有明显减少
cell.imageView.image = [UIImage imageNamed:imageName];
    
// 加载之后为 56M,控制器dealloc 之后内存明显减少,回到之前水平 44M 左右
NSString *file = [[NSBundle mainBundle] pathForResource:imageName ofType:nil];
cell.imageView.image = [UIImage imageWithContentsOfFile:file];

所以需要时刻注意图片操作是否合理,避免大量占用内存。
注意:

  1. imageWithContentsOfFile: 方法无法读取.xcassets里的图片。
  2. imageWithContentsOfFile: 方法读取图片需要加文件后缀名如png,jpg等。

5. Table View

Table view需要有很好的滚动性能,不然用户会在滚动过程中发现动画的瑕疵。
为了保证table view平滑滚动,确保你采取了以下的措施:

1.正确使用reuseIdentifier来重用cells。
2.将所有不需要透明的视图 opaque(不透明)设置为YES,包括cell自身。
3.缓存行高。
4.如果cell内现实的内容来自web,使用异步加载,缓存请求结果。
5.使用shadowPath来画阴影。
6.减少subviews的数量。
7.尽量不适用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果。
8.使用正确的数据结构来存储数据。
9.使用rowHeight, sectionFooterHeightsectionHeaderHeight来设定固定的高,不要请求delegate。

6. 不要阻塞主线程

永远不要使主线程承担过多。因为UIKit在主线程上做所有工作,渲染,管理触摸反应,回应输入等都需要在它上面完成。
一直使用主线程的风险就是如果你的代码真的block了主线程,你的app会失去反应。
大部分阻碍主进程的情形是你的app在做一些牵涉到读写外部资源的I/O操作,比如存储或者网络。

7. 选择正确的Collection

学会选择对业务场景最合适的类或者对象是写出能效高的代码的基础。当处理collections时这句话尤其正确。
一些常见collection的总结:

8. 打开gzip压缩

大量app依赖于远端资源和第三方API,你可能会开发一个需要从远端下载XML, JSON, HTML或者其它格式的app。
问题是我们的目标是移动设备,因此你就不能指望网络状况有多好。一个用户现在还在edge网络,下一分钟可能就切换到了3G。不论什么场景,你肯定不想让你的用户等太长时间。
减小文档的一个方式就是在服务端和你的app中打开gzip。这对于文字这种能有更高压缩率的数据来说会有更显著的效用。
好消息是,iOS已经在NSURLConnection中默认支持了gzip压缩,当然AFNetworking这些基于它的框架亦然。像Google App Engine这些云服务提供者也已经支持了压缩输出。

9. 重用和延迟加载(lazy load) Views

更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。
这里我们用到的技巧就是模仿UITableViewUICollectionView的操作:不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。
这样的话你就只需要在滚动发生时创建你的views,避免了不划算的内存分配。
创建views的能效问题也适用于你app的其它方面。想象一下一个用户点击一个按钮的时候需要呈现一个view的场景。有两种实现方法:

  1. 创建并隐藏这个view当这个screen加载的时候,当需要时显示它;
  2. 当需要时才创建并展示。
    每个方案都有其优缺点。用第一种方案的话因为你需要一开始就创建一个view并保持它直到不再使用,这就会更加消耗内存。然而这也会使你的app操作更敏感因为当用户点击按钮的时候它只需要改变一下这个view的可见性。
    第二种方案则相反-消耗更少内存,但是会在点击按钮的时候比第一种稍显卡顿。

10. 处理内存警告

一旦系统内存过低,iOS会通知所有运行中app。在官方文档中是这样记述:
如果你的app收到了内存警告,它就需要尽可能释放更多的内存。最佳方式是移除对缓存,图片object和其他一些可以重创建的objects的strong references.

幸运的是,UIKit提供了几种收集低内存警告的方法:

例如,UIViewController的默认行为是移除一些不可见的view,它的一些子类则可以补充这个方法,删掉一些额外的数据结构。一个有图片缓存的app可以移除不在屏幕上显示的图片。
这样对内存警报的处理是很必要的,若不重视,你的app就可能被系统杀掉。
然而,当你一定要确认你所选择的object是可以被重现创建的来释放内存。一定要在开发中用模拟器中的内存提醒模拟去测试一下。

11. 重用大开销对象

一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。然而,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。
想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。
注意如果你要选择第二种方法,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似。

Demo地址:iOS 内存优化

上一篇 下一篇

猜你喜欢

热点阅读