跳至主要内容

iOS性能优化(集合&转载)

http://www.jianshu.com/p/9e1f0b44935c
http://blog.cocoabit.com/2014-02-09-uitableview-gun-dong-liu-cheng-xing-you-hua/
http://tutuge.me/2015/02/19/提升UITableView性能-复杂页面的优化/
http://blog.jobbole.com/37984/
http://longxdragon.github.io/2015/05/26/UITableView优化技巧/


/*********以下是上面的页面内容********/

iOS性能优化

字数3576 阅读9220 评论14 
最近采用Instruments 来分析整个应用程序的性能.发现很多有意思的点,以及性能优化和一些分析性能消耗的技巧.小结如下.

Instruments使用技巧

关于Instruments官方有一个很有用的用户使用Guide,当然如果不习惯官方英文可以在这里找到中文本翻译版本PDF参阅.Instruments 确实是一个很强大的工具,用它来收集关于一个或多个系统进程的性能和行为的数据极为方便,并能及时跟踪随着时间产生的数据.还可以广泛收集不同类型的数据.关于Instrument工具基本使用不在赘述.如下重点说明一些使用技巧.

1.概览

工具通过Xcode工具栏中Product->Profile可以启动,启动后界面如下:

Instrument概览[via by chenkai]
当点击Time Profiler应用程序开始运行后.就能获取到整个应用程序运行消耗时间分布和百分比.为了保证数据分析在统一使用场景真实行有如下点需要注意:
在开始进行应用程序性能分析的时候,一定要使用真机,模拟器运行在Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操作在模拟器上运行的更慢,尤其是使用CAEAGLLayer来写一些OpenGL的代码时候. 这就导致模拟器性能数据和用户真机使用性能数据相去甚运.
另外在开始性能分析前另外一件重要的事情是,应用程序运行一定要发布配置 而不是调试配置.
在发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码.另iOS引入一种"Watch Dog"[看门狗]机制.不同的场景下,“看门狗”会监测应用的性能。如果超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程.开发者可以crashlog看到对应的日志.但Xcode在调试配置下会禁用"Watch Dog".

2.Time Profiler

选择Time Profiler启动.
time profile时间分析工具用来检测应用CPU的使用情况.可以看到应用程序中各个方法正在消耗CPU时间.使用大量CPU不一定是个问题.类似我们客户端中不同场景的天气动画[类似大雨]的路径就对CPU依赖就非常高,动画本身也是非常苛刻且耗费资源较多的任务.
点击Record 开始运行.

Time Profile 分析界面[via by chenkai]
刚开始我们拿到分析数据时往往是这样的:

性能数据[via by chenkai]
这里显示的是执行代码完整路径,其中系统和应用本身一些调用路径完全揉捏在一起.完全看不到我们关心的应用程序中实际代码执行耗时和代码路径实际所在位置.简单的方式可以快速勾选右边Call Tree中Separate Thread和Hide System Libraries两个选项[后面会解释选项作用]:

拆分后性能数据[via by chenkai]
可以看到直接能够看到应用程序各个方法调用耗时直接路径,剔除掉了系统相关方法和反向调用树路径.清爽很多.如果觉得这还不够直观,选择任意一个耗时方法分支[这里选择WeatherViewController viewDidLoad]双击进入会看到:

代码&耗时详情
可以直接定位到viewDidLoad的代码,也可以直观的看到改方法下调用其他方法耗时的时间.类似[self loadCityWeatherScroollerView]耗时是121x,x既耗时单位这里为ms毫秒.当然如果直接在Instrument找到问题觉得不方便修改,可以直接点击右上方Xcode按钮会直接定位Xcode对应调用方法入口.这样很容易能够快速定位代码占用CPU最多的方法.也可以打开Xcode快速修改并重新运行Profile来看修改后耗时前后对比.简单便捷.
这里对右侧call tree选项有必要做一下说明[官方user guide翻译]:
Separate By Thread:线程分离,只有这样才能在调用路径中能够清晰看到占用CPU最大的线程.
Invert Call Tree:从上到下跟踪堆栈信息.这个选项可以快捷的看到方法调用路径最深方法占用CPU耗时,比如FuncA{FunB{FunC}},勾选后堆栈以C->B->A把调用层级最深的C显示最外面. 
Hide Missing Symbols:如果dSYM无法找到你的APP或者调用系统框架的话,那么表中将看到调用方法名只能看到16进制的数值,勾选这个选项则可以隐藏这些符号,便于简化分析数据.
Hide System Libraries:这个就更有用了,勾选后耗时调用路径只会显示app耗时的代码,性能分析普遍我们都比较关系自己代码的耗时而不是系统的.基本是必选项.注意有些代码耗时也会纳入系统层级,可以进行勾选前后前后对执行路径进行比对会非常有用.
关于其他方法不再赘述.

性能分析&代码优化

我们这次性能优化主要针对如下两个使用场景:
A:应用程序第一次启动到进入天气首页的时间.
B:从后台切到前台天气首页占用时间.
在还没有拿到性能分析数据之前,一直认为第一次启动耗时主要浪费AppDelegate中第三方框架初始化上[类似WeiBo&WeChat 相关SDK初始化调用].当我们拿到实际性能数据耗时占用比时发现实际情况并非如此:

启动耗时
如上可以看到应用程序启动初始化工作主要会在MJAppDelegate如下两个方法展开:willFinishLaunchingWithOptionsdidFinishLaunchingWithOptions,其中第三方框架初始化工作主要是willFinishLaunchingWithOptions中完成的.而实际情况耗时占比非常小.基本可以忽略不计.
而我们要优化两个启动时间场景,不同在于.第一次进入应用需要经过新手教程、添加城市、请求城市数据、解析数据、初始化天气首页UI元素并加载场景动画. 而从后台进入时则从本地存储DT文件中解析天气数据、初始化天气首页UI元素并加载天气动画.

1.NSDateFormatter问题凸显

针对这点重点分析应用启动&天气首页耗时. 在AB两个场景均发现加载首页元素发现如下问题:

NSDate(TimeAgo)getDateStrByTimeZone耗时
继续跟踪发现:

NSDate耗时
在AB两个场景里均出现加载MJLineChartView 和 TendencyChartView 时获取时区对应时间上耗时较大.而耗时主要在getDateStrByTimeZone这个方法调用上.

getDateStrByTimeZone方法
其中创建一个NSDateFormatter对象平均耗时33ms左右 而设置NSDateFormatter的3个属性平均耗时也在30ms左右.因为首页24小时天气和未来几天预报中.需要for循环中遍历数据,导致这个方法别重复调用多次,则消耗时间不断叠加.
针对这个问题:
NSDateFormatter对象本身初始化很慢,同样还有NSCalendar也是如此.然而在一些使用场景中不可避免要使用他们,比如Json数据解析中.使用这个对象同时避免其性能开销带来性能开销,一般比较好的方式是通过添加属性(推荐)或创建静态变量保持该对象只被初始化一次,而被多次复用.不得不值得一提的是设置一个NSDateFormatter属性速度差不多是和创建新的实例对象一样慢!
添加属性方式如下:

