iOS性能优化方方面面

参考文章:

iOS内存、缓存及存储优化

懒加载

视图控制对象通过alloc和init来创建,但是视图控制对象不会在创建的那一刻就马上创建相应的视图,而是等到需要使用的时候才通过调用loadView来创建,这样的做法能提高内存的使用率。比如,当某个标签有很多UIViewController对象,那么对于任何一个UIViewController对象的视图,只有相应的标签被选中时才会被创建出来。针对于此,正确的使用懒加载技术,将对象创建延迟并在多处复用可以很好的减少内存的消耗。

更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中,避免了不划算的内存分配。

复用

在一个面向对象的语言中,数据的抽象化、继承、封装和多态性等特性使得一个系统可以在更高的层次上提供复用性。抽象化和继承关系使得概念和定义可以复用,多态性使得实现和应用可以复用,抽象化和封装可以保持和促进系统的可维护性。今天我们主要关注的是在内存上对对象的复用,以常见的UITableView为例。

列表中一个很重要的内存优化手段就是对UITableViewCells, UICollectionViewCells和UITableViewHeaderFooterViews设置正确的reuseIdentifier以在别处复用,即所谓的cell复用机制。为了性能最优化,table view用tableView:cellForRowAtIndexPath:为rows分配cells的时候,它的数据应该重用自UITableViewCell,table view维持一个队列可重用的UITableViewCell对象。不使用reuseIdentifier的话,每显示一行table view就不得不设置全新的cell,这对性能的影响相当大。使用方法就是在一个table view中添加一个新的cell时在data source object中添加这个方法:

static NSString *CellIdentifier = @"CellID";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

在必要时使用先前注册的nib或者class创造新的cell。如果没有可重用的cell,你也没有注册一个class或者nib的话,这个方法返回nil,正常使用时需要对nil进行处理。复用逻辑是假如tableview中有10个cell,窗口只容得下前4个,每个cell都是一样的,复用id也一样。从初始位置开始把cell向上滑动一点点,此时第一个cell的一部分消失了,第五个cell露出了一部分,这时第一个cell并没有进入到复用池,池子是空的,第五个cell自然也就不能在复用池中找到可复用的cell,当第五个cell完全显示出来,第一个cell也已经完全退出了窗口,这时第一个cell被放入到复用池。我们继续向上滑动,第六个cell将显示出来,因为第一个cell已经在复用池中了,第六个cell可以复用第一个cell,而不需重新创建对象。

另外,Apple官方建议不要在scrollview中嵌套scrollview,tableview也是scrollview的一种,不到万不得已时不要将它嵌到scrollview中。

NSCache

NSCache是苹果官方提供的缓存类,它的用法与NSMutableDictionary的用法很相似,在AFNetworking中,使用它来作为图片缓存。在AFNetworking的UIKit中,使用了NSCache来提供异步图片下载的缓存。NSCache是线程安全的,在多线程操作中,不需要对Cache加锁。NSCache的Key只是对对象的strong引用,对象不需要实现NSCopying协议,NSCache也不会像NSDictionary一样复制对象。

NSData *data;
NSString *key;

NSCache *cache = [[NSCache alloc] init]; //创建

[cache setObject:data forKey:key]; //保存

[cache setObject:data forKey:key cost:50];//cost用于计算记录在缓冲中所有对象的总成本。当出现内存警告,或者超出缓存的成本上限时,缓存会开启一个回收过程,删除部分元素。

cache.totalCostLimit = 100;//缓存空间的最大成本,超出上限会自动回收对象。默认值是0没有限制
cache.countLimit = 100;//能够缓存对象的最大数量,默认值也是0(默认没有限制)
cache.evictsObjectsWithDiscardedContent = YES;//标示是否回收废弃的内容,默认值是YES(自动回收)
NSData *cacheData = [cache objectForKey:key]; //获取

[cache removeObjectForKey:key];
[cache removeAllObjects];

