iPhone下每个App可用的内存是被限制的,如果一个App使用的内存超过20M,则系统会向该App发送Memory Warning
(内存警告)消息,收到此消息后,App必须正确处理,否则可能出错或出现内存泄漏。
目录
- 流程
- iOS 6以上版本的App对内存警告处理方法
- 相关方法
- loadView
- viewDidLoad
- viewDidUnload
- initWithCoder:
- awakeFromNib
- 结论
官方文档:
重写didReceiveMemoryWarning
方法:
- (void)didReceiveMemoryWarning {[super didReceiveMemoryWarning];// Dispose of any resources that can be recreated.NSLog(@"didReceiveMemoryWarning");
}
流程
当应用可用内存过低导致系统发出内存警告的时候,便会触发didReceiveMemoryWarning
方法。App收到内存警告会调用:
UIApplication::didRecieveMemoryWarning -> UIApplicationDelegate::applicationDidRecieveMemoryWarning
然后调用当前所有的viewController
进行处理,因此处理的主要工作在viewController。
创建viewController时,执行顺序是loadView -> viewDidLoad
。
当收到内存警告时,didRecieveMemoryWarning会判断当前viewController的view是否显示在window
上:
- 如果viewController未显示(在后台),会执行
didRecieveMemoryWarning -> viewDidUnload
,前者会自动将viewController的view及其所有子view全部销毁 - 如果viewController当前正在显示(在前台),则只执行
didRecieveMemoryWarning
,viewController的view不会被销毁 - 当重新显示该viewController时,执行过
viewDidLoad
的viewController(即原来在后台)会重新调用loadView -> viewDidLoad
。
iOS 6以上版本的App对内存警告处理方法
- (void)didReceiveMemoryWarning {[super didReceiveMemoryWarning]; //super调用的此方法即使没有显示在window上(在后台),也不会自动的将self.view释放。//此处做兼容处理需要加上iOS 6.0的宏开关,保证是在6.0下使用的,6.0以前屏蔽以下代码,否则会在下面使用self.view时自动加载viewDidUnLoadif ([[UIDevice currentDevice].systemVersion floatValue] >= 6.0) {//需要注意的是self.isViewLoaded是必不可少的,其他方式访问视图会导致它加载 ,在WWDC视频也忽视这一点。if (self.isViewLoaded && !self.view.window) { // 是否是正在使用的视图//codeself.view = nil;// 目的是再次进入时能够重新加载调用viewDidLoad函数。}}
}
- iOS 6之前:
viewDidUnload
和didReceiveMemoryWarning
都会被调用。 - iOS 6之后:
viewDidUnload
不会被调用didReceiveMemoryWarning
依然被调用。系统会自动处理View相关的内存,我们不用担心。也就是说不再支持viewDidUnload
了。
官方文档的解释是:系统会自动控制大的View所占用的内存,其他小的View所占用的内存是极其微小的,不值得为了省内存而去清理然后在重新创建。 如果你需要在内存警告的时候释放业务数据或者做些其他的特定处理,你可以实现didRecieveMemory
方法。
苹果官方给出的相关解释方案总是美好的,但现实往往是残酷的:
- 我们的工程是ARC的。
- 我们会在viewController里面强持有(strong)大量子View得成员变量
- 我们实现了大量的viewDidUnload函数来释放(2)里面持有的那个子View
让我们看看我们的代码到了iOS6以后会发生什么事情。因为所有的子View都是strong
持有的,这样会导致,即使系统内存警告导致了View的回收,他们也不会被真正的释放。于是乎,我们的程序可能就在后台被系统频繁的杀死。
栗子🌰:
一个App有三个tab(选项卡界面元素,比如“首页”、“通知”和“消息”的tabs):tabA、tabB、tabC(都从viewController继承,并且都实现了
didRecieveMemoryWarning
)。当程序启动时,默认显示tabA,这时,tabA的viewDidLoad
被调用,并且加载数据显示给用户,然后切换到tabB,B会重复A的加载过程。
这时系统产生了一个内存警告,tabA、tabB、tabC三个对象都会受到警告。
- tabA对象:因为它已经不在当前UI显示了,所以满足
[self.view window] == nil
,相关View被释放。- tabB对象:正在显示,所有didReceiveMemoryWarning什么也不会干。
- tabC对象:最悲惨,从来没有显示过,viewDidLoad从来没调用过,也没有显示过。然后有个self.view .这句的调用会导致一个结果,就是C对象的viewDidLoad会被调用一次,于是他的逻辑就是释放前先创建一次,然后再把自己释放,是不是很悲剧。(所以apple给的方案也不一定完美靠谱)。
iOS 6之后,应该做的:
- 不要把子View当成员变量来持有,使用
tag
来操作(其实不管在哪个版本最后都这么做)。 - 不需要实现
viewDidLoad
,由系统自己来控制相关的内存释放。 - 在需要的时候实现
didRecieveMemory
来释放一些业务数据减少内存的占用,不要操作UIView。
相关方法
loadView
永远不要主动调用这个函数。viewController会在View的属性被请求并且当前view值为nil时调用这个函数。
如果手动创建View,应该重写这个函数,且不要在重写的时候调用[super loadView]
方法。
如果用Interface Building
创建View并初始化viewController,那就意味着使用initWithNibName:bundle:
方法,这时就不应该重写loadView
函数。
这个方法的默认实现是这样:
- 寻找有关可用的Nib文件的信息,根据这个信息加载Nib文件(所以Nib的加载过程是在loadView中完成的)。
- 如果没有有关Nib文件的信息,默认创建一个空白的UIView对象,然后把对象赋值给viewController的主View。
所以,如果决定重写这个方法时,也应该完成上述步骤,并把子类的View赋给view属性(创建的View必须是唯一的实例,并且不被其他任何Controller共享),而且重写的这个函数不应该调用super
,这也是为了保持主View与Controller的单一映射关系。
viewDidLoad
这个方法在Controller加载了相关的Views
后被调用,而且跟这些Views存储在Nib
文件里还是在loadView
方法中生成无关。
这个方法的作用主要是进一步初始化Views
。viewDidLoad通常负责的是View及其子View被加载进内存之后的数据初始化的工作,即视图的数据部分的初始化。在iOS 3.0以及更高版本中,你应该重载viewDidUnload
方法来释放任何对View的引用或者它里面的内容(子View等等)。
其多数情况下是做Nib文件的后续工作。
viewDidUnload
iOS 6之前这个方法是viewDidLoad
的对立方法,在程序内存欠缺时此方法被controller调用,来释放View以及View相关的对象。
通常情况下,未显示在界面的viewController是UINavigationController Push栈中未在栈顶的viewController,或是UITabBarController中未显示的子viewController。这些viewController都在内存警告事件发生时,让系统自动调用viewDidUnload
方法。由于viewController通常保存着view以及相关对象object的引用,所以必须使用这个方法来放弃这些对象的所有权以便内存回收,但不要释放哪些难以重建的数据。
通常controller会保存Nib文件建立的Views的引用,但是也可能会保存着loadView方法创建的对象的引用。最完美的方法是使用合成器方法:self.myCustomView = nil;
,这样合成器会release
掉这个view,如果没有使用属性,那么得显式释放这个view。
iOS 6之后,由于viewDidUnload
事件在任何情况下都不会被触发,所以苹果在文档中建议,应该将回收内存的相关操作移到另一个函数didReceiveMemoryWarning
中。但是如果仅仅写成(以下为错误示例):
- (void)didReceiveMemoryWarning {[super didReceiveMemoryWarning];if (self.isViewLoaded && !self.view.window) {self.view = nil;}
}
iOS 6以后,不建议将view置为nil
的原因如下:
- UIView有一个CALayer成员变量,CALayer用于将自己画到屏幕上。
- CALayer是一个bitmap图像的容器类,当UIView调用自身的drawRect时,CALayer才会创建bitmap图像类。
- 具体占内存的其实是一个bitmap图像类,CALayer只占48Bytes,UiView只占96Bytes。而一个 iPad的全屏UIView的bitmap类会占到12MB的大小。
- 在iOS6,当系统发出内存警告时,系统会自动回收bitmap类,但是不回收UIView和CALayer类。这样既能回收大部分内存,又能在需要bitmap类时,通过调用UIView的drawRect:方法重建。
苹果系统对上面的内存回收做了一个优化:
- 当一段内存被分配时,它会被标记成 “In Use”,以防止被重复使用。当内存被释放时,这段内存被标记为“Not in use”,这样有新的内存申请时,这块内存就可能被分配给其他变量。
- CALayer包括的具体的bitmap内容的私有成员变量类型为CABackingStore,当收到内存警告时,CABackingStore类型的内存会被标记为Volatile类型,表示这块内存可能再次被原变量使用。
这样,有了上面优化后,当收到内存警告时,虽然所有的CALayer所包含的bitmap内存被标记成volatile了,但是只要这块内存没有被复用,当需要重建bitmap内存时,可以直接被复用,避免了再次调用UIView的drawRect:
方法。
简言之,iOS6之后,不需要做任何以前viewDidUnload的事情,更不需要把以前viewDidUnload 的代码移到didReceiveMemoryWarning方法中。
initWithCoder:
使用了Interface Building创建对象并实例化时会调用initWithCoder:
方法。
Interface Builder (IB) 是 Xcode 内置的图形界面编辑器,它允许我们用可视化的方式设计用户界面。当你在 Interface Builder 中创建对象时,实际上你是在使用 .xib
文件或 .storyboard
文件,这两种文件都基于 XML 文件格式来描述界面布局和配置。
反序列化是一种将数据流(在这种情况下是 XML 文件形式)转化成对象的过程。对于 Interface Builder 使用的 .xib
和 .storyboard
文件:
-
界面描述:
.xib
和.storyboard
文件包含了界面布局的描述和对象模型,例如视图(controller)、视图(view)、约束等。 -
加载和解析:当你的项目加载并运行界面时,这个描述是被应用的。系统将解析这些文件中的 XML 描述,并将它转换成实际的运行时对象。
-
系统完成解析:这个过程由系统自动完成,开发者仅需要在 Interface Builder 中定义界面并保存文件,系统会负责剩余的工作。
因此,使用Interface Builder创建对象的过程,实际上就是定义.xib
或.storyboard
文件的内容。随后,系统的运行时会处理这些文件,反序列化文件内容,从而创建相应的对象。开发者不需要手动进行这些反序列化工作,它作为加载过程的一部分已经被抽象掉了。
awakeFromNib
Interface Building创建的对象被实例化的时候调用。
当initWithCoder:
被调用之后,也会调用awakeFromNib
。但是awakeFromNib相对于initWithCoder: 有个优势,后者在调用时虽然subViews已经被添加到图层中去了,但是还没有引用。而在awakeFromNib调用时,各种IBOutlet也都连接好了。
IBOutlet
是Objective-C中的一种语言特性,它是用于连接Interface Builder(IB)与静态类型对象的一个标记与工具。它允许开发者在用户界面设计软件中设计元素,如按钮、标签等,然后在码中引用这些元素以便编程时使用。
这里是IBOutlet
的详细说明:
-
Objective-C属性:
IBOutlet
通常与@property一起使用,用于创建一个公开接口来访问私有的数据。 -
连接界面与代码:通过将 Interface Builder 中的对象链接到具有
IBOutlet
标准的属性上,你可以在各个场景(scenes)之间分享信息或行为。 -
自动布局:
IBOutlet
可以帮助Auto Layout视图或controller的布局。 -
Xcode中可视编辑:当你在Xcode的Interface Builder中设计界面时,可以拖拽从你的视图到你的代码文件(如ViewController)上,自动创建
IBOutlet
。 -
强引用:默认情况下,
IBOutlet
是strong
类型的引用,但可以在声明的时候使用不同的修饰符比如weak
来避免循环引用。
结论
所以流程应该是这样:
(loadView/nib
文件)来加载view
到内存 ——>viewDidLoad
函数进一步初始化这些view ——>内存不足时,调用viewDidUnload
函数释放views —->当需要使用view时又回到第一步
UIViewController完整的生命周期: