iOS开发内存警告Memory Warning和ViewController的生命周期的问题

Posted 「违规用户」

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发内存警告Memory Warning和ViewController的生命周期的问题相关的知识,希望对你有一定的参考价值。

ios开发内存警告Memory Warning和ViewController的生命周期的问题

  (2013-07-24 09:37:15) 转载
标签: 

memorywarning

 

viewcontroller的生命

 

didreceivememorywarn

viewdidunload

 

ios开发内存警告


IPhone下每个app可用的内存是被限制的,如果一个app使用的内存超过20M,则系统会向该app发送Memory Warning消息。苹果公司系统工程师建议,应用程序所占内存不应该超过20MB,开发人员圈内流传着一个粗略的经验法则:当应用程序占用了大约20MB内存时,iphone开始发出内存警告。当应用程序所占内存大约为30MB时,iphone OS会关闭应用程序。收到此消息后,app必须正确处理,否则可能出错或者出现内存泄露。app收到Memory Warning后会调用:UIApplication::didReceiveMemoryWarning -> UIApplicationDelegate::applicationDidReceiveMemoryWarning,然后调用当前所有的 viewController进行处理。因此处理的主要工作是在viewController。

我们知道,创建viewcontroller时,执行顺序是loadview -> viewDidLoad。

当收到内存警告时,如果viewcontroller未显示(在后台),会执行didReceiveMemoryWarning -> viewDidUnLoad;如果viewcontroller当前正在显示(在前台),则只执行didReceiveMemoryWarning。

当重新显示该viewController时,执行过viewDidUnLoad的viewcontroller(即原来在后台)会重新调用loadview -> viewDidLoad。

重载didReceiveMemoryWarning时,一定调用这个函数的super实现来允许父类(一般是UIVIewController)释放self.view。self.view释放之后,会调用下面的viewDidUnload函数.也就是说,尽管self.view是被处理了,但是outlets的变量因为被retain过,所以不会被释放,为了解决这个问题,就需要在viewDidUnload中释放这些retain过的outlets变量。通常controller会保存nib文件建立的views的引用,但是也可能会保存着loadView函数创建的对象的引用。最完美的方法是使用合成器方法:

self.myCertainView = nil;
这样合成器会release这个view,如果你没有使用property,那么你得自己显式释放这个view。

因此主要注意下面几个函数:

loadView 创建view,构建界面;
viewDidLoad 做些初始化工作。由于在初次创建viewcontroller和重新恢复时都会调用,因此这个函数需要注意区分不同的情况,设置正确的状态。
didReceiveMemoryWarning 释放不必须的内存,比如缓存,未显示的view等。
viewDidUnLoad 最大程度的释放可以释放的内存。比如应该释放view,这些view在调用loadview后可以重新生成。(其中成员变量释放后应设置为nil)。对于非界面的数据是否释放,需要具体分析,可以恢复的数据可以释放,不能恢复的数据就不要释放。

实际中如果viewcontroller是用xib生成的界面,则需要我们做的就比较少,主要是在viewDidLoad中恢复原来的界面状态。

如果是通过编程创建的界面,则需要做的工作就要更多些,上面4个函数中都需要进行正确处理。

iOS6.0及其以后,viewDidUnload不再有用,收到low-memeory时系统不会释放Views。
iOS6.0及以上版本的内存警告:
调用didReceiveMemoryWarning内调用super的didReceiveMemoryWarning调只是释放controller的resouse,不会释放view
处理方法:
-(void)didReceiveMemoryWarning

    [super didReceiveMemoryWarning];//即使没有显示在window上,也不会自动的将self.view释放。
    // Add code to clean up any of your own resources that are no longer necessary.
    // 此处做兼容处理需要加上ios6.0的宏开关,保证是在6.0下使用的,6.0以前屏蔽以下代码,否则会在下面使用self.view时自动加载viewDidLoad
    if ([self.view window] == nil)// 是否是正在使用的视图
       
    
        // Add code to preserve data stored in the views that might be
        // needed later.
        // Add code to clean up other strong references to the view in
        // the view hierarchy.
        self.view = nil;// 目的是再次进入时能够重新加载调用viewDidLoad函数。
    
   

但是似乎这么写相对于以前并不省事。最终我们找到一篇文章,文章中说其实并不值得回收这部分的内存,原因如下:
1. UIView是UIResponder的子类,而UIResponder有一个CALayer的成员变量,CALayer是具体用于将自己画到屏幕上的。
2. CALayer是一个bitmap图象的包装类,当UIView调用自身的drawRect时,CALayer才会创建这个bitmap图象类。
3. 具体占内存的其实是一个bitmap图象类,CALayer只占48bytes, UIView只占96bytes。而一个iPad的全屏UIView的bitmap类会占到12M的大小!
4.在iOS6时,当系统发出MemoryWarning时,系统会自动回收bitmap类。但是不回收UIView和CALayer类。这样即回收了大部分内存,又能在需要bitmap类时,根据CALayer类重建。
所以,iOS6这么做的意思是:我们根本没有必要为了几十byte而费力回收内存。
移动设备终端的内存极为有限,应用程序必须做好low-memory处理工作,才能避免程序因内存使用过大而崩溃。

