25个增强iOS应用程序性能的提示和技巧 — 中级篇
本文由破船译自:raywenderlich
转载请注明出处:BeyondVincent的博客
_____________
在开发iOS应用程序时。让程序具有良好的性能是非常关键的。这也是用户所期望的。假设你的程序执行迟钝或缓慢,会招致用户的差评。
然而由于iOS设备的局限性,有时候要想获得良好的性能。是非常困难的。在开发过程中,有很多事项须要记住。而且关于性能影响非常easy就忘记。
这就是为什么我要写这篇文章。本文收集了25个关于能够提升程序性能的提示和技巧。
文件夹
中级
在性能优化时,当你碰到一些复杂的问题。应该注意和使用例如以下技巧:
- 重用和延迟载入View
- 缓存、缓存、缓存
- 考虑绘制
- 处理内存警告
- 重用花销非常大的对象
- 使用Sprite Sheets
- 避免又一次处理数据
- 选择正确的数据格式
- 设置适当的背景图片
- 降低Web内容的影响
- 设置阴影路径
- 优化TableView
- 选择正确的数据存储方式
中级性能提升
如今。在进行代码优化时,你已经能够完毕一些0基础性能优化了。
可是以下还有另外一些优化方案,尽管可能不太明显(取决于程序的架构和相关代码),可是,假设能够正确的利用好这些方案,那么它们对性能的优化将非常明显!
程序界面中包括很多其它的view。意味着界面在显示的时候。须要进行很多其它的绘制任务。也就意味着须要消耗很多其它的CPU和内存资源。
特别是在一个UIScrollView里面加入了很多view。
这样的情况的管理技巧能够參考UITableView和UICollectionView的行为:不要一次性创建全部的subview。而是在须要的时候在创建view。而且当view使用完毕时候将它们加入到重用队列中。
这样就能够仅在UIScrollView滚动的时候才配置view,以此能够避免分配创建view的带来的成本——这可能是非常耗资源的。
如今有这样的一个问题:在程序中须要显示的view在什么时机创建(比方说,当用户点击某个button,须要显示某个view)。这里有两种可选方法:
- 在屏幕第一次载入以及隐藏的时候,创建view;然后在须要的时候。再把view显示出来。
- 直到须要显示view的时候,才创建并显示view。
每种方法都有各自的长处和确定。
使用第一种方法,须要消耗很多其它的内容,由于创建出来的view一直占领着内存。直到view被release掉。只是,使用这样的方法。当用户点击button时,程序会非常快的显示出view,由于仅仅须要改动一下view的可见性就可以。
而使用另外一种方法则产生相反的效果;当须要的时候猜创建view,这会消耗更少的内存。只是。当用户点击button的时候,不会马上显示出view。
在开发程序时,一个重要的规则就是“缓存重要的内容”——这些内容一般不会改变,而且訪问的频率比較高。
能够缓存写什么内容呢?比方远程server的响应内容,图片,甚至是计算结果,比方UITableView的行高。
NSURLConnection依据HTTP头的处理过程,已经把一些资源缓存到磁盘和内存中了。你甚至能够手动创建一个NSURLRequest 。让其仅仅载入缓存的值。
以下的代码片段一般用在为图片创建一个NSURLRequest:
+ (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请求,可是相同能够使用AFNetworking来抓取。这样的方法不用改动全部网络相关的代码——这是一个技巧!
:]
假设你要直到很多其它关于HTTP 缓存, NSURLCache, NSURLConnection 以及相关的内容, 那么看一下NSHipster中的the NSURLCache entry。
假设你须要缓存的内容没涉及到HTTP请求。那么使用NSCache。
NSCache的外观和行为与NSDictionary相似, 可是,当系统须要回收内存时,NSCache会自己主动的里面存储的内容。Mattt Thompson 在NSHipster上写了一篇关于NSCache非常不错的文章。
假设还想知道关于HTTP缓存很多其它的内容,那么建议阅读一下Google的这篇文章:best-practices document on HTTP caching。
在iOS中制作美丽的button有多种方法。
能够使用全尺寸图片。可缩放图片。或者使用CALayer, CoreGraphics, 甚至是OpenGL来手动測量和绘制button。
当然,这些方法的复杂程度也不同,而且性能也有所差别。这里有一篇相关文章值得阅读一下:关于iOS中图形的性能。
当中Andy Matuschak(以前是苹果的UIKit小组的组员)对这篇文章的评论中,对于不同的方法及其性能权衡有非常好的一个见解。
简单来说,使用预渲染图片技术是最快的,由于iOS中不用等到在屏幕上显示的时候才创建图形和对形状进行绘制(图片已经创建好了!)。
这样带来的问题是须要把全部的图片都放到程序bundle中。从而添加了程序的大小。因此使用可伸缩图片在这里将排上用场了:能够移除“浪费”空间的图片——iOS能够反复利用。而且针对不同的元素(比如button)不须要创建不同的图片。
只是,使用图片的话会失去代码对图片的控制能力,进而针对不同的程序。就须要反复的生成每个须要的图片。并反复的放到每个程序中。这个处理过程通常会比較慢。另外一点就是假设你须要一个动画,或者很多图片都要进行轻微的调整(比方多个颜色的覆盖)。那么须要在程序中加入很多图片,进而添加了程序bundle的大小。
总的来说,你须要考虑一下什么才是最重要的:绘制性能还是程序大小。一般来说都重要,所以在同一个project中,应该两种都应考虑。
当系统内存偏低时,iOS会通知全部在执行的程序。苹果的官方文档中介绍了怎样处理低内存警告:
If your app receives this warning, it must free up as much memory as possible. The best way to do this is to remove strong references to caches, image objects, and other data objects that can be recreated later.
假设程序收到了低内存警告,在程序中必须尽量释放内存。
最佳方法就是移除强引用的涉及到的缓存,图片对象,以及其它能够在之后使用时还能够又一次创建的数据对象。
UIKit中提供了例如以下几种方法来接收低内存(low-memory)警告:
- 实现app delegate中的applicationDidReceiveMemoryWarning: 方法。
- 在UIViewController子类中重写(Override)didReceiveMemoryWarning方法。
- 在通知中心里面注冊UIApplicationDidReceiveMemoryWarningNotificatio通知。
在收到以上随意的警告时,须要马上释放不论什么不须要的内存。
比如,UIViewController的默认情况是清除掉当前不可见的view;在UIViewController的子类中,能够清除一些额外的数据。程序中不没有显示在当前屏幕中的图片也能够release掉。
当收到低内存警告时。尽量释放内存是非常重要的。否则,执行中的程序有可能会被系统杀掉。
只是,在清除内存时要注意一下:确保被清除的对象之后还能够被创建出来。另外,在开发程序的时候。请使用iOS模拟器中的模拟内存警告功能对程序进行測试!
有些对象的初始化非常慢——比方NSDateFormatter和NSCalendar。只是有时候能够避免使用这些对象,比如在解析JSON/XML中的日期时。
当使用这些对象时。为了避免性能上的瓶颈,能够尝试尽量重用这些对象——在类中加入一个属性或者创建一个静态变量。
注意。假设使用静态变量的话,对象会在程序执行的时候一直存在,就像单例一样。
以下的代码演示创建一个延迟载入的日期格式属性。第一次调用属性的时候。会创建一个新的日期格式。
之后再调用的话。会返回已经创建好的实例对象:
// 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的日期格式时。相同跟创建新的一个NSDateFormatter实例对象时一样慢!
因此。在程序中假设须要频繁的处理日期格式。那么对NSDateFormatter进行重用是非常好的。
你是一个游戏开发人员吗?是的话那么sprite sheets是最佳选择之中的一个。
使用Sprite sheets跟经常使用的绘制方法比起来。绘制更快,而且消耗更少的内存。
以下是两个非常不错的sprite sheets教程:
第二个教程详细的介绍了像素格式——在游戏中能够衡量性能的影响。
假设你还不熟悉sprite sheets。那么能够看看这里的介绍:SpriteSheets – 视频, Part 1and Part 2. 这两个视频的作者是Andreas Löw, 他是纹理包(Texture Packer)的创建者, 纹理包是创建sprite sheets的重要工具。
除了使用sprite sheets外,这里还介绍了一些用于游戏开发中的技巧,比如。假设你有非常多sprite(比方射击类游戏中),那么能够重用sprite,而不用每次都创建sprite。
很多程序都须要从远程server中获取数据,以满足程序的需求。这些数据通常是JSON或XML格式。在请求和接收数据时,使用相同的数据结构非常重要。
为什么呢?在内存中把数据转换为适合程序的数据格式是须要付出额外代价的。
比如。假设你须要在table view中显示一些数据。那么请求和接收的数据格式最好是数组格式的。这样能够避免一些中间操作——将数据转换为适合程序使用的数据结构。
相似的,假设程序是依据键来訪问详细的值,那么最好请求和接收一个键/值对字典。
在第一时间获得的数据就是所须要格式的,能够避免将数据转换为适合程序的数据格式带来的额外代价。
将数据从程序传到网络server中有多种方法,当中使用的数据格式基本都是JSON和XML。
你须要做的就是在程序中选择正确的数据格式。
JSON的解析速度非常快。而且要比XML小得多。也就意味着仅仅须要传输更少数据。
而且在iOS5之后,已经有内置的JSON反序列化API了。所以使用JSON是非常easy的。
只是XML也有它自己的优势:假设使用SAX方法来解析XML。那么能够边读XML边解析。并不用等到全部的XML获取到了才開始解析。这与JSON是不同的。当处理大量数据时,这样的方法能够提升性能并降低内存的消耗。
在iOS编码中,跟别的很多东西相似。这里也有两种方法来给view设置一个背景图片:
- 能够使用UIColor的colorWithPatternImge方法来创建一个颜色,并将这个颜色设置为view的背景颜色。
- 能够给view加入一个UIImageView子视图。
假设你有一个全尺寸的背景图片,那么应该使用UIImageView。由于UIColor的colorWithPatternImge方法是用来创建小图片的——该图片会被反复使用。
此时使用UIImageView会节省非常多内存。
// You could also achieve the same result in Interface Builder UIImageView *backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]]; [self.view addSubview:backgroundView];
只是,假设你计划用小图片当做背景。那么应该使用UIColor的colorWithPatternImge方法。
这样的情况下绘制速度会非常快,而且不会消耗大量的内存。
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];
UIWebView非常实用。用它能够非常easy的显示web内容,甚至能够构建UIKit空间难以显示的内容。
只是,你能够能已经注意到程序中使用的UIWebView组建没有苹果的Safari程序快。这是由于JIT编译限制了WebKit的Nitro引擎的使用。
因此为了获得更加的性能,须要调整一下HTML的大小。首先就是尽量的摆脱JavaScript,并避免使用大的矿建,比如jQuery。有时候使用原始的JavaScript要比别的框架快。
另外,尽量的异步载入JavaScript文件——特别是不直接影响到页面行为时,比如分析脚本。
最后——让使用到的图片,跟实际须要的一样大小。如之前提到的,尽量使用sprite sheets,以此节省内存和提升速度。
很多其它相关信息,能够看一下: WWDC 2012 session #601 – 在iOS中优化UIWebView和站点中的Web内容。
假设须要在view活layer中加入一个阴影。该怎样处理呢?
大多数开发人员首先将QuartzCore框架加入到project中,然后加入例如以下代码:
#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;
看起来非常easy。不是吗?
然而不幸的是上面这样的方法有一个问题。
Core Animation在渲染阴影效果之前,必须通过做一个离屏(offscreen)才干确定view的形状。而这个离屏操作非常耗费资源。
以下有一种方法能够更easy的让系统进行阴影渲染:设置阴影路径!
view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];
通过设置阴影路径,iOS就不用总是再计算该怎样绘制阴影了。仅仅须要使用你预先计算好的路径就可以。
有一点不好的是,依据view的格式,自己可能非常难计算出路径。另外一个问题就是当view的frame改变时,必须每次都更新一下阴影路径。
假设你想了解很多其它相关信息,Mark Pospesel写了一篇非常棒的文章:shadowPath。
Table views须要高速的滚动——假设不能的话。用户会感觉到停顿。
为了让table view平滑的滚动。确保遵循了例如以下建议:
- 设置正确的reuseIdentifer以重用cell。
- 尽量将view设置为不透明。包括cell本身。
- 避免渐变,图像缩放以及离屏绘制。
- 假设row的高度不相同,那么将其缓存下来。
- 假设cell显示的内容来此网络。那么确保这些内容是通过异步来获取的。
- 使用shadowPath来设置阴影。
- 降低subview的数量。
- 在cellForRowAtIndexPath:中尽量做更少的操作。
假设须要做一些处理,那么最好做过一次之后,就将结果缓存起来。
- 使用适当的数据结构来保存须要的信息。不同的结构会带来不同的操作代价。
-
使用rowHeight, sectionFooterHeight 和 sectionHeaderHeight 来设置一个恒定 高度。而不要从delegate中获取。
当须要存储和读取大量的数据时,该怎样选择存储方式呢?
有例如以下选择:
- 使用NSUserDefaults进行存储
- 保存为XML。JSON或Plist格式的文件
- 利用NSCoding进行归档
- 存储到一个本地数据库,比如SQLite。
- 使用Core Data.
使用NSUserDefaults有什么问题呢? 尽管NSUserDefaults非常好而且easy。只是仅仅仅仅针对于存储小量数据(比方你的级别,或者声音是开或关)。假设要存储大量的数据。最好选择别的存储方式。
大量数据保存为结构化的文件也可能会带来问题。一般,在解析这些结构数据之前,须要将内容全部载入到内存中。这是非常消耗资源的。尽管能够使用SAX来处理XML文件,可是这有点复杂。
另外,载入到内存中的全部对象,不一定全部都须要用到。
那么使用NSCoding来保存大量数据怎么样呢?由于它相同是对文件进行读写,因此依旧存在上面说的问题。
要保存大量的数据,最好使用SQLite或Core Data。通过SQLite或Core Data能够进行详细的查询——仅仅须要获取并载入须要的数据对象——避免对数据进行不合理的搜索。在性能方面。SQLite和Core Data差不大。
SQLite和Core Data最大的差别实际上就是使用方法上。Core Data代表一个对象模型,而SQLite仅仅是一个DBMS。一般,苹果建议使用Core Data,只是假设你有特殊的原因不能使用Core Data的话,能够使用低级别的SQLite。
在程序中,假设选择使用SQLite。这里有个方便的库FMDB :能够利用该库操作SQLite数据库,而不用深入使用SQLite C API。