属性方式
针对NSDateFormatter时间开销出了重用对象外,尽量避免采用其处理多个日期格式.当然针对日期格式处理如果需要提高更多速度,可以直接采用C,可以采用第三方库来规避这个问题..

2.UIImage缓存取舍

在项目代码中看到大量使用如下代码:

UIImage使用
在Main Thread中发现不同动画场景中Image IO 开销和耗时所占比例均不一,在UIImage元素较多总体叠加耗时也会占用一定比例.内存开销也会明显增高.
UIImage加载图片方式一般有两种:
A:imagedNamed初始化
B:imageWithContentsOfFile初始化
二者不同之处在于,imageNamed默认加载图片成功后会内存中缓存图片,这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象.如果缓存中没有找到相应的图片对象,则从指定地方加载图片然后缓存对象,并返回这个图片对象.
而imageWithContentsOfFile则仅只加载图片,不缓存.
大量使用imageNamed方式会在不需要缓存的地方额外增加开销CPU的时间来做这件事.当应用程序需要加载一张比较大的图片并且使用一次性,那么其实是没有必要去缓存这个图片的,用imageWithContentsOfFile是最为经济的方式,这样不会因为UIImage元素较多情况下,CPU会被逐个分散在不必要缓存上浪费过多时间.
使用场景需要编程时,应该根据实际应用场景加以区分,UIimage虽小,但使用元素较多问题会有所凸显.

3.天气首页加载策略

在AB两种场景把性能数据对比分析发现:

天气首页WeatherView更新耗时
天气首页WeatherView初始化耗时一直300ms-450ms之间,占据首页耗时很大一部分.且一直固定的开销.占据Main Thread3分之一.
而用户进入最先看到是天气首页上半部分:

上半部分
而下半部分需要滚动才能看到下半部分.且不一定触发:

下半部分
而现在整个首页View的初始化和更新全部放到主线程来做.其中WeatherInfoView updateAllInfo方法更新耗时最长.更多的view意味着更多的渲染,也就是意味更多的CPU和内存消耗,对于我们天气首页在UIScrollView里边嵌套了很多view更是如此
而针对这种情况不要在主线程承载过多的操作.uikit渲染,用户输入回应都需要主进程上完成.主线程被意外block或者加载响应耗时过多都会影响到用户体验.而针对资源消耗过大操作,处理原则是最小化主线程的CPU占用,将工作“搬离”主线程, 不要阻塞主线程.类似本地一些IO完全移到其他线程来做.
调试time profiler过程中发现,即使占用了很少的CPU时间(如果你在Time Profiler中看到这些的数据),也可能会阻塞主线程。磁盘、网络、Lock、dispatch_sync以及向其它进程/线程发送消息都会阻塞主线 程。Time Profiler只能检测出占用CPU过多的堆栈,但检测不了这些IO的问题.很奇怪.在System Trace里面突然发现了CPU Time很低,但Wait Time很高的调用,说明在主线程处理I/O已经严重损害了app的性能,这个时候考虑把这个操作优化了.
而针对我们应用首页ui中多个view,在加载策略完全可以采用多线程进行同步加载,只把上半部分放在主线程中加载,下班可以同时开一个线程进行同步加载.这样可以大大降低组线程初始化和更新时间,当首页初始化完毕已经呈现是,下半部分其实已经另外一个线程处理完毕.
另外针对单个view 尽量不要在viewWillAppear费时的操作,viewWillAppear在 view 显示之前被调用,出于效率考虑,在这个方法中不要处理复杂费时的事情;只应该在这个方法设置 view 的显示属性之类的简单事情,比如背景色,字体等。不然,用户会明显感觉到 view 显示迟钝.

4:应用首次加载时间

应用首次启动加载操作:

首次加载
首次加载坐了如下操作:
A: 链接和载入:可以在Time Profile中显示dyld载入库函数,库会被映射到地址空间,同时完成绑定以及静态初始化.
B: UIKit初始化:如果应用的Root View Controller是由XIB实现的,也会在启动时被初始化.
C: 应用回调:调用UIApplicationDeleagte的回调:application:didFinishLaunchingWithOptions.
D: 第一次Core Animation调用:在启动后的方法-[UIApplication _resportAppLaunchFinished]中调用CA::Transaction::commit实现第一帧画面的绘制.
应用程序首次加载中启动方法willFinishLaunchingWithOptions和didFinishLaunchingWithOptions只做应用程序首次启动必须的要操作,而针对_dyid_start在初始化库framework函数的操作.不必要的Framework不要链接,避免首次加载耗时.
小结如上.很多地方代码调用和底层机制看的不是特别明白,整理总结关于优化部分实在有限,如上仅供各位参考.另外Instruments确实是把分析代码利器.目前没有任何一个第三方工具可以去替代.推荐各位使用.

写在前面
本文来自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS 程序员。这是他的个人网站:http://www.marcelofabri.com/,你还可以在Twitter上关注@marcelofabri_
性能对 iOS 应用的开发尤其重要,如果你的应用失去反应或者很慢,失望的用户会把他们的失望写满App Store的评论。然而由于iOS设备的限制,有时搞好性能是一件难事。开发过程中你会有很多需要注意的事项,你也很容易在做出选择时忘记考虑性能影响。
这正是我写下这篇文章的原因。这篇文章以一个方便查看的核对表的形式整合了你可以用来提升你app性能的25条建议和技巧。
请耐心读完这篇文章,为你未来的app提个速!
注意:每在优化代码之前,你都要注意一个问题,不要养成”预优化”代码的错误习惯。时常使用Instruments去profile你的代码来发现需要提升的方面。Matt Galloway写过一篇很棒的如何利用Instruments来优化代码的文章
还要注意的是,这里列出的其中一些建议是有代价的,所建议的方式会提升app的速度或者使它更加高效,但也可能需要花很多功夫去应用或者使代码变得更加复杂,所以要仔细选择。

目录

我要给出的建议将分为三个不同的等级: 入门级、 中级和进阶级:
入门级(这是些你一定会经常用在你app开发中的建议)
  • 1. 用ARC管理内存
  • 2. 在正确的地方使用reuseIdentifier
  • 3. 尽可能使Views不透明
  • 4. 避免庞大的XIB
  • 5. 不要block主线程
  • 6. 在Image Views中调整图片大小
  • 7. 选择正确的Collection
  • 8. 打开gzip压缩
中级(这些是你可能在一些相对复杂情况下可能用到的)
  • 9. 重用和延迟加载Views
  • 10. Cache, Cache, 还是Cache!
  • 11. 权衡渲染方法
  • 12. 处理内存警告
  • 13. 重用大开销的对象
  • 14. 使用Sprite Sheets
  • 15. 避免反复处理数据
  • 16. 选择正确的数据格式
  • 17. 正确地设定Background Images
  • 18. 减少使用Web特性
  • 19. 设定Shadow Path
  • 20. 优化你的Table View
  • 21. 选择正确的数据存储选项
进阶级(这些建议只应该在你确信他们可以解决问题和得心应手的情况下采用)
  • 22. 加速启动时间
  • 23. 使用Autorelease Pool
  • 24. 选择是否缓存图片
  • 25. 尽量避免日期格式转换
无需赘述,让我们进入正题吧~

初学者性能提升

这个部分致力于一些能提高性能的基本改变。但所有层次的开发者都有可能会从这个记录了一些被忽视的项目的小小的性能备忘录里获得一些提升。
1. 用ARC管理内存
ARC(Automatic Reference Counting, 自动引用计数)和iOS5一起发布,它避免了最常见的也就是经常是由于我们忘记释放内存所造成的内存泄露。它自动为你管理retain和release的过程,所以你就不必去手动干预了。
下面是你会经常用来去创建一个View的代码段:
1
2
3
4
UIView *view = [[UIView alloc] init];
 // ...
 [self.view addSubview:view];
 [view release];
忘掉代码段结尾的release简直像记得吃饭一样简单。而ARC会自动在底层为你做这些工作。
除了帮你避免内存泄露,ARC还可以帮你提高性能,它能保证释放掉不再需要的对象的内存。这都啥年代了,你应该在你的所有项目里使用ARC!
这里有一些更多关于ARC的学习资源:
ARC当然不能为你排除所有内存泄露的可能性。由于阻塞, retain 周期, 管理不完善的CoreFoundation object(还有C结构)或者就是代码太烂依然能导致内存泄露。
这里有一篇很棒的介绍ARC不能做到以及我们该怎么做的文章 http://conradstoll.com/blog/2013/1/19/blocks-operations-and-retain-cycles.html。

2. 在正确的地方使用 reuseIdentifier
一个开发中常见的错误就是没有给UITableViewCells, UICollectionViewCells,甚至是UITableViewHeaderFooterViews设置正确的reuseIdentifier。
为了性能最优化,table view用 `tableView:cellForRowAtIndexPath:` 为rows分配cells的时候,它的数据应该重用自UITableViewCell。 一个table view维持一个队列的数据可重用的UITableViewCell对象。
不使用reuseIdentifier的话,每显示一行table view就不得不设置全新的cell。这对性能的影响可是相当大的,尤其会使app的滚动体验大打折扣。
自iOS6起,除了UICollectionView的cells和补充views,你也应该在header和footer views中使用reuseIdentifiers。
想要使用reuseIdentifiers的话,在一个table view中添加一个新的cell时在data source object中添加这个方法:
1
2
static NSString *CellIdentifier = @"Cell";
 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
这个方法把那些已经存在的cell从队列中排除,或者在必要时使用先前注册的nib或者class创造新的cell。如果没有可重用的cell,你也没有注册一个class或者nib的话,这个方法返回nil。

3.尽量把views设置为不透明
如果你有不透明的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!

4. 避免过于庞大的XIB
iOS5中加入的Storyboards(分镜)正在快速取代XIB。然而XIB在一些场景中仍然很有用。比如你的app需要适应iOS5之前的设备,或者你有一个自定义的可重用的view,你就不可避免地要用到他们。
如果你不得不XIB的话,使他们尽量简单。尝试为每个Controller配置一个单独的XIB,尽可能把一个View Controller的view层次结构分散到单独的XIB中去。
需要注意的是,当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。Storyboards就是另一码事儿了,storyboard仅在需要时实例化一个view controller.
当家在XIB是,所有图片都被chache,如果你在做OS X开发的话,声音文件也是。Apple在相关文档中的记述是:
当你加载一个引用了图片或者声音资源的nib时,nib加载代码会把图片和声音文件写进内存。在OS X中,图片和声音资源被缓存在named cache中以便将来用到时获取。在iOS中,仅图片资源会被存进named caches。取决于你所在的平台,使用NSImage 或UIImage 的`imageNamed:`方法来获取图片资源。
很明显,同样的事情也发生在storyboards中,但我并没有找到任何支持这个结论的文档。如果你了解这个操作,写信给我!
想要了解更多关于storyboards的内容的话你可以看看 Matthijs Hollemans的Beginning Storyboards in iOS 5 Part 1 和 Part 2

5. 不要阻塞主线程
永远不要使主线程承担过多。因为UIKit在主线程上做所有工作,渲染,管理触摸反应,回应输入等都需要在它上面完成。
一直使用主线程的风险就是如果你的代码真的block了主线程,你的app会失去反应。这。。。正是在App Store中拿到一颗星的捷径 :]
大部分阻碍主进程的情形是你的app在做一些牵涉到读写外部资源的I/O操作,比如存储或者网络。
你可以使用`NSURLConnection`异步地做网络操作:
+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler
或者使用像 AFNetworking这样的框架来异步地做这些操作。
如果你需要做其它类型的需要耗费巨大资源的操作(比如时间敏感的计算或者存储读写)那就用 Grand Central Dispatch,或者 NSOperation 和 NSOperationQueues.
下面代码是使用GCD的模板
1
2
3
4
5
6
7
8
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // switch to a background thread and perform your expensive operation
 
    dispatch_async(dispatch_get_main_queue(), ^{
        // switch back to the main thread to update your UI
 
    });
});
发现代码中有一个嵌套的`dispatch_async`吗?这是因为任何UIKit相关的代码需要在主线程上进行。
如果你对 NSOperation 或者GCD 的细节感兴趣的话,看看Ray Wenderlich的 Multithreading and Grand Central Dispatch on iOS for Beginners, 还有 Soheil Azarpour 的 How To Use NSOperations and NSOperationQueues 教程。

6. 在Image Views中调整图片大小
如果要在`UIImageView`中显示一个来自bundle的图片,你应保证图片的大小和UIImageView的大小相同。在运行中缩放图片是很耗费资源的,特别是`UIImageView`嵌套在`UIScrollView`中的情况下。
如果图片是从远端服务加载的你不能控制图片大小,比如在下载前调整到合适大小的话,你可以在下载完成后,最好是用background thread,缩放一次,然后在UIImageView中使用缩放后的图片。

7. 选择正确的Collection
学会选择对业务场景最合适的类或者对象是写出能效高的代码的基础。当处理collections时这句话尤其正确。
Apple有一个 Collections Programming Topics 的文档详尽介绍了可用的classes间的差别和你该在哪些场景中使用它们。这对于任何使用collections的人来说是一个必读的文档。
呵呵,我就知道你因为太长没看…这是一些常见collection的总结:
  • Arrays: 有序的一组值。使用index来lookup很快,使用value lookup很慢, 插入/删除很慢。
  • Dictionaries: 存储键值对。 用键来查找比较快。
  • Sets: 无序的一组值。用值来查找很快,插入/删除很快。
