iOS优化


iOS系统会将内存分为固定(Wired)、活跃(actived)、空闲(inactive)、自由(free)四个类型,并且根据需要可以相互转化。ARC是目前主流的内存管理方式,避免了忘记释放内存而导致的内存泄露,自动管理retain和release过程。

懒加载

视图控制对象通过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 = @"RCCellID";

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中。

一些objects的初始化很慢,比如NSDateFormatter和NSCalendar,又不可避免地需要使用它们,比如从JSON或者XML中解析数据。想要避免使用这个对象的瓶颈就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。使用静态变量,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似,这个需要注意。

选择正确的数据结构

数据结构是计算机存储、组织数据的方式,是相互之间存在一种或多种特定关系的数据元素的集合。数据结构分为逻辑结构、存储结构(物理结构)和数据的运算。数据的逻辑结构是从具体问题抽象出来的数学模型,是描述数据元素及其关系的数学特性的,有时也把逻辑结构简称为数据结构。根据数据元素间关系的不同特性,通常有下列四类基本的结构:集合结构,该结构的数据元素间的关系是“属于同一个集合”;线性结构,该结构的数据元素之间存在着一对一的关系;树型结构,该结构的数据元素之间存在着一对多的关系;图形结构,该结构的数据元素之间存在着多对多的关系,也称网状结构。 一个数据结构有两个要素,一个是数据元素的集合,另一个是关系的集合。在形式上,数据结构通常可以采用一个二元组来表示。

通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。选择对业务场景最合适的类或者对象是写出能效高的代码的基础,处理数据集时这句话尤其正确。一些常见数据集包括:

Arrays: 有序的一组值。使用index查询很快,使用value查表很慢,插入/删除很慢。

Dictionaries: 存储键值对,用键来查找比较快。

Sets: 无序的一组值,用值来查找很快,插入/删除很快。

由于此处会在后续的算法一章中着重的分析,这里就不做过多了讲述了。

后台处理

在后台时,为了减少程序占用的内存,系统会自动在回收一些系统帮助你开辟的内存。比如:系统回收Core Animation的后备存储;去掉任何系统引用的缓存图片;去掉系统管理数据缓存强引用。除此之外,系统进入 background 之后,一般只有10分钟的运行时间,因此有很多值得注意的事项:

尽量减少内存的使用。当内存不足时,iOS将kill那些消耗内存最多的 App。

释放所有的共享资源,比如 Calendar 与 Address book。当应用程序进入后台时,如果它还在使用或没有释放共享资源,iOS会立即kill掉该应用程序。

对于图片对象、可以重新加载的大的视频或数据文件和任何没用而且可以轻易创建的对象应该尽快的去掉强引用。

正确处理App生命周期事件。当进入后台时,应该保持应用程序数据,以便回到前台时能够恢复。当进入 inactive 状态时,应该暂停当前的业务流。iOS运行App在后台运行的时间有限,因此后台代码不应该执行非常耗时的任务,可能的话就使用多线程。当进入后台时,iOS会保存当前App的一个快照,以便之后在合适的时候(装载view和数据时)呈现给用户以提高用户体验,因此在进入后台时,应该避免在屏幕上呈现用户信息,以免泄露用户个人资料。

不要更新UI或者执行大量消耗CPU或电池的代码。进入后台之后,不应该执行不必要的任务,不要执行 OpenGL ES 调用,应取消 Bonjour 相关的服务,正确处理网络链接失败,避免更新 UI,清除所有的警告或其他弹出对话框。

在后台时正确响应系统变化。 如:

设备旋转消息 UIDeviceOrientationDidChangeNotification

重要的时间变化(新的一天开始或时区变化)UIApplicationSignificantTimeChangeNotification

电池变化 UIDeviceBatteryLevelDidChangeNotification 和 UIDeviceBatteryStateDidChangeNotification

用户默认设置变化 NSUserDefaultsDidChangeNotification

本地化语言变化 NSCurrentLocaleDidChangeNotification 等。

保存用户数据或状态信息,所有没写到磁盘的文件或信息,在进入后台时,最好都写到磁盘去,因为程序可能在后台被杀死。

applicationDidEnterBackgound: 方法有大概5秒的时间让你完成自己的任务。如果超过时间还有未完成的任务,你的程序就会被终止而且从内存中清除。如果还需要长时间的运行任务,可以调用 beginBackgroundTaskWithExpirationHandler方法去请求后台运行时间和启动线程来运行长时间运行的任务。

[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{

// 如果在系统规定时间内任务还没有完成,在时间到之前会调用到这个方法,一般是10分钟

}];

两种图片加载方式

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

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

CPU缓存解析

cache里的基本存储单元是cacheline即缓存行,缓存通常分为一级缓存和二级缓存,有些还有三级缓存,通常数据传递路线是硬盘到内存到二级缓存到一级缓存再到cpu寄存器。cpu读取数据时,首先会先从一级缓存那里读取,如果该数据存在的话(即cache命中),直接取出数据,这里并没有访问内存,如果数据不在一级缓存,cpu就会到二级缓存那里寻找,同样,如果存在则直接取出数据,如果不存在,那就得从内存载入该数据,这时就需要访问一次内存,缓存就是为了提高cpu的工作效率。

iOS存储优化

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

Core Data

Core Date是iOS 3.0后引入的数据持久化解决方案,是对SQLite的封装,提供了更高级的持久化方式。Core Data是完全独立于任何UI层级的框架,它是作为模型层框架被设计出来的。在对数据库操作时,不需要使用sql语句,是一种ORM(“对象关系映射”)的数据库操作方式,ORM将关系数据库中的表,转化为程序中的对象,但实际上是对数据中的数据进行操作,按照面向对象的思想,使用实体模型来操作数据库。使用Core Data进行数据库存取不需要手动创建数据库,创建数据库的过程完全由Core Data框架自动完成,开发者需要做的就是把模型创建起来,Core Data实际上是将数据库的创建、表的创建、对象和表的转换等操作封装起来,以简化我们的操作。如果模型发生了变化,可以选择重新生成实体类文件,但是自动生成的数据库并不会自动更新,需要考虑重新生成数据库,并把之前数据库中数据进行移植。Core Data能够简化操作,但是它不支持跨平台使用,如果想实现跨平台,就需要使用SQLite来进行数据持久化。

App升级之后数据库字段或者表有更改会导致crash,Core Data的版本管理和数据迁移变得非常有用,比手动写sql语句操作要简单一些。另外,Core Data不光能操纵SQLite,Core Data和iCloud的结合也很好,如果有这方面需求的话可以优先考虑Core Data。在写入性能上,因为是使用sqlite格式作为磁盘存储格式,因此其性能是一样的。查询性能core data因为要兼容多种后端格式,因此查询时,其可用的语句比直接使用sqlite少,因此有些fetch实际上不是在sqlite中执行的。在内存不是很紧张时,直接fetch一个entity的所有数据然后在内存中做filter往往比使用predicate在fetch时过滤更快。Core Data还有其他sql所不具备的优点,如对undo的支持,多个context实现sketchbook类似的功能,为ManagedObject优化的row cash等。Core Data是支持多线程的,但需要thread confinement的方式实现,使用了多线程之后可以最大化的防止阻塞主线程。

从可操控行上来说,Core Data并不是一个很好的选择,虽然它简化的我们的数据处理操作,同时也屏蔽了处理数据的SQL语句,但其自动生成的SQL语句并不是最高效的,这不是Core Data自有的问题,所有ORM类型的处理方式都会存在这样的情况。Core Data在多人合作开发的时候,管理Core Data的模型需要很小心,尤其是合并的时候。

CoreData多线程安全

CoreData的NSPersistentStoreCoordinator和NSManagedObjectContext对象都是不能跨线程使用的,但NSManagedObjectContext已经对跨线程提供了内置的支持,在创建NSManagedObject的时候有个构造器参数initWithConcurrencyType,合理设置便可解决,它有三个方式:

NSConfinementConcurrencyType ( iOS 9 后已经取消)

NSMainQueueConcurrencyType (表示只会在主线程中执行)

NSPrivateQueueConcurrencyType (表示可以在子线程中执行)

搭建多线程 Core Data 环境的方案一般就是创建一个 NSMainQueueConcurrencyType 的 context 用于响应 UI 事件,其他涉及大量数据操作可能会阻塞 UI 的,就使用 NSPrivateQueueConcurrencyType 的 context。

let mainContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)

let backgroundContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)

NSManagedObjectContext是可以基于其他的 NSManagedObjectContext的,通过 setParentContext 方法,可以设置另外一个 NSManagedObjectContext 为自己的父级,这个时候子级可以访问父级下所有的对象,而且子级 NSManagedObjectContext 的内容变化后,如果执行save方法,会自动的 merge 到父级 NSManagedObjectContext 中,也就是子级save后,变动会同步到父级 NSManagedObjectContext。当然这个时候父级也必须再save一次,如果父级没有父级了,那么就会直接向NSPersistentStoreCoordinator中写入,如果有就会接着向再上一层的父级冒泡。

通过三个级别的 NSManagedObjectContext, 一个负责在background更新NSPersistentStoreCoordinator。一个用在主线程,主要执行插入,修改和删除操作,一些小的查询也可以在这里同步执行,如果有大的查询,就起一个新的 NSPrivateQueueConcurrencyType 类型的 NSManagedObjectContext,然后放在后台去执行查询,查询完成后将结果返回主线程。

NSManagedObjectContext在后台线程执行是通过 performBlock 方法来实现的,在传入的匿名block中执行的代码就是在子线程中了。managed object context 并非线程安全的,你不能随便地开启一个后台线程访问 managed object context 进行数据操作就管这叫支持多线程了,而是在 private queue 的 context 中进行操作时,使用以下方法:

func performBlock(_ block: () -> Void)//在私有队列中异步地执行 Blcok

func performBlockAndWait(_ block: () -> Void)//在私有队列中执行 Block 直至操作结束才返回

在不同线程中使用 managed object context 时,不需要我们创建后台线程然后访问 managed object context 进行操作,而是交给 context 自身绑定的私有队列去处理,我们只需要在上述两个方法的 Block 中执行操作即可。也可以在其他线程中来使用 context,但是要保证以上两个方法。而且,在 NSMainQueueConcurrencyType 的 context 中也应该使用这种方法执行操作,这样可以确保 context 本身在主线程中进行操作。

如果是查询的话,因为 NSManagedObject 也不能跨线程访问,所以在block里获取到的NSManagedObject对象只能将objectid传到主线程,主线程再通过 objectWithID 恢复对象的方法。

网络请求优化

当前的应用更多的是一种前端框架,内容依赖于远端资源或是第三方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,用它来展示网页内容或者创建UIKit很难做到的动画效果是很简单的一件事。但是,由于Webkit的Nitro Engine的限制,UIWebView并不像想象的那么快。尽可能移除不必要的JavaScript,避免使用过大的框架,能只用原生js就更好了。另外,尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。


未完待续

你可能感兴趣的:(iOS优化)