NSCache在系统发出低内存通知时,会自动删减缓存。NSCache可以设置数量限制,通过countLimit与 totalCostLimit来限制cache的数量或者限制cost。当缓存的数量超过countLimit,或者cost之和超过totalCostLimit,NSCache会自动释放部分缓存。countLimit并不是一个严格的限制,如果cache数量超出了limit,那么cache中的对象有可能立刻被清理出去,或者稍后,或者永远都不会被清理掉,而这个时机依赖于cache的实现细节。在使用setObject:forKey:cost:方法时,cost值只在比较容易获取到的时候才指定,若要通过复杂的计算来获取cost值,那使用缓存的意义就不大了。

通常,使用NSCache会结合NSDiscardableContent协议,实现了这个协议的类需要在被引用之前,必须调用beginContentAccess来标记为可使用的,如果在使用之前没有调用beiginContentAccess,那么就会抛出异常。在使用结束之后,调用endContentAccess,来标记它为可以被释放的。如果实现了NSDiscardableContent协议的对象放入了NSCache中,那么,在清除它的时候,会调用discardContentIfPossible方法来判断引用状况,没有引用,则销毁。

NSCacheDelegate中,-cache: willEvictObject: 方法,缓存将要删除对象时调用,不能在此方法中修改缓存。仅仅用于后台的打印,以便于程序员的测试。

@protocol NSCacheDelegate <NSObject>
@optional
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end

两种图片加载方式

常见的从bundle中加载图片的方式有两种,一个是用imageNamed,二是用imageWithContentsOfFile,imageNamed的优点是当加载时会缓存图片,这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话,如果缓存中没有找到相应的图片,这个方法从指定的文档中加载然后缓存并返回这个对象。而imageWithContentsOfFile仅加载图片。

如果你要加载一个大图片而且是一次性使用,那么就没必要缓存这个图片,用imageWithContentsOfFile足矣,这样不会浪费内存来缓存它。然而,在图片反复重用的情况下imageNamed是一个好得多的选择。

iOS存储优化

当存储大块数据时会有很多选择,比如:使用NSUerDefaults;使用XML, JSON, 或者 plist;使用NSCoding存档;使用类似SQLite的本地SQL数据库或者使用 Core Data。NSUserDefaults的问题是虽然它很便捷,但是它只适用于小数据,比如一些简单的布尔型的设置选项,再大点你就要考虑其它方式了。XML需要读取整个文件到内存里去解析,这样很不经济,而且使用SAX有些麻烦。NSCoding也需要读写文件,所以也有以上问题。在这种应用场景下,使用SQLite 或者 Core Data比较好,使用这些技术用特定的查询语句就能只加载你需要的对象。

在性能层面来讲,SQLite和Core Data是很相似的。他们的不同在于具体使用方法。Core Data代表一个对象的graph model(图表模型),但SQLite就是一个DBMS,也更加底层。使用SQLite,可以用FMDB这个库来简化SQLite的操作,节省了了解SQLite的C API的时间。

针对sqlite处理时,缓存经常用到的 sqlite 语句,优化数据库查询语句,用sqlite3_trace和sqlite3_profile来查找性能差的语句。如果可能的话,缓存查询结果。使用 sqlite_prepare将SQL查询编译成字节码时,要使用bind,重用那些已经prepared的语句。因为SQLite的SQL文本支持变量绑定,以便减少SQL语句被动态解析的次数,从而提高数据查询和数据操作的效率。sqlite3_prepare_v2的执行效率往往要低于
sqlite3_step的效率。看起来是不是很复杂,这也是我们利用FMDB这类库的原因。

网络请求优化

当前的应用更多的是一种前端框架,内容依赖于远端资源或是第三方API,在开发时难免需要从远端下载XML、JSON、HTML或者其它格式的内容,此时便对网络有了很大的依赖。但是,国内现在的网络是不稳定的,2G、3G、4G网络并存。不论什么场景,肯定不想让你的用户等太长时间。减小文档的一个方式就是在服务端和你的app中打开gzip,这对于文字这种能有更高压缩率的数据来说会有更显著的效用。iOS已经在NSURLConnection中默认支持了gzip压缩,当然AFNetworking这些基于它的框架亦然。像Google App Engine这些云服务提供者也已经支持了压缩输出。

