iOS 性能优化

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有以下优点:

  1. 渲染快速。CAShapeLayer 使用了硬件加速,绘制同一图形会比用 Core Graphics 快很多。

  2. 高效使用内存。一个 CAShapeLayer 不需要像普通 CALayer 一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。

  3. 不会被图层边界剪裁掉。

  4. 不会出现像素化。

总结一下绘制性能优化原则:

  • 绘制图形性能的优化最好的办法就是不去绘制。

  • 利用专有图层代替绘图需求。

  • 不得不用到绘图尽量缩小视图面积,并且尽量降低重绘频率。

  • 异步绘制,推测内容,提前在其他线程绘制图片,在主线程中直接设置图片。

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:

  1. 使用Core Graphics (CG开头的类)。
  2. 使用drawRect()方法,即使为空。
  3. 将CALayer的属性shouldRasterize设置为YES。
  4. 使用了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来具体分析项目的各个方面,找到性能的瓶颈所在来做针对性的性能优化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 总结一下平时优化app性能用到的方法 1.合理的分配线程,不要滥用多线程。开辟线程,以及线程之间的上下文切换是消耗...
    SpursGo阅读 964评论 0 2
  • 前言 工欲善其事,必先利其器。Instrument对于iOS开发来说,是发现并且解决问题的一把利器。 本文会用到的...
    iOS开发攻城狮阅读 7,927评论 1 62
  • 一. 如何让你的应用程序更加省电?答:(1). 如果程序用到定位,需要在定位完毕之后关闭定位,或者降低定位的频率,...
    Hevin_Chen阅读 1,126评论 0 4
  • 太阳花今天去医院做检查,结果依然不好。 她也终于决定离职,我们的纠葛也暂时停止。而她一走,我的心却空了。游戏没有对...
    秋子123阅读 249评论 0 0
  • 今天,公司部门给新人都定了导师,签订了指导人协议,以后要好好干哦!想着下班回来后规划规划人生,定位定位人生呢,可是...
    雪羽飞鸿阅读 198评论 1 2