8. 打开gzip压缩
大量app依赖于远端资源和第三方API,你可能会开发一个需要从远端下载XML, JSON, HTML或者其它格式的app。
问题是我们的目标是移动设备,因此你就不能指望网络状况有多好。一个用户现在还在edge网络,下一分钟可能就切换到了3G。不论什么场景,你肯定不想让你的用户等太长时间。
减小文档的一个方式就是在服务端和你的app中打开gzip。这对于文字这种能有更高压缩率的数据来说会有更显著的效用。
好消息是,iOS已经在NSURLConnection中默认支持了gzip压缩,当然AFNetworking这些基于它的框架亦然。像Google App Engine这些云服务提供者也已经支持了压缩输出。
如果你不知道如何利用Apache或者IIS(服务器)来打开gzip,可以读下这篇文章

中级性能提升

你确信你已经掌握了前述那些基础级的优化方案了吗?但实际情况是,有时一些解决方案并不像那些一样明显,它们往往严重依赖于你如何架构和书写你的app。下面的这些建议就是针对这些场景的。

9. 重用和延迟加载(lazy load) Views
更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。
这里我们用到的技巧就是模仿`UITableView`和`UICollectionView`的操作: 不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。
这样的话你就只需要在滚动发生时创建你的views,避免了不划算的内存分配。
创建views的能效问题也适用于你app的其它方面。想象一下一个用户点击一个按钮的时候需要呈现一个view的场景。有两种实现方法:
  • 1. 创建并隐藏这个view当这个screen加载的时候,当需要时显示它;
  • 2. 当需要时才创建并展示。
每个方案都有其优缺点。
用第一种方案的话因为你需要一开始就创建一个view并保持它直到不再使用,这就会更加消耗内存。然而这也会使你的app操作更敏感因为当用户点击按钮的时候它只需要改变一下这个view的可见性。
第二种方案则相反-消耗更少内存,但是会在点击按钮的时候比第一种稍显卡顿。

10. Cache, Cache, 还是Cache!
一个极好的原则就是,缓存所需要的,也就是那些不大可能改变但是需要经常读取的东西。
我们能缓存些什么呢?一些选项是,远端服务器的响应,图片,甚至计算结果,比如UITableView的行高。
NSURLConnection默认会缓存资源在内存或者存储中根据它所加载的HTTP Headers。你甚至可以手动创建一个NSURLRequest然后使它只加载缓存的值。
下面是一个可用的代码段,你可以可以用它去为一个基本不会改变的图片创建一个NSURLRequest并缓存它:
1
2
3
4
5
6
7
8
9
10
+ (NSMutableURLRequest *)imageRequestWithURL:(NSURL *)url {
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
 
    request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; // this will make sure the request always returns the cached image
    request.HTTPShouldHandleCookies = NO;
    request.HTTPShouldUsePipelining = YES;
    [request addValue:@"image/*" forHTTPHeaderField:@"Accept"];
 
    return request;
}
注意你可以通过 NSURLConnection 获取一个URL request, AFNetworking也一样的。这样你就不必为采用这条tip而改变所有的networking代码了。
如果想了解更多关于HTTP caching, NSURLCache, NSURLConnection的相关知识,可以读下这篇文章()
如果你需要缓存其它不是HTTP Request的东西,你可以用NSCache。
NSCache和NSDictionary类似,不同的是系统回收内存的时候它会自动删掉它的内容。 Mattt Thompson有一篇很棒的关于它的文章::http://nshipster.com/nscache/
如果你对HTTP感兴趣可以读下Google的这篇 best-practices document on HTTP caching

11. 权衡渲染方法
在iOS中可以有很多方法做出漂亮的按钮。你可以用整幅的图片,可调大小的图片,uozhe可以用CALayer, CoreGraphics甚至OpenGL来画它们。
当然每个不同的解决方法都有不同的复杂程度和相应的性能。有一篇Apple UIKit team中的一员Andy Matuschak推荐过的很棒的关于graphic性能的帖子很值得一读。
简单来说,就是用事先渲染好的图片更快一些,因为如此一来iOS就免去了创建一个图片再画东西上去然后显示在屏幕上的程序。问题是你需要把所有你需要用到的图片放到app的bundle里面,这样就增加了体积 – 这就是使用可变大小的图片更好的地方了: 你可以省去一些不必要的空间,也不需要再为不同的元素(比如按钮)来做不同的图。
然而,使用图片也意味着你失去了使用代码调整图片的机动性,你需要一遍又一遍不断地重做他们,这样就很浪费时间了,而且你如果要做一个动画效果,虽然每幅图只是一些细节的变化你就需要很多的图片造成bundle大小的不断增大。
总得来说,你需要权衡一下利弊,到底是要性能能还是要bundle保持合适的大小。

12. 处理内存警告
一旦系统内存过低,iOS会通知所有运行中app。在官方文档中是这样记述:
如果你的app收到了内存警告,它就需要尽可能释放更多的内存。最佳方式是移除对缓存,图片object和其他一些可以重创建的objects的strong references.
幸运的是,UIKit提供了几种收集低内存警告的方法:
  • 在app delegate中使用`applicationDidReceiveMemoryWarning:` 的方法
  • 在你的自定义UIViewController的子类(subclass)中覆盖`didReceiveMemoryWarning`
  • 注册并接收 UIApplicationDidReceiveMemoryWarningNotification 的通知
一旦收到这类通知,你就需要释放任何不必要的内存使用。
例如,UIViewController的默认行为是移除一些不可见的view, 它的一些子类则可以补充这个方法,删掉一些额外的数据结构。一个有图片缓存的app可以移除不在屏幕上显示的图片。
这样对内存警报的处理是很必要的,若不重视,你的app就可能被系统杀掉。
然而,当你一定要确认你所选择的object是可以被重现创建的来释放内存。一定要在开发中用模拟器中的内存提醒模拟去测试一下。

13. 重用大开销对象
Reuse Expensive Objects
一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。然而,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。
想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。
注意如果你要选择第二种方法,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似。
下面的代码说明了使用一个属性来延迟加载一个date formatter. 第一次调用时它会创建一个新的实例,以后的调用则将返回已经创建的实例:
1
2
3
4
5
6
7
8
9
10
11
12
// in your .h or inside a class extension
@property (nonatomic, strong) NSDateFormatter *formatter;
 
// inside the implementation (.m)
// When you need, just use self.formatter
- (NSDateFormatter *)formatter {
    if (! _formatter) {
        _formatter = [[NSDateFormatter alloc] init];
        _formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy"; // twitter date format
    }
    return _formatter;
}
还需要注意的是,其实设置一个NSDateFormatter的速度差不多是和创建新的一样慢的!所以如果你的app需要经常进行日期格式处理的话,你会从这个方法中得到不小的性能提升。

14. 使用Sprite Sheets
你是一个游戏开发者吗,那么Sprite sheets一定是一个你的最好的朋友了。Sprite sheet可以让渲染速度加快,甚至比标准的屏幕渲染方法节省内存。
我们有两个很好的关于Sprite的教程:
  1. How To Use Animations and Sprite Sheets in Cocos2D
  2. How to Create and Optimize Sprite Sheets in Cocos2D with Texture Packer and Pixel Formats
