1.启动时间
应用启动时间长短对用户第一次体验至关重要,同时系统对应用的启动、恢复等状态的运行时间也有严格的要求,在应用超时的情况下系统会直接关闭应用。以下是几个常见场景下系统对app运行时间的要求:
- Launch 20秒
- Resume 10秒
- Suspend 10秒
- Quit 6秒
- Background Task 10分钟
要获取准确的app启动所需时间,最简单的方法时首先在main.c中添加如下代码:
CFAbsoluteTime StartTime;
int main(int argc, char **argv) {
StartTime = CFAbsoluteTimeGetCurrent();
然后在AppDelegate的回调方法application:didFinishLaunchingWithOptions中添加:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@”Lauched in %f seconds.”, (CFAbsoluteTimeGetCurrent() – StartTime));
});
可能你会觉得为什么这样可拿到系统启动的时间,因为这个dispatch_async中提交的工作会在app主线程启动后的下一个run lopp中运行,此时app已经完成了载入并且将要显示第一帧画面,也就是系统会运行到-[UIApplication _reportAppLaunchFinished]
之前。
2.懒加载
当需要显示View的时候才去创建,而不是创建好所有View,在需要显示的时候改变他的hidden属性或则透明度。
每个方案都有其优缺点:
第一种方案则相反-消耗更少内存,但是会在点击按钮的时候比第一种稍显卡顿。
第二种方案一开始就创建一个view会消耗内存,然而这也会使你的app操作更流畅。
具体选择哪一种就看开发者的选择了,你是愿意那空间换时间还是愿意拿时间换空间,虽然现在时间越来越宝贵,空间相对于时间来说显得没那么重要,但是目前市场上还是有一部分用户使用低端机的,为了适配这些机型,两者还是要综合考虑的。
3.正确加载图片
加载图片常用的两种方式:
[UIImage imageNamed:@"myImage"];
[UIImage imageWithContentsOfFile:@"myImage"];
第一种方法首先会到缓存中查找如果存在返回图片对象,缓存中没有就会从资源文件中加载并缓存到内存中去。
第二种是从磁盘中读取加载图片。如果你要加载一个大图片而且是一次性使用,那么就没必要缓存这个图片,用imageWithContentsOfFile足矣,这样不会浪费内存来缓存它。
4.尽量设置View为不透明
如果你有不透明的Views,你应该设置它们的opaque属性为YES。原因是这会使系统用一个最优的方式渲染这些views。这个简单的属性在IB或者代码里都可以设定。
Apple的文档对于为图片设置不透明属性的描述是:
(opaque)这个属性给渲染系统提供了一个如何处理这个view的提示。如果设为YES, 渲染系统就认为这个view是完全不透明的,这使得渲染系统优化一些渲染过程和提高性能。如果设置为NO,渲染系统正常地和其它内容组成这个View。默认值是YES。
在相对比较静止的画面中,设置这个属性不会有太大影响。然而当这个view嵌在scroll view里边,或者是一个复杂动画的一部分,不设置这个属性的话会在很大程度上影响app的性能。
你可以在模拟器中用Debug\Color Blended Layers选项来发现哪些view没有被设置为opaque。目标就是,能设为opaque的就全设为opaque!
5.避免过于庞大的XIB
当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。
如果你不得不XIB的话,使他们尽量简单。尝试为每个Controller配置一个单独的XIB,尽可能把一个View Controller的view层次结构分散到单独的XIB中去。
6.重用大开销对象
一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。还需要注意的是,设置一个NSDateFormatter的速度差不多是和创建新的一样慢的!官方建议缓存NSDateFormatter可以提高效率。
NSDateFormatter优化方法:
一. 延迟转换
只在UI需要使用转换结果时再进行转换。
二. 缓存到内存
不同的iOS系统版本下,NSDateFormatter的线程安全性也不同,缓存的方式也有所区别。iOS7之前,NSDateFormatter是非线程安全的,因此,多个线程访问同一个NSDateFormatter对象,会导致APP崩溃。iOS7以及iOS7以后,NSDateFormatter都是线程安全的,所以我们无需担心NSDateFormatter对象使用过程中被另外一条线程修改。
iOS7之前:
+(NSDateFormatter *)cachedDateFormatter {
NSMutableDictionary *threadDict = [[NSThread currentThread] threadDictionary];
NSDateFormatter *dateFormatter = [threadDict objectForKey:@"cachedDateFormatter"];
if (!dateFormatter) {
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[NSLocale currentLocale]];
[dateFormatter setDateFormat:@"YYYY-MM-dd HH:mm:ss"];
[threadDict setObject:dateFormatter forKey:@"cachedDateFormatter"];
}
return dateFormatter;
}
iOS7之后:(包括iOS7)
static NSDateFormatter *cachedDataFormatter = nil;
+(NSDateFormatter *)cachedDateFormatter {
if (!cachedDataFormatter) {
cachedDataFormatter = [[NSDateFormatter alloc] init];
[cachedDataFormatter setLocale:[NSLocale currentLocale]];
[cachedDataFormatter setDateFormat:@"YYYY-MM-dd HH:mm:ss"];
}
return cachedDataFormatter;
}
如果缓存了NSDateFormatter或者是其他依赖于currentLocale的对象,那么我们应该监听NSCurrentLocaleDidChangeNotification通知,当currentLocale变化时,及时更新被缓存的NSDateFormatter对象。
三. 利用C语言库
如果日期格式是固定的,我们可以采用C语言中的strptime函数,这样更加简单高效。
- (NSDate *)easyDateFormatter {
time_t t;
struct tm tm;
//ISO8601时间格式:2004-05-03T17:30:08+08:00
char *iso8601 = "2016-09-18";
strptime(iso8601, "%Y-%m-%d", &tm);
tm.tm_isdst = -1;
//tm结构体中的tm.tm_hour为负数,会导致mktime(&tm)计算错误
tm.tm_hour = 0;
t = mktime(&tm);
return [NSDate dateWithTimeIntervalSince1970:t+[[NSTimeZone localTimeZone] secondsFromGMT]];
}
7.UITableView的优化
每个iOS开发者都会使用到UITableView,它也是APP数据展示的一个非常常用而且重要的UI控件,对它的性能优化也是必不可少的。
- 通过正确的设置 reuseIdentifier 来重用 Cell。
- 尽量减少不必要的透明 View。
- 尽量避免渐变效果、图片拉伸和离屏渲染。
- 当不同的行的高度不一样时,尽量缓存它们的高度值。
- 如果Cell 展示的内容来自网络,确保用异步加载的方式来获取数据,并且缓存服务器的 response。
- 使用 shadowPath 来设置阴影效果。
- 尽量减少 subview 的数量,对于 subview 较多并且样式多变的 Cell,可以考虑用异步绘制或重写drawRect。
- 尽量优化 - [UITableView tableView:cellForRowAtIndexPath:]
方法中的处理逻辑,如果确实要做一些处理,可以考虑做一次,缓存结果。 - 选择合适的数据结构来承载数据,不同的数据结构对不同操作的开销是存在差异的。
- 缓存动态行高
8.不要在主线程处理耗时操作
这个不需要赘述了,比较阻塞主线程导致页面假死的情况我相信大家都遇到过,带给使用者的用户体验你肯定非常清楚。
遇到耗时操作,我们可以使用Grand Central Dispatch,或者 NSOperation 和 NSOperationQueues单独开辟线程去处理相关操作,需要更新UI的时候再切换到主线程中刷新UI。
GCD的模板:(短小精悍,虽然我短,但是我能旋转!)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//todo something
dispatch_async(dispatch_get_main_queue(), ^{
// 切换到主线程刷新UI
});
});
当然了,多线程虽然虽然很好,但是增加了程序的复杂度和潜在风险,你需要考虑线程安全、线程依赖等相关问题。开辟多线程的同时也会花费相应资源。这个需要你综合去评估了!
9.绘制图形
当我们自定义图形的时候,一般会选择重写View的drawRect方法。如果UIView检测到-drawRect:方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以contentsScale,一旦你实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的内存可从这个公式得出:图层宽x图层高x4字节,宽高的单位均为像素。如果视图的尺寸很大,可以想象这将会是一个多大的内存开销。
CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。用CGPath来定义想要绘制的图形,CAShapeLayer会自动渲染。它可以完美替代我们的直接使用CoreGraphics绘制layer,对比之下CAShapeLayer有以下优点:
渲染快速。CAShapeLayer 使用了硬件加速,绘制同一图形会比用 Core Graphics 快很多。
高效使用内存。一个 CAShapeLayer 不需要像普通 CALayer 一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
不会被图层边界剪裁掉。
不会出现像素化。
总结一下绘制性能优化原则:
绘制图形性能的优化最好的办法就是不去绘制。
利用专有图层代替绘图需求。
不得不用到绘图尽量缩小视图面积,并且尽量降低重绘频率。
异步绘制,推测内容,提前在其他线程绘制图片,在主线程中直接设置图片。
10.图形和动画
图形性能对用户体验有直接的影响,Instruments中的Core Animation工具用于测量物理机上的图形性能,通过视图的刷新频率大小来判断应用的图形性能。例如一个复杂的列表滚动时它的刷新率应该努力趋近于 60fps才能让用户觉得够流畅,从这个数字也可以算出run loop最长的响应时间应该是16(1/60)毫秒。
启动Instruments的Core Animation工具后可以发现左下部分有一堆选项,我们来逐个介绍:
1. Color Blended Layers
表示混合的图层会为红色,不透明的图层为绿色,通常我们希望绿色的区域越多越好。Blended Layer是因为这些Layer是透明的,系统在渲染这些view时需要将该view和下层view混合(Blend)后才能 计算出该像素点的实际颜色,如果这种blended layer很多,那么在滚动列表时就甭想有流畅的效果。这也是尽量设置View为不透明的原因。
解决blended layer问题也很简单,检查红色区域view的opaque属性,记得设置成YES。
2. Color Hits Green and Misses Red
设置layer的阴影(shadow)、圆角(cornerRadius)、遮罩(mask)、渐变(Gradient)等会让其渲染的开销很高,设置layer的shouldRasterize为YES,系统会将这些Layer缓存成Bitmap位图供渲染使用,如果失效时便丢弃这些Bitmap重新生成。
使用这个选项后时,如果Rasterized的Layer失效,便会标注为红色,如果有效标注为绿色。当测试的应用频繁闪现出红色标注图层时,表明对图层 做的Rasterization作用不大。
图层Rasterization栅格化好处是对刷新率影响较小,坏处是删格化处理后的Bitmap缓存需要占用内存,而且当图层需要缩放时,要对删格 化后的Bitmap做额外计算。
3. Color Misaligned Images
Misaligned Image表示要绘制的点无法直接映射到频幕上的像素点,此时系统需要对相邻的像素点做anti-aliasing反锯齿计算,增加了图形负担,通常这种问题出在对某些View的Frame重新计算和设置时产生的。
被缩放的图片会被标记为黄色,像素不对齐则会标注为紫色。
上图中被标注为黄色的图层,这是由于图层显示的是被缩放后的图片,如果这些图片是通过网络下载的,可以通过程序更新为确定的绘制大小来解决。还 有些系统Navigation Bar和Tool Bar的背景图片使用的是拉伸(Streched)图片,也会被表示为黄色,这是属于正常情况,通常无需修改。这种问题一般对性能影响不大,而是可能会在边缘处虚化。
4. Color Offscreen-Rendered Yellow
Offscreen-Rendering离屏渲染意思是iOS要显示一个视图时,需要先在后台用CPU计算出视图的Bitmap,再交给GPU 做Onscreen-Rendering显示在屏幕上,因为显示一个视图需要两次计算,所以这种Offscreen-Rendering会导致app的图 形性能下降。
大部分Offscreen-Rendering都是和视图Layer的Shadow和Mask相关,下列情况会导致视图的Offscreen- Rendering:
- 使用Core Graphics (CG开头的类)。
- 使用drawRect()方法,即使为空。
- 将CALayer的属性shouldRasterize设置为YES。
- 使用了CALayer的setMasksToBounds(masks)和setShadow*(shadow)方法以及设置cornerRadius(圆角), masks(遮罩), shadows(阴影),edge antialiasing(反锯齿)等。
前两种情况使用的是CPU离屏渲染,首先分配一块内存,然后进行渲染操作生成一份bitmap位图,整个渲染过程会在你的应用中同步的进行,接着再将位图打包发送到iOS里一个单独的进程--render server,理想情况下,render server将内容交给GPU直接显示到屏幕上。
offscreen-render对性能到底有什么影响?
通常大家说的离屏渲染指的是GPU这块(当然CPU这块也会有影响,也需要消耗一定的资源),比如修改了layer的阴影或者圆角,GPU需要做额外的渲染操作。通常GPU在做渲染的时候是很快的,但是涉及到offscreen-render的时候情况就可能有些不同,因为需要额外开辟一个新的缓冲区进行渲染,然后绘制到当前屏幕的过程需要做onscreen跟offscreen上下文之间的切换,这个过程的消耗会比较昂贵,涉及到OpenGL的pipeline跟barrier,而且offscreen-render在每一帧都会涉及到,因此处理不当肯定会对性能产生一定的影响,所以可以的话尽量减少offscreen-render的图层
最后,最重要的是要学会如何使用Xcode集成的开发工具Instruments来具体分析项目的各个方面,找到性能的瓶颈所在来做针对性的性能优化。