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的总结:

  • Arrays: 有序的一组值。使用index来lookup很快,使用value lookup很慢,插入/删除很慢。
  • Dictionaries: 存储键值对。用键来查找比较快。
  • Sets: 无序的一组值。用值来查找很快,插入/删除很快。

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提供了几种收集低内存警告的方法:

  • 在app delegate中使用applicationDidReceiveMemoryWarning:的方法
  • 在你的自定义UIViewController的子类(subclass)中覆盖didReceiveMemoryWarning
  • 注册并接收 UIApplicationDidReceiveMemoryWarningNotification的通知
    一旦收到这类通知,你就需要释放任何不必要的内存使用。

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

11. 重用大开销对象

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

Demo地址:iOS 内存优化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容

  • 一. 视图控制对象通过alloc和init来创建,但是视图控制对象不会在创建的那一刻就马上创建相应的视图,而是等到...
    iOS菜鸟攻城狮阅读 605评论 0 7
  • 1. 用ARC管理内存 ARC(Automatic ReferenceCounting, 自动引用计数),它避免了...
    anyurchao阅读 2,823评论 0 16
  • 1、运行MemoryProblems后,运行崩溃出现EXC_BAD_ACCESS,启动NSZombieEnable...
    雒琰湦阅读 1,091评论 0 1
  • 引起内存泄漏的原因 引起内存泄漏的原因主要有三类,如下 循环引用 强引用 非OC对象 1、循环引用。最简单的循环引...
    荒漠现甘泉阅读 152评论 0 2
  • 1. 避免内存泄漏 ① 避免对象之间循环引用(代理一定要弱引用)② block 中对象的循环引用、添加的通知在销毁...
    Install_be阅读 166评论 0 0