第二个教程涵盖了可能在很大程度上影响你游戏性能的pixel格式的细节。
如果你对于spirte sheet还不是很熟悉,可以看下这两个(youtube)视频SpriteSheets – The Movie, Part 1 和 Part 2。视频的作者是创建Sprite sheet很流行的工具之一Texture Packer的作者Andreas Löw。
除了使用Sprite sheets,其它写在这里的建议当然也可以用于游戏开发中。比如你需要很多的Sprite sheets,像敌人,导弹之类的动作类必备元素,你可以重用这些sprites而不用每次都要重新创建。

15. 避免反复处理数据
许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要。在内存中操作数据使它们满足你的数据结构是开销很大的。
比如你需要数据来展示一个table view,最好直接从服务器取array结构的数据以避免额外的中间数据结构改变。
类似的,如果需要从特定key中取数据,那么就使用键值对的dictionary。

16. 选择正确的数据格式
从app和网络服务间传输数据有很多方案,最常见的就是JSON和XML。你需要选择对你的app来说最合适的一个。
解析JSON会比XML更快一些,JSON也通常更小更便于传输。从iOS5起有了官方内建的JSON deserialization 就更加方便使用了。
但是XML也有XML的好处,比如使用SAX 来解析XML就像解析本地文件一样,你不需像解析json一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会极大地减低内存消耗和增加性能。

17. 正确设定背景图片
在View里放背景图片就像很多其它iOS编程一样有很多方法:
  1. 使用UIColor的 colorWithPatternImage来设置背景色;
  2. 在view中添加一个UIImageView作为一个子View。
如果你使用全画幅的背景图,你就必须使用UIImageView因为UIColor的colorWithPatternImage是用来创建小的重复的图片作为背景的。这种情形下使用UIImageView可以节约不少的内存:
1
2
3
// You could also achieve the same result in Interface Builder
UIImageView *backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]];
[self.view addSubview:backgroundView];
如果你用小图平铺来创建背景,你就需要用UIColor的colorWithPatternImage来做了,它会更快地渲染也不会花费很多内存:
1
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];

18. 减少使用Web特性
UIWebView很有用,用它来展示网页内容或者创建UIKit很难做到的动画效果是很简单的一件事。
但是你可能有注意到UIWebView并不像驱动Safari的那么快。这是由于以JIT compilation 为特色的Webkit的Nitro Engine的限制。
所以想要更高的性能你就要调整下你的HTML了。第一件要做的事就是尽可能移除不必要的javascript,避免使用过大的框架。能只用原生js就更好了。
另外,尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。
最后,永远要注意你使用的图片,保证图片的符合你使用的大小。使用Sprite sheet提高加载速度和节约内存。

19. 设定Shadow Path
如何在一个View或者一个layer上加一个shadow呢,QuartzCore框架是很多开发者的选择:
1
2
3
4
5
6
7
8
9
#import <QuartzCore/QuartzCore.h>
 
// Somewhere later ...
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];
使用shadow path的话iOS就不必每次都计算如何渲染,它使用一个预先计算好的路径。但问题是自己计算path的话可能在某些View中比较困难,且每当view的frame变化的时候你都需要去update shadow path.
想了解更多可以看看Mark Pospesel的这篇

20. 优化Table View
Table view需要有很好的滚动性能,不然用户会在滚动过程中发现动画的瑕疵。
为了保证table view平滑滚动,确保你采取了以下的措施:
  • 正确使用`reuseIdentifier`来重用cells
  • 尽量使所有的view opaque,包括cell自身
  • 避免渐变,图片缩放,后台选人
  • 缓存行高
  • 如果cell内现实的内容来自web,使用异步加载,缓存请求结果
  • 使用`shadowPath`来画阴影
  • 减少subviews的数量
  • 尽量不适用`cellForRowAtIndexPath:`,如果你需要用到它,只用一次然后缓存结果
  • 使用正确的数据结构来存储数据
  • 使用`rowHeight`, `sectionFooterHeight` 和 `sectionHeaderHeight`来设定固定的高,不要请求delegate
21. 选择正确的数据存储选项

当存储大块数据时你会怎么做?
你有很多选择,比如:
  • 使用`NSUerDefaults`
  • 使用XML, JSON, 或者 plist
  • 使用NSCoding存档
  • 使用类似SQLite的本地SQL数据库
  • 使用 Core Data
NSUserDefaults的问题是什么?虽然它很nice也很便捷,但是它只适用于小数据,比如一些简单的布尔型的设置选项,再大点你就要考虑其它方式了
XML这种结构化档案呢?总体来说,你需要读取整个文件到内存里去解析,这样是很不经济的。使用SAX又是一个很麻烦的事情。
NSCoding?不幸的是,它也需要读写文件,所以也有以上问题。
在这种应用场景下,使用SQLite 或者 Core Data比较好。使用这些技术你用特定的查询语句就能只加载你需要的对象。
在性能层面来讲,SQLite和Core Data是很相似的。他们的不同在于具体使用方法。Core Data代表一个对象的graph model,但SQLite就是一个DBMS。Apple在一般情况下建议使用Core Data,但是如果你有理由不使用它,那么就去使用更加底层的SQLite吧。
如果你使用SQLite,你可以用FMDB(https://github.com/ccgus/fmdb)这个库来简化SQLite的操作,这样你就不用花很多经历了解SQLite的C API了。

进阶性能提示

想要一些是你成为程序猿忍者的精英级的建议吗?下面这些提示可以帮你把你的app优化到极致!
22. 加速启动时间
快速打开app是很重要的,特别是用户第一次打开它时,对app来讲,第一印象太太太重要了。
你能做的就是使它尽可能做更多的异步任务,比如加载远端或者数据库数据,解析数据。
还是那句话,避免过于庞大的XIB,因为他们是在主线程上加载的。所以尽量使用没有这个问题的Storyboards吧!
注意,用Xcode debug时watchdog并不运行,一定要把设备从Xcode断开来测试启动速度

23. 使用Autorelease Pool
`NSAutoreleasePool`负责释放block中的autoreleased objects。一般情况下它会自动被UIKit调用。但是有些状况下你也需要手动去创建它。
假如你创建很多临时对象,你会发现内存一直在减少直到这些对象被release的时候。这是因为只有当UIKit用光了autorelease pool的时候memory才会被释放。
好消息是你可以在你自己的@autoreleasepool里创建临时的对象来避免这个行为:
1
2
3
4
5
6
7
8
9
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. */
    }
}
这段代码在每次遍历后释放所有autorelease对象
更多关于NSAutoreleasePool请参考官方文档

24. 选择是否缓存图片
常见的从bundle中加载图片的方式有两种,一个是用`imageNamed`,二是用`imageWithContentsOfFile`,第一种比较常见一点。
既然有两种类似的方法来实现相同的目的,那么他们之间的差别是什么呢?
`imageNamed`的优点是当加载时会缓存图片。`imageNamed`的文档中这么说:
这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话。如果缓存中没有找到相应的图片,这个方法从指定的文档中加载然后缓存并返回这个对象。
相反的,`imageWithContentsOfFile`仅加载图片。
下面的代码说明了这两种方法的用法:
1
2
3
UIImage *img = [UIImage imageNamed:@"myImage"]; // caching
 // or
 UIImage *img = [UIImage imageWithContentsOfFile:@"myImage"]; // no caching