Web浏览器对请求压缩的支持并不太好,因为浏览器不知道目标服务器是否能够支持对请求的解压缩。如果服务器无法理解压缩模式,那么请求就会被丢弃,客户端应用将无法得到响应。与响应压缩一样,客户端不应改将CPU时间浪费在压缩如PDF、加密数据、图像、音频及视频等已经压缩的内容上。使用Base64预先压缩数据是个很好的方法,比如以Base64格式上传JPEG文件,那么可以对Base64数据进行压缩,相较于未压缩的Base64数据,压缩后的数据体积会降低30%左右。

从app和网络服务间传输数据有很多方案,最常见的就是JSON和XML,你需要选择对你的app来说最合适的一个。压缩模式的效率在很大程序上取决于待压缩的数据,不过通常情况下JSON都是一种更为高效的模式。解析JSON会比XML更快一些,JSON也通常更小更便于传输。但是XML也有XML的好处,比如使用SAX来解析XML就像解析本地文件一样,你不需像解析json一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会极大地减低内存消耗和增加性能。

降低请求延迟有两项最佳实践:在单个TCP连接上发送HTTP请求,以管道的形式发送HTTP请求,从而优化全双工TCP连接的使用。Apache和IIS都支持管道,无需任何额外的配置。通过HTTP缓存机制的基本原理,在iOS应用中利用这些规则,可以在本地缓存内容以避免不必要的网络流量。如我们上面提到的NSURLCache、NSCache等。

另外,优化网络请求的一环是DNS解析,因为客户端app的请求第一步都是DNS解析(如果用域名访问的话),但由于cache的存在使得大部分的解析请求并不会产生任何延迟。各平台都有自己的cache过期策略,iOS系统一般是24小时之后会过期,还有进入飞行模式再切回来、开关机、重置网络设置等也会导致DNS cache的清除。所以一般情况下用户在第二天打开你的app都会经历一次完整的DNS解析请求,网络情况差的时候会明显增加应用请求的总耗时。如果能直接跳过DNS解析这一步,当然能提升网络性能了。一种方法是使用DNS映射,另外就是直接用ip请求数据。DNS解析请求简单来说,就是输入一个域名,输出一个ip地址。做自己的映射机制也就是客户端本地维护这样一个映射文件,只不过这个映射文件需要能从服务器更新,还要做一些容错处理。

在服务器端和客户端使用相同的数据结构很重要,在内存中操作数据使它们满足你的数据结构是开销很大的。比如用数据来展示一个table view,最好直接从服务器取array结构的数据以避免额外的中间数据结构改变。类似的,如果需要从特定key中取数据,那么就使用键值对的dictionary。

UIWebView

对于UIWebView,用它来展示网页内容或者创建UIKit很难做到的动画效果是很简单的一件事。但是,由于Webkit的Nitro Engine的限制,UIWebView并不像想象的那么快。尽可能移除不必要的JavaScript,避免使用过大的框架,能只用原生js就更好了。另外,尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。
UIWebView的缓存策略可通过NSURLRequest来设置后,进行loadRequest.

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

推荐阅读更多精彩内容

  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,144评论 1 23
  • iOS系统会将内存分为固定(Wired)、活跃(actived)、空闲(inactive)、自由(free)四个类...
    雨润听潮阅读 513评论 0 3
  • 在开发iOS应用程序时,让程序具有良好的性能是非常关键的。这也是用户所期望的,如果你的程序运行迟钝或缓慢,会招致用...
    imkakaxi阅读 968评论 0 0
  • 那个背影 如诗如画如你 摇曳动人 那声呼...
    Distes凌霄阅读 71评论 0 2
  • 很多人都明白了:生下来活下去,生活不容易。 人活一回,别总想着为谁活,谁又能为你活? 好好爱惜自己,别再累死累活!...
    沐语心理阅读 1,835评论 2 99