昨天我们讲解了程序性能优化中的内存优化,今天继续跟随南昌网络公司小编学习如何优化程序性能。重点分析程序性能优化之优化资源文件,延迟加载和数据持久优化这三种方法,让你的程序“飞”起来。
从狭义上讲,资源文件是放置在应用程序本地与应用程序一起编译、打包和发布的非程序代码文件,如应用中用到的声音、视频、图片和文本文件等。从广义上讲,资源文件可以放置于任何地方,既可以放置于本地,也可以放在云服务器中。
在iOS中,本地资源文件编译后,会放置于应用程序包文件中(即<应用名>.app文件)。如下代码用于访问如图1所示team.plist本地资源文件:
图1 资源文件
图1所示的“球队图片”组也放置了一些资源文件。添加资源文件的方法是通过右键添加文件到工程中。资源文件在使用的过程中需要优化,包括文件格式、文件类型、文件大小和文件结构等方面,使得它更适合某个应用。“适合”这两个字很重要。当然,优化方向有很多,下面我们从图片文件优化和音频文件优化这两个方面介绍一下。
1.图片文件优化
图片文件优化包括文件格式和文件大小的优化。在移动设备中,支持的图片格式主要是PNG、GIF和JPEG格式,苹果推荐使用PNG格式。在Xcode中,集成了第三方PNG优化工具pngcrush①,它可以在编译的时候对PNG格式文件进行优化和压缩,而我们只需要设定如图2所示的编译参数Compress PNG Files为Yes就可以了。
图2 设定编译参数Compress PNG Files
打开“ ImageFile”工程中“测试图片”目录中background(未优化).png文件,在Finder中查看该文件的属性,它是一个320×480px、大小为317KB的PNG图片,如图3所示。
使用Xcode编译工程,在编译之后的目录中找到ImageFile.app包文件。打开包文件,查看目录中background(未优化).png文件的属性,可以发现该文件是205KB的PNG图片了,如图4所示。
图3 未优化的PNG文件属性
图4 优化的PNG文件属性
Xcode工具可以在编译时优化PNG图片,但是即便经过优化和压缩的PNG图片文件,也比JPEG图片文件大得多。开“ImageFile”工程中“测试图片”目录中的background-8(优化压缩).png文件和background-h.jpg文件,比较可以发现,前者是经过优化的质量最低的PNG-8(8位PNG格式)文件,其大小是61KB;后者是经过优化的质量最高的JPEG格式文件,其大小是22KB。在本例中,PNG比JPEG文件大3倍多。
如果是本地资源文件,这样的差别不是很大,但如果是分布在网络云服务器中的资源文件,应用在加载这些图片时,会从网络上下载到本地,这时候JPEG就很有优势了。
综上所述,如果在本地资源的情况下,我们应该优先使用PNG格式文件,如果资源来源于网络,最好采用JPEG格式文件。
另外,图片是一种很特殊的资源文件。创建UIImage对象时,可以使用静态工厂方法+imageNamed:和实例构造器-initWithContentsOfFile:。+imageNamed:方法会在内存中建立缓存,这些缓存直到应用停止才清除。如果是贯穿整个应用的图片(如图标、logo等),推荐使用+imageNamed:创建。如果是仅使用一次的图片,推荐使用实例构造器-initWithContentsOfFile:创建。
2.音频文件优化
在讨论音频文件优化之前,我们先讨论一下音频文件格式。在iOS平台中,主要的音频文件格式有以下4种。
WAV文件。WAV是一种由微软和IBM联合开发的用于音频数字存储的文件格式。WAV文件的格式灵活,
可以存储多种类型的音频数据。由于文件较大,不太适合于移动设备这些存储容量小的设备。
MP3(MPEG Audio Layer 3)文件。MP3利用MPEG Audio Layer 3技术,将数据以1∶10甚至1∶12的缩率压缩成容量较小的文件。MP3是一种有损压缩格式,它 尽可能地去掉人耳无法感觉的部分和不敏感的部分。这么高的压缩比率非常适合于移动设备这些存储容量小的设备,现在非常流行。
CAFF(Core Audio File Format)文件。CAFF是苹果开发的专门用于Mac OS X和iOS系统的无压缩音频格式,它被设计用来替换老的WAV格式。
AIFF(Audio Interchange File Format)文件。AIFF是苹果开发的专门用于Mac OS X系统的专业的音频文件格式。AIFF的压缩格式是AIFF-C(或AIFC),将数据以4∶1压缩率进行压缩,应用于Mac OS X和iOS系统。
音频文件优化包括了文件格式和文件大小的优化,但也要考虑到文件使用场景、采用的技术(OpenAL、AVAudioPlayer)等因素。在iOS应用中,使用本地音频资源文件的主要应用场景是背景音乐和音乐特效,下面我们从这两个方面介绍相关的优化技术。
(1)背景音乐优化
背景音乐会在应用中反复播放,它会一直驻留在内存中并耗费CPU,所以更合适比较小的文件,而压缩文件是不错的选择。压缩文件主要有AIFC和MP3两种格式,一般我们首选AIFC,因为这是苹果推荐的格式。但是我们获得的原始文件格式不一定是AIFC,这种情况下我们需要使用afconvert工具将其转换为AIFC格式。在终端中执行如下命令:
$ afconvert -f AIFC -d ima4 Fx08822_cast.wav
其中-f AIFC参数用于转换为AIFC格式,-d ima4参数指定解码方式,Fx08822_cast.wav是要转换的源文件。转换成功后,会在相同目录下生成Fx08822_cast.aifc文件。本例中的源文件Fx08822_cast.wav的大小是295KB,转换之后的Fx08822_cast.aifc文件的大小是82KB。当然,afconvert工具也可以转换MP3等其他压缩格式文件。如果我们同时有WAV文件,就应该优先采用WAV文件。MP3本身是有损压缩,如果再经过afconvert转换,音频的质量会受到影响。
(2)音乐特效优化
音乐特效用于很多游戏中,如发射子弹、敌人被打死或按钮点击等发出的声音,这些声音都是比较短的。如果追求震撼的3D效果,可以采用苹果专用的无压缩CAFF格式文件,其他格式的文件尽量不要考虑。一般不要使用压缩音频文件,这主要是因为音乐特效通常采用OpenAL技术,它只接受无压缩的音频文件。另外,压缩音频文件都会造成音质的丢失。如果我们没有CAFF格式的文件,也可以使用afconvert工具将其转换为CAFF格式。在终端中执行如下命令:
$ afconvert -f caff -d LEI16 Fx08822_cast.wav
其中-f caff参数用于转换为CAFF格式,-d LEI16参数指定解码方式,Fx08822_cast.wav是要转换的源文件。默认音频的采样频率为22050Hz,如果想提高音频采样频率,可以通过如下命令:
$ afconvert -f caff -d LEI16@44100 Fx08822_cast.wav
其中-d LEI16@44100参数中的44100表示音频采用频率44100Hz。
如果我们采用的资源文件不在本地,而是在分布在网络云服务器中,那么情况就另当别论了。应用在加载这些音频文件时,带宽往往是要考虑的问题,减小文件大小胜过对音质的要求,这种情况下MP3格式是非常适合的。
综上所述,音频文件在使用本地资源的情况下,应用于背景音乐时AIFC格式是首选,应用于音乐特效时CAFF格式是首选。如果是资源来源于网络,最好采用MP3格式文件。
3.延迟加载
延迟加载(lazy load)指一些对象不是在应用和视图等初始化时创建,而是在用到它的时候创建。当应用中有一些对象并不经常使用时,延迟加载可以提高程序性能。
3.1 资源文件的延迟加载
首先,我们要考虑的就是对资源文件的延迟加载。由于资源文件的访问涉及IO操作,这本身就会耗费一定的CPU时间,如果文件比较大而且加载时机又不合适,就会造成内存浪费。前面我们了解到资源文件包括图片、音频和文本文件等,无论是什么类型的文件,有些情况下采用延迟加载是很有必要的。例如,我们有如图5所示的需求,可以使用分屏件(UIPageControl)左右滑动屏幕来浏览这3张图片。
5 图片延迟加载实例
PageControlNavigation实例是没有采用延迟加载的实现代码,其中的ViewController代码如下:
我们是在viewDidLoad方法中一次加载全部3张图片,但是有的时候用户不一定会浏览后面的图片,他可能只看到第一张或第二张,后面的第三张没有去看,此时后面的两张图片仍然加载内存的话,会造成内存浪费。采用延迟加载实现时(见实例LazyLoadPageControlNavigation),ViewController的代码如下:
我们重新修改了这个实例,在viewDidLoad方法中只加载第一张图片,见第①行代码。如果用户滑动屏幕或点击分屏控件进入第二个屏幕,则调用loadImage:方法加载第二张图片,类似地,如果要进入第三个屏幕,则调用loadImage:方法加载第三张图片。
在这两种实现方式中,LazyLoadPageControlNavigation实现了延迟加载。很显然,LazyLoadPageControlNavigation的延迟加载友好很多。那么,两者究竟有多大的差别,这是可以量化的。通过Instruments工具的Allocations模板,可以分析ViewController视图控制器加载时内存占用方面的差别。图6是无延迟加载实现案例的Allocations模板跟踪,图7是采用延迟加载实现案例的Allocations模板跟踪。
如图6所示,界面启动用时,内存占用马上达到8.12MB。如图7所示,界面启动用时,内存占用3.08MB,当我们滑动到第二和第三屏幕时,内存占用达到8.06MB,内存变化会有明显的两个阶梯。
图6 无延迟加载实现案例的Allocations模板跟踪
图7 使用延迟加载实现案例的Allocations模板跟踪
在上面的案例中可以发现,延迟加载的优势很明显。如果一定会访问到资源文件,则延迟加载这些资源文件时,内存占用方面就没有优势了,但是在界面加载速度方面还是有优势的。
3.2 故事板文件的延迟加载
xib和故事板也都属于资源文件,它是非常特殊的资源文件,应用不仅需要读取它,而且要根据里面描述的信息创建视图和子视图,以及它们的视图控制器等对象。创建这么多对象会耗费很多时间,占用很多内存,因此,它们的延迟加载问题非常重要。
默认情况下,创建基于故事板的应用时,只有一个故事板文件。这种情况下,故事板内部的视图控制器的创建和加载都是由Segue来控制的,Segue会帮助我们管理好这些控制器,包括延迟加载等问题。
我们创建一个实用型应用程序①,研究故事板的延迟加载机理。实用型应用一般会有两个视图:主视图,它显示应用的主要功能;子视图,它用来对应用进行一些设置。我们自己创建一个实用型应用,如图8所示。
图8 实用型应用
在Xcode 5之前可以使用Utility Application模板创建,Xcode 6之后就没有这个模板了,我们可以通过SingleView Application模板创建StoryboardLazyLoadDemo工程。
在主视图中点击 按钮,MainViewController会延迟加载FlipsideViewController,然后弹出模态模式。使用模态Segue连接MainViewController和FlipsideViewController,如图9所示,我们基本上不需要编写什么代码。
图9 模态视图的Segue
Segue定义了两个视图控制器的导航关系,也用来维护和管理下一个视图控制器的延迟加载时机,这种情况下我们无法“插手”视图控制器的延迟加载。但是一种情况除外,那就是使用了故事板,而控制器之间没有定义导航关系,没有定义Segue,如图10所示。
图10 没有定义Segue的故事板
这种情况下,添加showInfo:方法响应主视图的 按钮点击事件,具体可参考StoryboardLazyLoadNoSegueDemo工程的MainViewController,相关代码如下:
在单一故事板文件中,第①行代码可以获得当前的故事板对象。如果想在多故事板的情况下获得非当前故事板对象,可以通过第②行代码的UIStoryboard构造器创建。本例中不用使用该语句,使用它会多创建一个故事板对象,就会占用更多的内存。
3.3 xib 文件的延迟加载
相对于故事板而言,xib要灵活很多。xib文件有两种:一种是描述视图控制器的,另一种是描述视图的,它们的加载方式有所区别。无论是哪一种,分散管理的xib文件使我们通过编程方式访问它更加方便。
Xcode 6不能创建基于xib文件的工程了,我们通过Single View Application模板创建NibLazyLoadDemo工程,然后删除主故事板文件。接着我们创建视图控制器,如图11所示,一定要选择Also create XIB file复选框,这会帮助我们创建与视图控制器对应的xib文件。
图11 创建视图控制器
创建好视图控制器后,我们需要修改AppDelegate使应用启动时能够加载MainViewController。AppDelegate的主要代码如下:
上述代码中,第①~③行是我们添加的代码,第①行代码用于创建Window对象,在xib构建的视图中必须放到一个Window中,第②行代码通过xib文件创建视图控制器对象,然后再把视图控制器添加到Window对象中。主视图控制器MainViewController中showInfo:方法的代码如下:
本例中的xib文件是视图控制器xib文件,我们可以使用视图控制器的initWithNibName:bundle:构造器从xib文件中创建视图控制器对象。
有些情况下,故事板和xib会混合使用。在有故事板的工程中,有时候需要使用别人已经编写好的xib文件和对应类(视图或视图控制器)。当然,通过上面的两种方式也是可以的。
4.数据持久化的优化
在iOS中,数据持久化的载体主要有文件、SQLite数据库和Core Data。本节中,我们就从这几个方面入手讨论数据持久化的优化问题。
4.1 使用文件
文件是数据持久化的重要载体。文件优化可以包括很多方面,下面我们从文件访问、文件结构和文件大小这3个方面来介绍。
(1)文件访问优化
避免多次写入很少的数据,最好是当数据积攒到一定数量时一次写入。因为文件访问涉及IO操作,我们知道频繁的IO操作会影响性能,所以最好将文件读写访问从主线程中剥离出来,由一个子线程负责。另外,过于频繁地写入数据会影响设备中闪存的寿命。
文件的写入应该采用增量方式,每次只写入变化的部分,不要为改变几个字节写入整个文件。这样就要求不能采用简单的属性列表对象写入方式。这是一个很复杂的问题,文件内容的变化可以是追加、删除和修改。文件追加很容易实现,删除就比较麻烦了,需要找到要删除的数据,这样访问文件就采用随机访问方式了。修改与删除的问题是一样的。与其这么麻烦,不如采用别的持久化技术了。
(2)文件结构优化
文件要保存数据,它就应该是结构化的。苹果中的.plist文件就是很好的结构化文件,其结构是层次模型的树形结构,层次的深浅会影响读取/写入的速度。在能够满足用户需求的情况下,要减少层次深度。下面是一个世界杯足球赛部分小组信息的属性列表文件team(5层次).plist:
该文件有5个层次,具体如图12所示,其中第一层是数组类型集合;第二层是字典集合,其中描述了小组名和小组中的球队列表;第三层是数组类型集合,描述了小组中的球队列表;第四层是字典集合;第五层是字符串,描述了球队名和球队图标信息。
图12 5个层次的team.plist文件
这个文件访问起来很不方便,遍历起来也很不方便,也很影响性能。我们重新设计了这个属性列表文件,其内容如下:
此时这个文件有3个层次,其中第一层是数组类型集合,第二层是字典集合,第三层是字符串,描述了球队名和球队图标信息,如图13所示。
图13 3层次的team.plist文件
与上面的5层次文件相比,3层次访问起来比较方便,性能会比较好。此外,在文件大小方面,3层次文件是647KB,5层次文件是893KB。
(3)文件大小优化
文件大小也是优化的一个重要指标。从上面的比较可以看到,调整文件结构可以减少文件大小。此外,我们也可以通过序列化.plist文件减少文件大小。 Foundation框架提供了NSPropertyListSerialization类,它就是为此而设计的。NSPropertyListSerialization类中有2个常用方法,具体如下所示。
+ dataWithPropertyList:format:options:error:。按照指定的格式和操作参数,序列化属性列表对象到NSData对象。
+ propertyListWithData:options:format:error:。按照指定的格式和操作参数,从NSData对象反序列化到属性列表对象中。
为了介绍NSPropertyListSerialization类,现在我们换成序列化二进制文件NotesList.binary。下面我们修改数据持久层工程PersistenceLayer中的NoteDAO类,首先,添加如下两个方法:
在上述代码中,readFromArray:方法从文件中读取数据到NSMutableArray,其流程是读取文件到NSMutableData对象,然后再从NSMutableData对象中反序列化处理属性列表对象。本例中的属性列表对象是NSMutableArray类型,其中第①行代码用于处理这一过程。propertyListWithData后面的参数是反序列化的数据来源,它是NSData类型。options后面的参数是NSPropertyListReadOptions。在Swift版本中,使用表达式NSPropertyListReadOptions-(NSPropertyListMutabilityOptions.MutableContainers.rawValue),Objective-C版本是NSPropertyListMutableContainersAndLeaves。NSPropertyListMutabilityOptions是枚举类型,其成员值如下。
Immutable。属性列表包含不可变对象。Objective-C版本为NSPropertyListImmutable。
MutableContainers。属性列表父节点是可变类型,子节点是不可变类型。Objective-C版本为NSPropertyListMutableContainers。
MutableContainersAndLeaves。属性列表父节点和子节点都是可变类型。Objective-C版本为NSPropertyListMutableContainersAndLeaves。
另外,在第①行代码中,format参数为nil(或NULL),说明格式是自动识别的。
南昌网络公司小编提示:属性列表对象是与属性列表文件结构对应的,它可以是NSData、NSString、NSArray和NSDictionary类型以及它们的可变类型。此外,还可以是NSDate和NSNumber类型。
write:toFilePath:方法把NSMutableArray数据序列化后写入到文件中,流程是先序列化NSMutableArray数据到 NSData 对象中,然后在把 NSData 对象写入到文件中。第②行代码就是完成序列化处理的,+dataWithPropertyList:format:options:error:方法中array参数是要序列化的属性列表对象,format参数是NSPropertyListFormat枚举类型。NSPropertyListFormat枚举类型包含的常量有如下几个。
XMLFormat_v1_0。指定属性列表文件格式是XML格式,仍然是纯文本类型,不会压缩文件。Objective-C版本为NSPropertyListXMLFormat_v1_0。
BinaryFormat_v1_0。指定属性列表文件格式为二进制格式,文件是二进制类型,会压缩文件。Objective-C版本为NSPropertyListBinaryFormat_v1_0。
OpenStepFormat。指定属性列表文件格式为ASCII码格式,对于旧格式的属性列表文件,不支持写入操作。
Objective-C版本为NSPropertyListOpenStepFormat。
本例中,我们设置的是BinaryFormat_v1_0,大小减少了,加载速度提高了,这样就达到了优化的效果。
4.2 使用 SQLite 数据库
当需要处理较大的数据集合时,就不能采用文件了。因为文件不支持事务处理,这时候我们可以选择SQLite数据库或Core Data。本节中,我们先从表结构、查询和插入(或删除)这几个方面介绍一下SQLite数据库方面的优化。
(1)表结构优化
SQLite是嵌入式关系型数据,它可以建立多表之间复杂的关系,但是如果放在iOS、Android等这些移动设备上时,我们需要考虑设备上本地表能建多少,表中字段有多少,表之间关系的复杂程度等问题。
在CPU处理能力低、内存少、存储空间少的情况下,我们不能在本地建立复杂表关系,表的个数不要超过5个,表中的字段数也不宜太多。移动设备中的数据不可能是企业级系统数据的全部,它只是企业级系统的补充和扩展。例如,在你的iPhone手机中,不可能有全部的新浪微博用户信息,一方面是不安全,另一方面是数据量很大,最高配置的iPhone也不可能存放下这么多数据。这是我们在开发移动应用时始终要牢记的:移动设备在整个应用系统中的角色是什么?
(2)查询优化
查询是衡量数据库性能的重要指标之一。在查询方面可优化的有很多,例如建立索引、限制返回记录数和where条件子句等。
使用索引,能够提高查询的性能。具体哪些字段需要创建索引,这很关键。只有在表连接或where条件子句中使用字段时,才能提高查询性能。在INTEGER PRIMARY KEY字段上,一般不用建索引。如果表中的数据很少,则建索引的效果不大。
由于移动设备屏幕相对来说比较小,屏幕上能显示的数据不多,如果一次查询出的记录数超过屏幕能显示的行数,这就没有必要了,因为这样反而会占用更多的内存,耗费宝贵的CPU时间。因此,我们需要为查询添加返回记录数的限制。下面的语句是SQLite支持的写法:
SELECT * FROM Note Limit 10 Offset 5;
以上语句表示从Note表查询数据出来,其中10表示查询的最大记录数不超过10个,5表示偏移量,即跳过5行取10个。
在where条件子句的优化方面,就有更多优化方式了。比如,尽量不要使用LIKE模糊匹配查询,如果可能,则使用=查询;尽量不要使用IN语句,可以使用=和or替代。此外,在多个条件中,要把非文本的条件放在前面,文本条件放在后面,示例代码如下:
(salary > 5000000) AND (lastName LIKE 'Guan') 优于 (lastName LIKE 'Guan') AND (salary > 5000000)
这是因为非文本的条件判断比较快,如果不满足,就不用再计算后面的条件表达式了。
(3)插入(或删除)优化
索引可以提供查询性能,但是对于插入和删除是有负面影响的。索引就像是书中的目录,插入和删除数据必然造成索引重排,所以创建索引要慎重。
在SQLite中,有一些PRAGMA指令可以改变数据库的行为。PRAGMA synchronous指令用于设置数据同步操作。同步是指在插入数据时,将数据同时保存到存储介质中。如果PRAGMA synchronous = OFF,则表示关闭了数据同步,不等待数据保存到存储介质就可继续执行插入操作,这在大量数据插入时可以大大提高速度。在Objective-C中,可以调用sqlite3_exec函数设置数据是否同步,相关语句如下:
sqlite3_open(DATABASE, &db)
sqlite3_exec(db, "PRAGMA synchronous = OFF", nil, nil, nil)
插入完成后,也可以重新设置PRAGMA synchronous = NORMAL或PRAGMA synchronous = FULL。
4.3 使用 Core Data
Core Data是面向对象的ORM技术,苹果公司推荐使用。它提供了缓冲、延迟加载等技术,其性能比较好,但有时候我们会发现它的性能要比SQLite差,这主要与存储类型的设置有关。Core Data的存储类型有NSSQLiteStoreType、NSBinaryStoreType和NSInMemoryStoreType,我们主要采用NSSQLiteStoreType类型,这样底层存储就采用了SQLite数据库,SQLite数据库的优点也能发挥出来。
使用Core Data时,还要考虑查询优化问题。它的查询是通过NSFetchRequest执行Predicate定义的逻辑查询条件实现的,在优化规则上与SQLite的where条件子句是一样的。此外,如果要查询返回记录数的限制,可以使用如下语句:
这两条语句相当于SELECT * FROM Note Limit 10 Offset 5;。
此外,还可以设置pragma指令,相关语句如下:
在上述代码中,我们首先把这些pragma指令放置于NSMutableDictionary可变字典中,然后以NSSQLitePragmasOption为键再把指令设置到可变字典中。NSPersistentStore对象的addPersistentStoreWithType:configuration:URL:
options:error:方法的options参数用于接收设置的NSDictionary对象。
为了方便分析Core Data的执行情况,我们可以使用Instruments工具中的Core Data跟踪模板,如图14所示。
图14 选择Core Data跟踪模板
进入Core Data跟踪模板后,如图15所示,可以看到其内部有3个跟踪项目:Core Data Fetches、Core DataCache和Core Data Saves。
图15 Core Data跟踪模板
当我们执行查询、插入和删除操作时,在Core Data Fetches和Core Data Saves的跟踪项目右边会产生很多线。
其中,①部分的线段为Fetch count(查询的记录数据);②部分的线段为Fetch duration(执行查询的持续时间),将虚线拉到上面可以看到这些内容的具体数值;如果有数据要保存,③部分产生的线段为Save duration(保存所持续的时间);④部分是更具体的信息,Fetch entity列是查询的实体类(Note),Fetch count列是查询的记录数,Fetch duration列是查询的执行时间。
了解更多相关资讯,关注南昌网络公司--百恒网络官方网站。百恒网络是一家专业从事南昌网站建设、微信开发、APP开发、网络营销等服务的南昌网络公司,技术过硬,经验丰富。如有任何网站方面的问题,百恒网络随时欢迎大家来电咨询,我们专业为您解答!