那么我们应该如何选择呢?
如果你要加载一个大图片而且是一次性使用,那么就没必要缓存这个图片,用`imageWithContentsOfFile`足矣,这样不会浪费内存来缓存它。
然而,在图片反复重用的情况下`imageNamed`是一个好得多的选择。

25. 避免日期格式转换
如果你要用`NSDateFormatter`来处理很多日期格式,应该小心以待。就像先前提到的,任何时候重用`NSDateFormatters`都是一个好的实践。
然而,如果你需要更多速度,那么直接用C是一个好的方案。Sam Soffes有一个不错的帖子(http://soff.es/how-to-drastically-improve-your-app-with-an-afternoon-and-instruments)里面有一些可以用来解析ISO-8601日期字符串的代码,简单重写一下就可以拿来用了。
嗯,直接用C来搞,看起来不错了,但是你相信吗,我们还有更好的方案!
如果你可以控制你所处理的日期格式,尽量选择Unix时间戳。你可以方便地从时间戳转换到NSDate:
1
2
3
- (NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp {
 return [NSDate dateWithTimeIntervalSince1970:timestamp];
 }
这样会比用C来解析日期字符串还快!
需要注意的是,许多web API会以微秒的形式返回时间戳,因为这种格式在javascript中更方便使用。记住用`dateFromUnixTimestamp`之前除以1000就好了。

更多阅读

下列这些WWDC视频强烈推荐给想要提高app性能的开发者。你首先需要保证你有使你的Apple ID注册为一个开发者身份才能看在这里看WWDC 2012的视频
  • #406: Adopting Automatic Reference Counting
  • #238: iOS App Performance: Graphics and Animations
  • #242: iOS App Performance: Memory
  • #235: iOS App Performance: Responsiveness
  • #409: Learning Instruments
  • #706: Networking Best Practices
  • #514: OpenGL ES Tools and Techniques
  • #506: Optimizing 2D Graphics and Animation Performance
  • #601: Optimizing Web Content in UIWebViews and Websites on iOS
  • #225: Up and Running: Making a Great Impression with Every Launch
一些01年的WWDC视频也很有价值:
  • #308: Blocks and Grand Central Dispatch in Practice
  • #323: Introducing Automatic Reference Counting
  • #312: iOS Performance and Power Optimization with Instruments
  • #105: Polishing Your App: Tips and tricks to improve the responsiveness and performance
  • #121: Understanding UIKit Rendering
其它一些值得看的视频,大部分来自iOS 5 Tech Talks
  • Your iOS App Performance Hitlist
  • Optimizing App Performance with Instruments
  • Understanding iOS View Compositing
基于《Your iOS App Performance Hitlist》这个Michael Jurewitz的视频,Ole Begemann写了一篇文字总结的文章
Apple提供了一个非常有用的叫做“Performance Tuning | 性能调优”的资源。

UITableView优化技巧

最近在微博上看到一个很好的开源项目VVeboTableViewDemo,是关于如何优化UITableView的。加上正好最近也在优化项目中的类似朋友圈功能这块,思考了很多关于UITableView的优化技巧,相信这块是难点也是痛点,所以决定详细的整理下我对优化UITableView的理解。
UITableView作为iOS开发中最重要的控件之一,其中的实现原理很是考究。Apple在这块的优化水平直接决定了iOS的体验能甩安卓几条街,哈哈,扯淡扯多了。。。好了,废话不多说,直接进入主题。首先来谈谈我对UITableView的认识:

UITableView的简单认识

UITableView最核心的思想就是UITableViewCell的重用机制。简单的理解就是:UITableView只会创建一屏幕(或一屏幕多一点)的UITableViewCell,其他都是从中取出来重用的。每当Cell滑出屏幕时,就会放入到一个集合(或数组)中(这里就相当于一个重用池),当要显示某一位置的Cell时,会先去集合(或数组)中取,如果有,就直接拿来显示;如果没有,才会创建。这样做的好处可想而知,极大的减少了内存的开销。
知道UITableViewCell的重用原理后,我们来看看UITableView的回调方法。UITableView最主要的两个回调方法是tableView:cellForRowAtIndexPath:tableView:heightForRowAtIndexPath:。理想上我们是会认为UITableView会先调用前者,再调用后者,因为这和我们创建控件的思路是一样的,先创建它,再设置它的布局。但实际上却并非如此,我们都知道,UITableView是继承自UIScrollView的,需要先确定它的contentSize及每个Cell的位置,然后才会把重用的Cell放置到对应的位置。所以事实上,UITableView的回调顺序是先多次调用tableView:heightForRowAtIndexPath:以确定contentSize及Cell的位置,然后才会调用tableView:cellForRowAtIndexPath:,从而来显示在当前屏幕的Cell。
举个例子来说:如果现在要显示100个Cell,当前屏幕显示5个。那么刷新(reload)UITableView时,UITableView会先调用100次tableView:heightForRowAtIndexPath:方法,然后调用5次tableView:cellForRowAtIndexPath:方法;滚动屏幕时,每当Cell滚入屏幕,都会调用一次tableView:heightForRowAtIndexPath:tableView:cellForRowAtIndexPath:方法。
看到这里,想必大伙也都能隐约察觉到,UITableView优化的首要任务是要优化上面两个回调方法。事实也确实如此,下面按照我探讨进阶的过程,来研究如何优化:
优化探索,项目拿到手时代码是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    ContacterTableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ContacterTableCell"];
    if (!cell) {
        cell = (ContacterTableCell *)[[[NSBundle mainBundle] loadNibNamed:@"ContacterTableCell" owner:self options:nil] lastObject];
    }
    NSDictionary *dict = self.dataList[indexPath.row];
    [cell setContentInfo:dict];
    return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    return cell.frame.size.height;
}
看到这段代码,对于刚毕业的我来说,觉得还是蛮巧妙的,但巧归巧,当Cell非常复杂的时候,直接卡出翔了。。。特别是在我的Touch4上,这我能忍?!好吧,依据上面UITableView原理的分析,我们先来分析它为什么卡?
这样写,在Cell赋值内容的时候,会根据内容设置布局,当然也就可以知道Cell的高度,想想如果1000行,那就会调用1000+页面Cell个数次tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法,而我们对Cell的处理操作,都是在这个方法里的!什么赋值、布局等等。开销自然很大,这种方案Pass。。。改进代码。
改进代码后:
1
2
3
4
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSDictionary *dict = self.dataList[indexPath.row];
    return [ContacterTableCell cellHeightOfInfo:dict];
}
思路是把赋值和计算布局分离。这样让tableView:cellForRowAtIndexPath:方法只负责赋值,tableView:heightForRowAtIndexPath:方法只负责计算高度。注意:两个方法尽可能的各司其职,不要重叠代码!两者都需要尽可能的简单易算。Run一下,会发现UITableView滚动流畅了很多。。。
基于上面的实现思路,我们可以在获得数据后,直接先根据数据源计算出对应的布局,并缓存到数据源中,这样在tableView:heightForRowAtIndexPath:方法中就直接返回高度,而不需要每次都计算了。
1
2
3
4
5
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
     NSDictionary *dict = self.dataList[indexPath.row];
 CGRect rect = [dict[@"frame"] CGRectValue];
     return rect.frame.height;
}
其实上面的改进方法并不是最佳方案,但基本能满足简单的界面!记得开头我的任务吗?像朋友圈那样的图文混排,这种方案还是扛不住的!我们需要进入更深层次的探究:自定义Cell的绘制
我们在Cell上添加系统控件的时候,实质上系统都需要调用底层的接口进行绘制,当我们大量添加控件时,对资源的开销也会很大,所以我们可以索性直接绘制,提高效率。是不是说的很抽象?废话不多说,直接上代码:
首先需要给自定义的Cell添加draw方法,(当然也可以重写drawRect)然后在方法体中实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//异步绘制
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        CGRect rect = [_data[@"frame"] CGRectValue];
        UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
        CGContextRef context = UIGraphicsGetCurrentContext();
 //整个内容的背景
        [[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];
        CGContextFillRect(context, rect);
 //转发内容的背景
        if ([_data valueForKey:@"subData"]) {
            [[UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1] set];
            CGRect subFrame = [_data[@"subData"][@"frame"] CGRectValue];
            CGContextFillRect(context, subFrame);
            [[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set];
            CGContextFillRect(context, CGRectMake(0, subFrame.origin.y, rect.size.width, .5));
        }
        
        {
     //名字
            float leftX = SIZE_GAP_LEFT+SIZE_AVATAR+SIZE_GAP_BIG;
            float x = leftX;
            float y = (SIZE_AVATAR-(SIZE_FONT_NAME+SIZE_FONT_SUBTITLE+6))/2-2+SIZE_GAP_TOP+SIZE_GAP_SMALL-5;
            [_data[@"name"] drawInContext:context withPosition:CGPointMake(x, y) andFont:FontWithSize(SIZE_FONT_NAME)
                             andTextColor:[UIColor colorWithRed:106/255.0 green:140/255.0 blue:181/255.0 alpha:1]
                                andHeight:rect.size.height];
     //时间+设备
            y += SIZE_FONT_NAME+5;
            float fromX = leftX;
            float size = [UIScreen screenWidth]-leftX;
            NSString *from = [NSString stringWithFormat:@"%@  %@", _data[@"time"], _data[@"from"]];
            [from drawInContext:context withPosition:CGPointMake(fromX, y) andFont:FontWithSize(SIZE_FONT_SUBTITLE)
                   andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]
                      andHeight:rect.size.height andWidth:size];
        }
 //将绘制的内容以图片的形式返回,并调主线程显示
 UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        dispatch_async(dispatch_get_main_queue(), ^{
            if (flag==drawColorFlag) {
                postBGView.frame = rect;
                postBGView.image = nil;
                postBGView.image = temp;
            }
 }
 //内容如果是图文混排,就添加View,用CoreText绘制
 [self drawText];
}}
上述代码只贴出来部分功能,但大体的思路都是一样的,各个信息都是根据之前算好的布局进行绘制的。这里是需要异步绘制,但如果在重写drawRect方法就不需要用GCD异步线程了,因为drawRect本来就是异步绘制的。对于图文混排的绘制,可以移步Google,研究下CoreText,这块内容太多了,不便展开。
好了,至此,我们又让UITableView的效率提高了一个等级!但我们的步伐还远远不止这些,下面我们还可以从UIScrollView的角度出发,再次找到突破口。
滑动UITableView时,按需加载对应的内容
直接上代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
    NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
    NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
    NSInteger skipCount = 8;
    if (labs(cip.row-ip.row)>skipCount) {
        NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
        NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
        if (velocity.y<0) {
            NSIndexPath *indexPath = [temp lastObject];
            if (indexPath.row+3<datas.count) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
            }
        } else {
            NSIndexPath *indexPath = [temp firstObject];
            if (indexPath.row>3) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
            }
        }
        [needLoadArr addObjectsFromArray:arr];
    }
}
记得在tableView:cellForRowAtIndexPath:方法中加入判断:
1
2
3
4
if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
    [cell clear];
    return;
}
滚动很快时,只加载目标范围内的Cell,这样按需加载,极大的提高流畅度。
写了这么多,也差不多该来个总结了!UITableView的优化主要从三个方面入手:
  • 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法;
  • 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;
  • 滑动时按需加载,这个在大量图片展示,网络加载的时候很管用!(SDWebImage已经实现异步加载,配合这条性能杠杠的)。
