iOS 内存管理之四:内存优化
转自:https://blog.boolchow.com/2016/04/04/Memory-Manage-4-Memory-Optimization/
所谓的内存优化,在设计程序的过程中,我们要在保证程序运行效率的前提下,尽量压缩程序运行时所占用的内
存。无论硬件设备的内存有多大,程序运行时占用内存越少越好。下面我将介绍在开发项目过程中,一些优化内存的方法。
1.关于UITableView
在项目开发中,UITableView
是用的比较多的一个视图控件。如果能够对 UITableView
的使用做好优化,程序的性能将提高很多。
(1)善于使用UITableViewCell的重用机制
重用机制:这种机制下系统默认有一个可变数组
NSMutableArray* visiableCells
,用来保存当前显示的cell。一个可变字典NSMutableDictnery* reusableTableCells
,用来保存可重复利用的cell。UITableView
只会创建一屏幕的cell,放在visiableCells
中。每当cell滑出屏幕,就会放到reusableTableCells
中,当要显示某一个位置的cell时,先去reusableTableCells
中取,如果有,直接取来用;如果没有,就会创建。这样极大减少了内存的开销。
在iOS 6之后,在UITableView和UICollectionView中除了可以复用cell,还可以复用各个Section的Header和Footer。可见Apple一直在不断优化。在项目开发中,我们需要给 UITableViewCells
、 UICollectionViewCells
、UITableViewHeaderFooterViews
设置正确的 reuseIdentifier
。当有多类cell需要复用是,我们可以根据 reuseIdentifier
区分。我们可以在Xcode中设置,如下图:
![](https://img.haomeiwen.com/i2414101/31d1928454cfa3b5.png)
下面是一个简单的cell复用的示例:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = nil;
UITableViewCell *cell = nil;
cellIdentifier = @"你的xib文件视图中标注的reuseIdentifier";
cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; //根据identifier复用cell
//如果没有对应的cell,创建cell
if(!cell){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
return cell;
}
复用cell是一个很好的机制,但是使用不当也会出现问题,也就是所谓的复用重叠问题。看下面代码:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = @"myCell1";
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
if ((indexPath.row%2) == 0) {
cell.backgroundColor = [UIColor blueColor];
}
}
cell.textLabel.text = [NSString stringWithFormat:@"%ld",(long)indexPath.row];
// Configure the cell...
return cell;
}
我本打算将偶数行的设置为蓝色,基数行为默认颜色,并将cell的内容设置为行数,加以区分。结果如图:
![](https://img.haomeiwen.com/i2414101/3d61d30a68bd3d0e.png)
从上图可以看出,开始初始化的13~14个cell正常,但是当滑动tableview时,就出现了问题,有的基数行cell也变为了蓝色。这是因为,下面的cell基本都是复用的,当没有显示指定cell的属性时,它就会使用已经创建过的cell的属性,导致有的蓝色有的白色。解决办法就是像下面这样写:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = @"myCell1";
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
if ((indexPath.row%2) == 0) {
cell.backgroundColor = [UIColor blueColor];
}
cell.textLabel.text = [NSString stringWithFormat:@"%ld",(long)indexPath.row];
// Configure the cell...
return cell;
}
切记:当对多种cell赋予属性时,一定不能写在
if (!cell){}
里面,避免复用出现问题。
(2)优化UITableViewCell高度计算
UITableView有两个很重要的回调方法:tableView:cellForRowAtIndexPath:
和tableView:heightForRowAtIndexPath:
。很多人认为,在初始化tableview时,会先调用前者进行创建,然后再调用后者进行布局和属性设置。然而并非如此。真实的情况是这样的:UITableView是继承自UIScrollView的,需要先确定它的contentSize及每个Cell的位置,然后才会把重用的Cell放置到对应的位置。所以事实上,UITableView的回调顺序是先多次调用 tableView:heightForRowAtIndexPath:
以确定contentSize及Cell的位置,然后才会调用 tableView:cellForRowAtIndexPath:
,从而来显示在当前屏幕的Cell。
举个例子:如果现在要显示20个Cell,当前屏幕显示5个。那么刷新(reload)UITableView时,UITableView会先调用20次 tableView:heightForRowAtIndexPath:
方法,然后调用5次tableView:cellForRowAtIndexPath:
方法;滚动屏幕时,每当Cell滚入屏幕,都会调用一次tableView:heightForRowAtIndexPath:
、tableView:cellForRowAtIndexPath:
方法。
所以,对于UITableViewCell的高度计算的优化,就是对这两个函数的处理。至于如何优化@我就叫Sunny怎么了写了一篇很好的文章去介绍。我就不多说了。
(3) 懒加载(延迟加载)
懒加载并不是减少了程序内存消耗,而是将加载对象的时间推迟,在使用到对象的时候在对其进行初始化。例如一个UITableView一共有20行,但是屏幕只显示5行数组。那么在初始化tableview的时候,可以只先加载5行数据,另外15行等到显示的时候再去加载。这样可以减少初始化tableview时所需要的内存。(这样说有点牵强,因为实时加载会影响tableview的流畅度,但是大体就是这个意思 ><)
2.关于图片的处理
图片在内存中会占很大开销,如果适当的处理图片,会减少很多内存的消耗。
(1)缓存图片
常见的从bundle中加载图片的方式有两种,一个是用imageNamed
,二是用imageWithContentsOfFile
,第一种比较常见一点。
imageNamed
的优点是当加载时会缓存图片。imageNamed
的文档中这么说:
这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话。如果缓存中没有找到相应的图片,这个方法从指定的文档中加载然后缓存并返回这个对象。
也就是说,imageNamed
方法加载的图片,会对图片进行缓存。而 imageWithContentsOfFile
方法不会。
所以,如果要加载的图片比较小,而且会反复使用,这种情况选择用 imageNamed
;如果要加载一个大图片,而且是一次性使用,那就使用 imageWithContentsOfFile
,没必要浪费内存去缓存它。
代码示例:
// 对图片进行缓存
UIImage *img = [UIImage imageNamed:@"imgName"];
// 不对图片缓存,用完即释放
UIImage *img = [UIImage imageWithContentsOfFile:@"imgName"];
(2)调整图片大小
我们经常从网络获取图片或者从本地bundle获取图片,然后加载到 UIImageView
中。在加载图片时,应尽量保证图片大小和 UIImageView
大小相同。因为在运行中缩放图片很耗费资源,如果 UIImageView
嵌套在 UIScrollView
或者 UITableView
中,会更耗费资源。
对于从本地bundle中加载的图片,我们可以事先件图片处理好。对于从网络下载的图片,在下载完成后,我们需要对图片进行缩放,然后再加载。
(3)代码渲染 or 直接获取
前面已经说过,用代码去渲染一张图片会使图片占用内存翻倍。但是用代码去绘制图片,能够很好的去控制图片,并且能够做出很多漂亮的效果,前提是牺牲一部分内存;那如果所有图片都从bundle中加载呢?那会使bundle的体积增大,同时不能够用代码去灵活处理图片的效果。
所以,在开发过程中,是代码渲染图片,还是从bundle获取图片,需要做一个权衡。
3.数据处理
在项目开发中,我们会使用到各种格式的数据,例如 JSON
、XML
等。还有各种各样的数据结构,例如数组、链表、字典、集合等。使用正确的数据格式和使用正确的数据结构,会减少我们的资源消耗。
(1)选择正确的数据格式
App与网络进行交互时,常常采用 JSON
或者 XML
类型的数据格式。
JSON
是一种轻量级的数据交换格式,具有良好的可读和便于快速编写的特性。解析 JSON
会比 XML
更快,但是 JSON
传输的数据比较小。
XML
是一种重量级的数据交换格式,适用于很大的数据传输。当数据量较大时,使用 XML
数据格式,会极大减少内存消耗,增加性能。
另外,尽量避免数据多次转化。例如tableview中需要以数组的形势去赋值。那么服务器尽量返回数组类型。如果返回 JSON
类型,在去转换为 NSArray
类型,也会增加开销。
(2)选择正确的数据结构
不同的数据结构,处理数据的速度是不同的。
- 数组 NSArray NSMutableArray:有序的一组值。使用索引来查询很快,使用值查找很慢, 插入/删除很慢。
- 字典 NSDictionary NSMutableDictionary:存储键值对。用键来查找比较快。
- 集合 NSSet NSMutableSet:无序的一组值。用值来查找很快,插入/删除很快。
4.View的处理
(1)避免使用过于复杂的xib
在目前很多项目开发中,还经常用到 xib
。当加载一个 xib
时,所有的内容都会放到内存里,包括任何图片。如果 xib
文件过于庞大,会占用很多内存。xib
与 storyboard
不同,xib
即使暂时用不到,view也会存在于内存里;storyboard
仅在需要时实例化一个view controller
。
而且设置view属性时,尽可能的把 opaque
属性设置为YES(不透明)。这样会提高渲染系统优化一些渲染过程和提高性能。
(2)正确设置View的背景
设置UIView的背景图片主要有两种方式:
- 使用
UIColor
的colorWithPatternImage
来设置背景色; - 给
UIView
添加UIImageView
子视图。
第一种方式,适合使用小图平铺创建背景,能更快渲染也不会会费很多内存。例如使用一个10x10的像素大小重复背景。
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"backgroundImg"]];
第二种方式,适合于使用大图,即整张图片来设置背景。如果使用 colorWithPatternImage
会消耗太多内存从而收到内存警告导致应用程序突然崩溃。而使用 UIImageView
会节约不少内存。
UIImageView *backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgroundImg"]];
[self.view addSubview:backgroundView];
(3)设定Shadow Path
如果用下面代码给 view.layer
添加一个shadow:
UIView *view = [[UIView alloc] init];
// Setup the shadow ...
view.layer.shadowOffset = CGSizeMake(-1.0f, 1.0f);
view.layer.shadowRadius = 5.0f;
view.layer.shadowOpacity = 0.6;
这会使Core Animation
不得不在后台得出图形并加好阴影之后再去渲染,这会开销很大。
如果使用shadowPath则会避免这种问题:
view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];
5.合理使用Autorelease Pool
NSAutoreleasePool
负责释放block中的autoreleased objects。一般情况下它会自动被UIKit调用。但是有些状况下也需要手动去创建它。
假如创建很多临时对象,你会发现内存一直在减少直到这些对象被release的时候。这是因为只有当UIKit用光了autorelease pool的时候memory才会被释放。
但是如果自己定义 @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];
/* Process the string, creating and autoreleasing more objects. */
}
}
6.正确处理缓存
缓存可以分为内存缓存和磁盘缓存。在项目开发过程中,我们经常会对一些图片、声音、数据进行缓存。合理利用缓存机制,会大大提高程序的性能,提高APP的流畅性。例如被广为使用的 SDWebImage,它使用的缓存机制是这样的:
(1)先根据查看内存缓存,如果有直接获取。
(2)如果内存没有,从磁盘缓存获取。
(3)如果磁盘缓存也没有,直接通过URL从网络下载。
当然这只是一个简单的描述,更加详细请看@南峰子_老驴的一篇SDWebImage实现分析。
合理处理缓存,能够提高程序的性能,不用每次都从网络获取数据。但是也不能什么都存入缓存,这会消耗很多内存和磁盘空间。所以应合理使用缓存机制。
网友评论