内存管理Part 3
本章主要讨论跟UIViewController 相关的记忆体相关问题,严格说起来比较像是在讨论UIViewController 的life cycle。
总之,我们要回答的问题是─当我们建立了一个UIViewController之后,Xcode给我们的template中,会叫我们去实现一个叫做didReceiveMemoryWarning:
的method,然后你可能从一些相关文件上知道,当系统内存不够的时候,我们应该要在这个method里头释放一些内存,那么,有哪些内存是应该要释放的?我们应该怎么实现这个method?
内存不足警告(Memory Warnings)
在桌面操作系统中,如果内存空间不足,应用程序使用的内存空间不足了,这时候操作系统会自动将内存中的部分资料,存入磁盘的虚拟内存( Virtaul Memory)当中,需要使用的时候,再从虚拟内存中加载回缓存的变量,Mac OS X就有这样的机制。
iOS在发展之初到现在,都没有虚拟内存,而是会在内存快要用完的时候,对应用程式发出内存不足的警告,要求释放一些可以暂时不需要用到的实例,让应用程序可以有足够的内存空间继续运行。如果无视内存不足警告,继续放任内存空间变大,系统最后便会强制要求终止应用程序。
在内存不足的时候,除了会对UIApplication的delegate (就是所谓的AppDelegate)呼叫applicationDidReceiveMemoryWarning:
之外,也会对系统中所有的UIViewController呼叫didReceiveMemoryWarning:
。如果我们想要知道哪些内存是可以在didReceiveMemoryWarning:
释放的,不妨先回顾一下在iOS 6之前,iOS是怎么做的—
从iOS 问世到iOS 5,只要发生内存不足,就会把所有不在堆栈最前面的View Controller 的view 释放掉。因为这些View Controller 的view 并不在画面上,用户根本看不到,所以暂时先放掉也没有关系。
iOS 6 之前记忆体不足时系统主动释放View 的行为
所谓不在最前景的view controller 就是:假如我们今天有一个tab bar controller,tab bar 里头有四个项目,对应到四个view controller,但是其实只会显示一个,那么,在iOS 6 之前,只要发生记忆体警告的时候,其他三个view controller 的view 就会被释放。
在navigation controller 的navigation stack 里头,也只有最上面的view controller的画面需要显示,其他view controller 的view 也可以被释放。
所以,如果你曾经在iOS 6 之前的环境上开发过iOS App,可能会遇到一个奇怪的bug:你把一些状态直接记录在view 里头,像是改变了一些label 里头的文字,但是继续做了一些操作,然后回到这个view 之后,发现view 莫名其妙的回复到初始值,原本放在label 的文字不见了,其实就是遇到了记忆体警告的结果。
UIViewController 与View 的关系
UIViewController
负责管理在应用程序中每个会用到的画面,最主要的property就是view
,而这个property是使用Lazy Loadingpattern实现。Lazy Loading就是:我们要去使用某个实例的时候,我们才去建立那个实例,避免在控件初始时就建立了所有的property,而达到让初始化实例这个动作加速的效果。1
当我们在透过alloc
、init
或initWithNibName:bundle:
建立View Controller的时候,并不会马上建立view,而是当我们呼叫view
这个属性的时候才会建立。我们以下面的代码为例:
// 建立 MyViewController 的 instance,這時候還沒有建立 view
MyViewController *controller = [[MyViewController alloc]
initWithNibName:NSStringFromClass([MyViewController class]) bundle:nil];
// 在被加入到 navigation stack 的時候,會去呼叫 [controller view]
// 這時候 view 才被建立起來
[navigationController pushViewController:controller animated:YES];
[controller release];
用Lazy Loading 的方式实现一个getter的方式大致如下。在我们自己的代码中,想要有效使用内存,我们也可以尝试这么写。
- (UIView *)view
{
if (!_view) {
_view = [[UIView alloc]
initWithFrame:[UIScreen mainScreen].bounds];
}
return view;
}
不过,UIViewController
在还没有view,而要去建立view的时候,会呼叫的其实是loadView
这个method,在view成功载入之后,则会呼叫viewDidLoad
。我们虽然不知道苹果到底是怎么实现UIViewController
,但不外乎类似这样:
- (UIView *)view
{
if (!_view) {
[self loadView];
if (_view) {
[self viewDidLoad];
}
}
return view;
}
所以,如果你有天不小心写出像下面的程式码,就会进入死循环:因为呼叫[self view]
的时候发现没有view,就会呼叫loadView
,但loadView
又去呼叫[self view]
。
- (void)loadView
{
[self view];
}
在iOS 6之前,如果某个View Controller不在最上层,发出内存警告时,系统就会通知这些View Controller把view指向nil;而当我们再次需要使用这个View Controller的时候,就会因为呼叫到view
,而把view重新载入回来。
所以我们要注意,viewDidLoad
并不是UIViewController
的Initializer,—虽然我们在开始使用某个view controller的时候,一定会调用到一次viewDidLoad
,我们也通常会在这个地方,做一些初始化这个view controller的事情—但viewDidLoad
是有机会在View Controller的Life Cycle中被重复调用好几遍—在建立了view之后,view也可以再次指向nil,所以view controller可能会被重复释放与载入view,viewDidLoad
也会被重复调用。
所以在iOS 6之前,你曾经遇到某个View Controller回复到初始值这样的问题,就是:原本有状态的view因为内存警告被释放了,而我们如果在viewDidLoad
再次被呼叫的时候,没有正确还原状态,自然只有初始状态的view。
iOS 如何知道哪个View Controller 位在最上层?
那么,view controller自己怎么知道自己位在最前景呢?其实很简单:view controller被放到最上层时,会被呼叫到viewWillAppear:
以及viewDidAppear:
,离开最上层时,会调用viewWillDisappear:
与viewDidDisappear:
。
只有调用过viewWillAppear:
以及viewDidAppear:
,而没有调用过viewWillDisappear:
与viewDidDisappear:
的View Controller,就是位在最前面的View Controller。
我们经常会overrideviewWillAppear:
这些method,在做override的时候,应该要呼叫一次super的方法,因为super的viewWillAppear:
这些method其实作了一些必要的事情,在iOS 6之前是用来确保哪些view该被释放,虽然苹果推出iOS 6时,或许是认为像是iPhone 5这样的装置在可用资源上远远超越过去的硬件,因此不再刻意释放view,但调用一下super的方法还是比较保险。
所以我们应该要在didReceiveMemoryWarning:
做什么?
从过去的经验来看,不在最上层的view 其实可以释放,在iOS 6 之后,当我们遇到内存不足时,我们也可以选择性的决定要不要释放view,像web view 这种内存怪物在没用到的时候实在应该要放掉。
以下是苹果的范例程式:
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
if ([self.view window] == nil) {
self.view = nil
}
}
相关阅读
1 .也可以参见Wikipedia上的说明 http://en.wikipedia.org/wiki/Lazy_loading ↩