low-memory 处理思路
通 常一个应用程序会包含多个view controllers,当从view跳转到另一个view时,之前的view只是不可见状态,并不会立即被清理掉,而是保存在内存中,以便下一次的快速 显现。但是如果应用程序接收到系统发出的low-memory warning,我们就不得不把当前不可见状态下的views清理掉,腾出更多的可使用内存;当前可见的view controller也要合理释放掉一些缓存数据,图片资源和一些不是正在使用的资源,以避免应用程序崩溃。

思路是这样,具体的实施根据系统版本不同而略有差异,本文将详细说明一下iOS 5与iOS 6的low-memory处理。

iOS 5 的处理
在iOS 6 之前,如果应用程序接收到了low-memory警告,当前不可见的view controllers会接收到viewDidUnload消息(也可以理解为自动调用viewDidUnload方法),所以我们需要在 viewDidUnload 方法中释放掉所有 outlets ,以及可再次创建的资源。当前可见的view controller 通过didReceiveMemoryWarning 合理释放资源,具体见代码注释。

举一个简单的例子,有这样一个view controller:
@interface MyViewController : UIViewController  
    NSArray *dataArray; 
 
@property (nonatomic, strong) IBOutlet UITableView *tableView; 
@end

对应的处理则为:
#pragma mark -
#pragma mark Memory management

- (void)didReceiveMemoryWarning
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
    // Relinquish ownership any cached data, images, etc that aren't in use.


- (void)viewDidUnload
    // Relinquish ownership of anything that can be recreated in viewDidLoad or on demand.
    // For example: self.myOutlet = nil;
    self.tableView = nil;
    dataArray = nil;
   
    [super viewDidUnload];


iOS 6 的处理
iOS 6 废弃了viewDidUnload方法,这就意味着一切需要我们自己在didReceiveMemoryWarning中操作。
具体应该怎么做呢?

1.将 outlets 置为 weak
当view dealloc时,没有人握着任何一个指向subviews的强引用,那么subviews实例变量将会自动置空。
@property (nonatomic, weak) IBOutlet UITableView *tableView;

2.在didReceiveMemoryWarning中将缓存数据置空
#pragma mark -  
#pragma mark Memory management  
- (void)didReceiveMemoryWarning 
 
    [super didReceiveMemoryWarning]; 
    // Dispose of any resources that can be recreated.  
    dataArray = nil; 

不要忘记一点,每当tableview reload 的时候,需要判断一下 dataArray ,若为空则重新创建。

兼容iOS 5 与 iOS 6
好吧,重点来了,倘若希望程序兼容iOS 5 与 iOS 6怎么办呢? 这里有一个小技巧,我们需要对didReceiveMemoryWarning 做一些手脚:
#pragma mark -
#pragma mark Memory management

- (void)didReceiveMemoryWarning

    [super didReceiveMemoryWarning];
   
    if ([self isViewLoaded] && self.view.window == nil)
        self.view = nil;
    
   
    dataArray = nil;


判断一下view是否是window的一部分,如果不是,那么可以放心的将self.view 置为空,以换取更多可用内存。

这 样会是什么现象呢?假如,从view controller A 跳转到 view controller B ,然后模拟low-memory警告,此时,view controller A 将会执行self.view = nil ; 当我们从 B 退回 A 时, A 会重新调用一次 viewDidLoad ,此时数据全部重新创建,简单兼容无压力~~

Note:
如果你好奇Apple为什么废弃viewDidUnload,可以看看Apple 的解释:
Apple deprecated viewDidUnload for a good reason. The memory savings from setting a few outlets to nil just weren’t worth it and added a lot of complexity for little benefit. For iOS 6+ apps, you can simply forget about view unloading and only implement didReceiveMemoryWarning if the view controller can let go of cached data that you can recreate on demand later.


原文地址:http://justsee.iteye.com/blog/1820588
官方文档:https://developer.apple.com/library/ios/#featuredarticles/ViewControllerPGforiPhoneOS/ViewLoadingandUnloading/ViewLoadingandUnloading.html

ViewController的生命周期和didReceiveMemoryWarning后的流程:http://blog.csdn.net/iunion/article/details/8699491


ViewController的生命周期中各方法执行流程如下: init—>loadView—>viewDidLoad—>viewWillApper—>viewDidApper—>viewWillDisapper—>viewDidDisapper—>viewWillUnload->viewDidUnload—>dealloc
跟随如下文字理解viewController对view加载过程:

1 先判断子类是否重写了loadView,如果有直接调用。之后调viewDidLoad完成View的加载。

2 如果是外部通过调用initWithNibName:bundle指定nib文件名的话,ViewController记载此nib来创建View。

3 如果initWithNibName:bundle的name参数为nil,则ViewController会通过以下两个步骤找到与其关联的nib。

A 如果类名包含Controller,例如ViewController的类名是MyViewController,则查找是否存在MyView.nib;

B 找跟ViewController类名一样的文件,例如MyViewController,则查找是否存在MyViewController.nib。

4 如果子类没有重写的loadView,则ViewController会从StroyBoards中找或者调用其默认的loadView,默认的loadView返回一个空白的UIView对象。

注意第一步,ViewController是判断子类是否重写了loadView,而不是判断调用子类的loadView之后ViewController的View是否为空。就是说,如果子类重写了loadView的话,不管子类在loadView里面能否获取到View,ViewController都会直接调viewDidLoad完成View的加载


那为什么要写成 self.myOutlet = nil; ,实际上这个语法是执行了 property 里的setter 方法,而不是一个简单的变量赋值,它干了两件事:1、老数据 release 掉,2、新数据(nil)retain(当 property 设置为 retain 的情况下),当然对 nil retain 是无意义的。如果写成 myOutlet = nil,那就是简单的把 myOutlet 指向 nil,这样内存就泄漏了,因为老数据没有 release。而如果仅仅写成 [myOutlet release] 也会有问题,因为当 view 被 dealloc 的时候会 再次 release,程序就出错了,而对 nil release 是没有问题的。

dealloc 是当前 viewController 被释放的时候,清空所有当前 viewController 里面的实体和数据来释放内存,该方法也是自动调用的,无需手动执行。举例说明当 modalView 被 dismissModalViewControllerAnimated 或者 navigationController 回到上一页的时候,这个方法就会被自动调用。因为这个页面已经不再使用了,所以可以把所有实体和数据都释放(release)掉。