除了上面最主要的三个方面外,还有很多几乎大伙都很熟知的优化点:
  • 正确使用reuseIdentifier来重用Cells
  • 尽量使所有的view opaque,包括Cell自身
  • 尽量少用或不用透明图层
  • 如果Cell内现实的内容来自web,使用异步加载,缓存请求结果
  • 减少subviews的数量
  • heightForRowAtIndexPath:中尽量不使用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
  • 尽量少用addView给Cell动态添加View,可以初始化时就添加,然后通过hide来控制是否显示
尾巴
肯定很多人会非常好奇,为什么我都是手动用代码创建Cell的?现在主流不都是XibStoryboard什么的嘛?我的回答是:要想提高效率,还是手动写有用!抛开XibStoryboard需要系统自动转码,给系统多加了一层负担不谈,自定义Cell的绘制更是无从下手,所以,在我看来,复杂的需要高效的界面,还是手动写代码吧!!!
最后如果你们的项目都是用的XibStoryboard,并需要优化UITableView的话,sunnyxx大神提出了好的方案:http://blog.sunnyxx.com/2015/05/17/cell-height-calculation/ 大伙可以自行研究研究。
知识是需要不断学习的,作为刚上路的我,如果有什么理解不到位的,欢迎大伙留言指正,如果你有什么更牛逼的想法,希望一起交流交流。
注明:本篇的分析源码来源于开源项目VVeboTableViewDemo
参考:https://github.com/johnil/VVeboTableViewDemo

UITableView 滚动流畅性优化

影响 UITableView 滚动的流畅性的原因

  1. 在代理方法中做了过多的计算占用了 UI 线程的时间
  2. 同上
  3. Cell 中 view 的组织复杂
关于第一点,首先要明白 tableview 的代理(这里指 datasource 和 delegate 的那套方法,下同)方法的调用顺序,和时机。对于一般的应用会有如下顺序:
  1. 向代理要 number Of Rows。
  2. 对于每行向代理要 height For Row At Index Path。
  3. 向代理要 当前屏幕可见的 cell For Row At Index Path 。(实测显示4寸屏的手机会取 屏幕显示数量+2,3.5 寸屏同4寸屏数量,虽然 3.5 寸屏可显示的cell 数量要小于 4 寸屏!)
  4. 然后 cell 就显示出来了。

tableView:heightForRowAtIndexPath:

