性能优化
80% 的性能优化都是不必要的, 如果应用存在明显问题. 再逐一核对.
1. 内存的优化:
- 重用和懒加载Views.
- 模仿tableview进行重用: 不要一次创建所有的SubViews . 而当需要时才创建, 当使用过后, 放入可重用的缓存池.
- 一般在scrollView中滚动时创建你的views,避免不必要的内存分配.
- 也可以在用户点击了一个按钮需要 创建呈现一个view的场景.
- 重用大开销对象 . 一些object的初始化很慢,比如
NSDateFormatter
和NSCalendar
, 然后有时不可避免的需要使用它们. 可以通过属性记录或者静态变量(这种方式会同单例一样在app运行时一直存在.) 创建;
1.1 Cache选择.
需要缓存的数据原则: 那些不大可能改变但是需要经常读取的东西. 如: 服务器的响应,图片,甚至计算结果(cell的行高).
NSCache和NSDictionary类似,不同的是系统会自动回收.所以尽量使用NSCache.
1.2 选择正确的集合
- Arrays: 使用index来lookup很快, 但是使用value lookup很慢. 同样插入/删除都很慢.
- Dictionaries: 用key查找比较快;
- Sets: 无序的. 用值查找很快, 插入和删除很快 ;
2. 界面和渲染优化:
2.1 基本
- 尽量将不透明view的Opaque属性设置为true. 尤其是他们出现在赋值动画或是ScrollView中. 通常我们可以对tableview中的cell做此设置.(这样可以让系统用一个最优的方式渲染这些views. 你可以在模拟器中用Debug\Color Blended Layers选项来发现哪些view没有被设置为opaque。目标就是,能设为opaque的就全设为opaque!).
在imageView设置前, 尽量先调整好图片大小. 尤其放在UIScrollView中 , 运行中自动缩放是十分耗能. 如果图片是从远端服务器加载到的, 你不能控制图片大小. 那么最好在
background thread
缩放到固定大小, 然后在imageView中显示.避免使用过大的 xib,尝试为每一个Controller配置一个单独的xib, 尽可能把一个ViewController的View层次结构分散到单独的xib中去. 因为当你加载一个xib的时候所有内容都被放在内存里,包括图片,声音文件; 如果有一个不会即刻用到的View. 就违反懒加载意义了.
CALayer: 采用CornerRadius 设置圆角时, 如果数量过多,例如QQ用户会话列表,那么滚动时会大幅降低应用的帧数. 所需圆角较多的情况下, 可以预先生成图片并缓存, 再使用. (更新: iOS9之后苹果已经修复,可以直接设置CornerRadius)
-
正确设置背景图片:
- 如果使用小图平铺来创建背景图的话, 采用UIColor的colorWithPatternImage的渲染速度会更快.它是用来创建小的重复的的图片做背景,
self.view.backgroundColor = [UIColor colorWithPatternImage:image]
- 如果你需要加载一个大图片 且只供一次性使用, 采用
imageWIthContentsOfFile
来加载 ; - 如果是全画幅做背景且需要多次重用, 使用
imageNamed
会将图片缓存 ;
UIImageView *backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]];
- 如果使用小图平铺来创建背景图的话, 采用UIColor的colorWithPatternImage的渲染速度会更快.它是用来创建小的重复的的图片做背景,
[self.view addSubview:backgroundView];
```
- 设置Shadow Path: 如何在一个View或者layer上加一个shadow, QuartzCore框架是很多人第一选择. 但是Core Animation不得不先在后台得出你的图形并并加好阴影之后才渲染. 这开销很大. 可以使用shadowPath避免这个问题 .
view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];
这样iOS就不必每次都计算如何渲染, 它使用一个预先计算好的路径.不过可能在某些View计算path比较困难,且当View的frame变化时,你都需要区update shadowPath.
- 尽量不要在viewWillAppear中进行太多操作.
2.2 权衡渲染
在iOS中可以有很多方法做出漂亮的按钮. 你可以用整幅图片, 可调大小图片,或者CAlayer,CoreGraphics甚至OpenGL来画他们. 每个不同的解决方法都有不同的复杂程度和相应的性能.
简单来说: 就是用事先渲染好的图片更快. 省去绘画步骤; 但是若把所有的图片都放到bundle中, 这样就增加了体积. --- 因此, 这就是使用可变大小的图片更好的地方.
另外, 使用图片也意味着你失去了使用代码调整图片的机动性. 如果要做一个动画效果, 可能就需要多个图片了.
所以,就需要权衡空间与时间花费.
2.3 使用Sprite Sheets(游戏开发)
Sprite Sheets 可以让渲染速度加快, 甚至比标准的屏幕渲染方法节省内存 ;
3. 多线程方面
- 除渲染,触摸响应,UI操作 等,尽量都使用异步来处理: 如 I/O,存储,网络,异步线程通知.
4.网络方面的优化.
- 网络响应NSURLConnection缓存策略.
- 当传输网络数据过大时, 采用gzip压缩方式可以一定程度降低文件大小.(AFN框架已支持) gzip;
避免反复处理数据 : 许多应用需要从服务器加载功能所需的JSON/XML数据, 在服务器端和客户端尽量使用相同的数据结构. 因为在内存中操作数据转换数据结构是开销很大的;(获取对应的数组结构或字典结构)
JSON 相比较于 XML,优点在于解析快,更小更易于传输,但其实当你传输较大数据的时候,使用 SAX 解析 XML 时,可以做到边下载边解析,可极大的降低内存消耗 .
- 减少使用Web特性: 想要更高的性能你就要调整下你的HTML了。
- 第一件要做的事就是尽可能移除不必要的javascript,避免使用过大的框架。能只用原生js就更好了。
- 另外,尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。
- 最后,永远要注意你使用的图片,保证图片的符合你使用的大小。使用Sprite sheet提高加载速度和节约内存。
6.tableview的优化.
简单总结:
- 缓存行高
- cell的所有子视图都用预先创建,不需要的可以先设置隐藏
- 所有子视图都应该是添加到contentView上,
- 所有子视图都必须指定背景色,且不设置alpha
- cell栅格化
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.mainScreen().scale
- 异步绘制
layer.drawsAsynchronously = true
.
1. cell重用机制
UITableView只会创建一屏幕或多一点的Cell, 每当Cell滑出屏幕时, 就会放入到一个缓存池中,当要显示某一位置的cell时,会根据注册ID先去缓存池中取, 如果有,就直接拿来使用, 如果没有,才会创建;
- 当cell里需加载网络图片的话, 采用异步方式(如直接使用SDWebImage),且先放置一个默认图片, 去的网络数据后再更新.
- 如果单个cell所需网络数据的请求时间过长, 可以选择在tableview停止滑动之后再加载网络数据, 可以利用UIScrollViewDelegate中的
scrollViewDidEndDragging和scrollViewDidEndDecelerating
来获知停止滑动. 还可以在scrollViewWillBeginDragging函数做,添加代码来停止正在进行的数据加载; - 尽量使用rowHeigh, sectionFooterHeight,sectionHeaderHeight 来设置固定的高,而不是请求代理,即便需要请求,也不要在代理方法内计算行高, 提前缓存好 . 如标题2.
- 避免渐变,图片缩放,后台选人
- 尽量使所有的view opaque,包括cell自身
- 减少Subviews的数量
- 使用
shadowPath
来画阴影 - 尽量不适用或减少放置在CellForRowAtIdexPath方法内的操作.如果你需要用到它,只用一次然后缓存结果
- 使用正确的数据结构来存储数据
2. 缓存动态行高
在使用UITableVIew的时候可能会遇到这种情况: UITableViewCell中的内容来自网络端,可能需要根据内容而生成高度不一致的Cell.
解决方式
2.1 常规解决方式:
先实现delegate中的cell高度的定义方法. heightForRowAtIndexPath:
和数据源的cellForRowAtIndexPath
, 但是cell高度定义的代理方法总是优先于cell设置的数据源方法调用,这里始终有个矛盾,即在设置cell高度的时候,cell显示数据还没有拿到;
所以,我们先要获取数据,使用+(float)cellHeighWithText:(NSString*)
或其他方式计算出内容的行高,设置之后再通过数据源把内容填进去.
不足: 在这里,首先实际上对于同一条数据,内容被加载了两次;其次,在计算富文本或者大量数据大小的时候,tableview会出现卡顿.
2.2 使用NSCache
简单来说,NSCache是一个傻瓜式的缓存控件, 存取方式类似于NSDictionary, 工作方式与苹果的内存管理体系一致. 在内存紧张的时候,它会自动释放存储的对象;所以,项目中所有称之为缓存的对象都应该被被换成NSCache,代替NSArray和NSDictionary的缓存功能,(例如苹果的webView的缓存默认是交给NSCache管理的);
如果tableview数量多且不规则,我们需要做的就是在计算高度的时候就生成一个UITableViewCell或是直接cell行高并存入NSCache,需要返回Cell时, 先从缓存池中需找哪个Cell, 如果没找到则使用UITableView的重用机制重用,如果还找不到,在新建一个;
-(UITableViewCell* )tableview:(UITableView* )tableview preparedCellForIndexPath:(NSIndexPath * )indexPath withData:(id)data {
NSString * key = [NSString stringWithFormat:@"%ld-%ld",(long)indexPath.section,indexPath.row];
//try to get the cell from cache.
YQTableViewCell * cell = [_cellCache objectForKey:key];
if(!cell) {
//cache中没有从重用中获取
static NSString * ID = @"cell";
cell = (YQTableviewCell* ) [tableview dequeueReusableCellWithIdentifier:ID];
if(!cell) { //还没有 ,创建 并存入缓存池
cell = [[YQTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
//数据内容传给cell显示
[cell setContentText:data]
//存入cache
[_cellCache setObject:cell forked:key];
}
return cell;
}
7.数据存储方式
选择正确的数据存储选项 plist-归解档-偏好设置 - SQLite - Core Data.
- 小规模数据的使用NSUserDefaults 存储是比较好的方式 .
- plist(XML),是需要读取整个文件到内存中解析, 这个不是很实惠.
- 归解档: 同样有上面问题.
- 对于较大数据使用SQLite 或Core Data,SQLite进行大量数据操作时需要关闭事务,可节省大量时间.
8. 处理内存警告
系统内存过低时,iOS会通知所有的运行中app . 最佳方式是移除对缓存,图片object和其他一些可以重新创建的对象的强引用.
UIKit 提供了几种收集内存警告的方法:
- app delegate 中的
applicationDidReceiverMemoryWarning
的方法 - 在自定义控制器的子类中覆盖
didReceiverMemoryWarning
方法 - 自己注册并接受 UIApplicationDidReceiverMemoryWarningNotification 通知.
收到这个通知, 你就需要释放任何不必要的内存使用, 例如: UIViewController的默认行为是移除一些不可见的view . 它的一些子类则可以补充这个方法, 删掉一些额外的数据结构. 一个有图片缓存的app可以移除不再屏幕上显示的图片 .
9. 加速启动时间
快速打开app是很重要的, 特别是第一次打开的时候 ; 所能做的就是使它在此期间尽可能的做更多的异步任务, 比如请求数据,解析数据.避免过大的xib,因为这是在主线程上加载的. 所以尽量使用sb吧. (测试启动速度要把设备从xcode断开,因为xcode debug时watchdog并不运行)
- 在didFInishLaunching 里 开始尽可能多的异步任务来使你的应用尽早得到展示数据
10. 手动使用Autorelease Pool
手动释放临时对象.
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. */
}
}