开发iOS应用程序时,让程序具有良好的性能是非常关键的。这也是用户所期望的,如果你的程序运行迟钝或缓慢,会招致用户的差评。

 

  然而由于iOS设备的局限性,有时候要想获得良好的性能,是很困难的。在开发过程中,有许多事项需要记住,并且关于性能影响很容易就忘记。

  这就是为什么我要写这篇文章!本文收集了25个关于可以提升程序性能的提示和技巧。

 

  目录

 

  我把性能优化技巧分为3个不同的等级:初级、中级和高级:

 

  高级

  当且仅当下面这些技巧能够解决问题的时候,才使用它们:

 

  加速启动时间

 

  使用Autorelease Pool

 

  缓存图片 — 或者不缓存

  尽量避免Date格式化

  高级性能提升

 

  寻找一些高明的方法,让自己变为一个全代码忍者?下面这些高级的性能优化技巧可以在适当的时候让程序尽可能的高效运行!

  22) 加速启动时间

 

  能快速的启动程序非常重要,特别是在用户第一次启动程序时。第一映像对程序来说非常重要!

  让程序尽量快速启动的方法就是尽量以异步方式执行任务,例如网络请求,数据访问或解析。

 

  另外,避免使用臃肿的XIBs,因为XIB的加载是在主线程中进行的。但是记住storyboard没有这样的问题——所以如果可以的话就使用storyboard吧!

  注意:在利用Xcode进行调试时,watchdog不会运行,所在设备中测试程序启动性能时,不要将设备连接到Xcode。

  23) 使用Autorelease Pool

  NSAutoreleasePool负责释放一个代码块中的自动释放对象。一般都是由UIKit来创建的。不过有些情况下需要手动创建NSAutoreleasePool。

 

  例如,如果在代码中创建了大量的临时对象,你将注意到内存使用量在增加,直到这些对象被释放。问题是只有当UIKit耗尽了 autorelease pool,这些对象才会被释放,也就是说当不再需要这些对象之后,这些对象还在内存中占据着资源。

  不过这个问题完全可以避免:在@autoreleasepool代码块中创建临时对象,如下代码:

  NSArray *urls = <# An array of file URLs #>;for (NSURL *url in urls) @autoreleasepool NSError *error; NSString *fileContents = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error];

 

  当每次迭代完之后,都会释放所有的autorelease对象。

  关于NSAutoreleasePool的更多内容可以阅读苹果的官方文档。

  24) 缓存图片 — 或者不缓存

  iOS中从程序bundle中加载UIImage一般有两种方法。第一种比较常见:imageNamed。第二种方法很少使用:imageWithContentsOfFile

 

  为什么有两种方法完成同样的事情呢?

  imageNamed的优点在于可以缓存已经加载的图片。苹果的文档中有如下说法:

  This method looks in the system caches for an image object with the specified name and returns that object if it exists. If a matching image object is not already in the cache, this method loads the image data from the specified file, caches it, and then returns the resulting object.

  这种方法会在系统缓存中根据指定的名字寻找图片,如果找到了就返回。如果没有在缓存中找到图片,该方法会从指定的文件中加载图片数据,并将其缓存起来,然后再把结果返回。

  而imageWithContentsOfFile方法只是简单的加载图片,并不会将图片缓存起来。

  这两个方法的使用方法如下:

 

  UIImage *img = [UIImage imageNamed:@"myImage"]; // caching// orUIImage *img = [UIImage imageWithContentsOfFile:@"myImage"]; // no caching

  那么该如何选择呢?

  如果加载一张很大的图片,并且只使用一次,那么就不需要缓存这个图片。这种情况imageWithContentsOfFile比较合适——系统不会浪费内存来缓存图片。

  然而,如果在程序中经常需要重用的图片,那么最好是选择imageNamed方法。这种方法可以节省出每次都从磁盘加载图片的时间。

  25) 尽量避免Date格式化

  如果有许多日期需要使用NSDateFormatter,那么需要小心对待了。如之前(重用花销很大的对象)所提到的,无论什么时候,都应该尽量重用NSDateFormatters。

  然而,如果你需要更快的速度,那么应该使用C来直接解析日期,而不是NSDateFormatter。Sam Soffes写了一篇文章,其中提供了一些解析ISO-8601格式日期字符的串代码。你只需要简单的调整一下其中的代码就可以满足自己特殊的需求了。

 

  这听起来不错把——不过你相信这还有更好的一个办法吗?

 

  如果你自己能控制处理日期的格式,那么可以选择 Unix timestamps。Unix timestamps是一个简单的整数,代表了从新纪元时间(epoch)开始到现在已经过了多少秒,通常这个新纪元参考时间是00:00:00 UTC on 1 January 1970。

  你可以很容易的见这个时间戳转换为NSDate,如下所示:

  - (NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp return [NSDate dateWithTimeIntervalSince1970:timestamp];

 

  上面这个方法比C函数还要快!

  注意:许多网络APIs返回的时间戳都是毫秒,因此需要注意的是在将这个时间戳传递给dateFromUnixTimestamp之前需要除以1000。


在开发过程中,下面这些初级技巧需要时刻注意:

  使用ARC进行内存管理

  在适当的情况下使用reuseIdentifier

  尽可能将View设置为不透明(Opaque)

  避免臃肿的XIBs

  不要阻塞主线程

  让图片的大小跟UIImageView一样

  选择正确的集合

  使用GZIP压缩

  初级性能提升

  本部分内容介绍几本的程序性能提升技巧。其实所有级别的开发者都能从中获益。

  1) 使用ARC进行内存管理

  ARC是在iOS 5中发布的,它解决了最常见的内存泄露问题——也是开发者最容易健忘的。

  ARC的全称是“Automatic Reference Counting”——自动引用计数,它会自动的在代码中做retain/release工作,开发者不用再手动处理。

  下面是创建一个View通用的一些代码块:

  UIView *view = [[UIView alloc] init];// ...[self.view addSubview:view];[view release];

  在上面代码结束的地方很容易会忘记调用release。不过当使用ARC时,ARC会在后台自动的帮你调用release。

  ARC除了能避免内存泄露外,还有助于程序性能的提升:当程序中的对象不再需要的时候,ARC会自动销毁对象。所以,你应该在工程中使用ARC。

  下面是一些学习ARC很棒的一些资源:

  苹果的官方文档

  Matthijs Hollemans的初级ARC

  Tony Dahbura的如何在Cocos2D 2.X工程中使用ARC

  如果你仍然不确定ARC带来的好处,那么看一些这篇文章:8个关于ARC的神话——这能够让你相信你应该在工程中使用ARC!

  值得注意的是,ARC并不能避免所有的内存泄露。使用ARC之后,工程中可能还会有内存泄露,不过引起这些内存泄露的主要原因是:block,retain循环,对CoreFoundation对象(通常是C结构)管理不善,以及真的是代码没写好。

  这里有一篇文章是介绍哪些问题是ARC不能解决的 — 以及如何处理这些问题。以上是关于iOS开发内存警告Memory Warning和ViewController的生命周期的问题的主要内容,如果未能解决你的问题,请参考以下文章

错误:Java HotSpot(TM) 64-Bit Server VM warning: Insufficient space for shared memory file

OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory failed error='Cannot allocate memory&#

关于Java HotSpot(TM) 64-Bit Server VM warning: Insufficient space for shared memory file: 11043的解决办

深入解析iOS内存 WWDC2018 iOS Memory Deep Dive

内存警告后释放 UIImage 时崩溃

微信小程序 (node) warning: possible EventEmitter memory leak detected