很多人都把优化的重点放到了 cell for row at indexpath 那个方法里了,在这里尽可能的少计算,但是却忽略了另一个很轻松就能提升加载时间的方法 :
   - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
Table View 在每次 reload data 时都会要所有 cell 的高度!这就是说你有一百行 cell,就像代理要100次每个cell 的高度,而不是当前屏幕显示的cell 的数量的高度!虽然在 iOS 7 下多了计算 cell 高度的方法,但是减少 计算高度时的时间,对于提升加载 Table View 的速度有非常明显的提高!
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
(**iOS 7专用**)
但是有人说了,我早听别人说了,reloadData 方法尽量不要调用,我插入新行都用 insertRowsAtIndexPaths:withRowAnimation: 删除也用 delete 那个,这个总行了吧?!
这样也不能忽略 height For Row At Index Path 这个回调的重要性。因为在每次插入或者删除一行后同样需要调用一遍 所有行 的这个回调方法!是所有行!你没看错,所有只是简单的减少一个代理方法的计算量,就可以明显的提升加载速度。
对于提升 tableView:heightForRowAtIndexPath: 计算量,就是尽可能的让这个方法的计算复杂度为 O(1),就是只是简单的从数组中取一个值,然后返回!
也许有人又要问了,我的应用都是动态的高度,就像微博那样的,不定数量的文字,可能还有图片,大小也不固定,这些怎么返回固定的高度啊?
我指的固定高度不是 row 的高度都一样那种固定,而是让在 tableView:heightForRowAtIndexPath: 这个回调里取这个高度的时间是近乎固定的。
对于高度的计算,还有个小细节需要注意,就是如果 row 的高度都一定,那就删除代理中的这个 tableView:heightForRowAtIndexPath: 方法,设置 Table View 的 rowHeight 属性,相似的 numberOfRowsInSection: 系列的方法,我就不都写出来了。苹果的文档里介绍这样也可以减少了调用时间。
现在回归正题。对于 cell 高度不固定的,传统的方法是为 cell 写个计算行高的类方法,传入那些动态的元素(文字,图片等),然后返回计算后的高度。在 tableView:heightForRowAtIndexPath: 中调用这个方法,填入需要的参数计算cell 高度。这当然没有什么问题,只是要是计算量很复杂,你每次 reloadData ,光计算行高就要花去 rowCount * 单行高评价计算时间,想想有100行,你不定期的需要 reloadData 或者 insert(delete) row。。。。解决办法就是:
用 “空间换时间”
将计算行高的时间提前到从服务器搂回数据的时候,计算完了高度一并写回数据库,别告诉我你在主线程里阻塞式的处理网络请求。。。。面壁思过去吧,别浪费了 GCD,NSOperationQueue的青春。最先想到的还是 NSThread 的同学,证明你已经老了。。。现在几乎大部分的多线程操作都不需要用到 NSThread 和 runloop了。

tableView:cellForRowAtIndexPath:

说完了计算 cell 行高的优化,现在来谈 tableView:cellForRowAtIndexPath: 回调的优化。优化思路同上,也是通过预处理减少在这个回调中的计算时间。这个回调重点谈的是对图片异步加载的优化。
图片异步加载无非就是在这个方法里发起异步请求,图片加载完后根据 UIImageView 的引用设置图片。有经验的程序员可能会使用 懒加载 的方式减少快速滑动时因为网络请求过于频繁与切换线程显示图片造成的卡顿。这里还有个问题,拿回来的图片一定和最后显示的大小不一样,有时候偷懒,直接设置 image view 的 contentMode 属性要 image view 自己 *压缩*。这是一个很取巧的方法,但是对 table view 的滚动速度也会造成 不容忽视的影响。对图片变形需要对图片做 transform ,每次压缩图片都要对图片乘以一个变换矩阵,如果你的图片很多,这个计算量是不同忽视的。
优化建议:从网络搂回来图片后先根据需要显示的图片大小切成合适大小的图,每次只显示处理过大小的图片,当查看大图时在显示大图。如果服务器能直接返回预处理好的小图和图片的大小更好。
使用 Instrument 的 Core Animation 模板可以查看图片的压缩情况。如图:
Core Animation 模板
Instrument 中的 Core Animation 模板只有在调试真机时才有,调试模拟器上的应用没有这个模板!!!但是可以在模拟器的 Debug 菜单下找到这些调试选项。
切记:调试应用性能一定要用真机,Mac 的性能完爆 iPhone,所有不要说我的应用在模拟器上调试时不卡啊!模拟器只能模拟 iOS 软件的运行环境,不能模拟硬件性能!
Color Misaligned Images
这些选项对设备的所有应用有效,也就是说你不需要选择 target 就能调试 (方便竞品分析 :)!
对于 Misaligned images 会有两种颜色:一种是洋红色,另一种是黄色。
Misaligned images
洋红色是因为像素没对齐,比如上面的 label,一般情况下因为像素没对齐,需要抗锯齿,图像会出现模糊的现象。
解决办法:在设置 view 的 frame 时,在高分屏避免出现 21.3,6.7这样的小数,尤其是 x,y坐标,用 ceil 或 floor 或 round 取整。每 0.5 个点对应一个 pixel,0.3,0.7这样的就难为 iPhone 了,低分屏不要出现小数。
黄色是因为显示的图片实际大小与显示大小不同,对图片进行了拉伸,测试显示使用 image view 显示实际大小的图也会变黄。
减少洋红色和黄色可以提升滚动的流畅性

手动 Drawing 视图提升流畅性

如果通过上面的方法,滚动速度还不能达到可以容忍的速度,那就只剩下最后一个办法了,手动绘制视图。
手动绘制方法,不是直接子类化 UITableViewCell,然后覆盖 drawRect: 方法,这样你会得到一个大黑块!因为 cell 中不是只有一个 content view。如果不了解 cell 的层次结构,可以用 Reveal 去看下。
绘制 cell 不建议使用 UIView,建议使用 CALayer。 UIView 的绘制是建立在 CoreGraphic 上的,使用的是 CPU。CALayer 使用的是 Core Animation,CPU,GPU 通吃,由系统决定使用哪个。View的绘制使用的是自下向上的一层一层的绘制,然后渲染。Layer处理的是 Texure,利用 GPU 的 Texture Cache 和独立的浮点数计算单元加速 纹理 的处理。
问题已解:在 UIView 的 drawRect 里使用 CG 开头的绘图命令只是触发的伪离屏渲染,绘图还是靠 CPU 同步的在主线程绘制,在 layer 中触发的离屏渲染会触发真正的离屏渲染,在一个独立的进程里绘制。
https://lobste.rs/s/ckm4uw/a_performance-minded_take_on_ios_design/comments/itdkfh
GPU 不喜欢 透明,所以所有的绘图一定要弄成不透明,对于圆角和阴影这些的可以截个伪透明的小图然后绘制上去。在layer的回调里一定也只做绘图,不做计算!

评论

此博客中的热门博文

使__attribute__((annotate("xxx")))能作用于OC函数

viewWillAppear不执行的解决办法

iOS中使用xpc/xpc.h