iOS内存优化

iOS系统会将内存分为自由(Free)、空闲(inactive)、活跃(active)以及固定(wired)四个类型,并且根据系统需要进行相互转化。ARC(Automatic ReferenceCounting, 自动引用计数)是当前主流的内存处理方式,它避免了最常见的由于忘记释放内存所造成的内存泄露,并自动为你管理retain和release的过程。除了避免内存泄露,ARC还可以帮你提高性能,保证释放掉不再需要的对象内存。

懒加载

视图控制对象通过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中添加这个方法:

在必要时使用先前注册的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)很相似,这个需要注意。

下面使用一个属性来延迟加载一个date formatter:

这样会比用C来解析日期字符串还快!许多web API会以微秒的形式返回时间戳,因为这种格式在javascript中更方便使用。记住用dateFromUnixTimestamp之前除以1000。

XIB和Storyboards

在开发中XIB和Storyboards为我们封装了很多细节,包括自动布局、Size Class等最新的技术,一种理念即“所见即所得”的开发方式,在日常开发中确实对我们带来了很大帮助,但是在优化上,可掌控性也相应的弱化了很多。

如果使用xib的话,使他们尽量简单,尝试为每个Controller配置一个单独的xib,尽可能把一个View Controller的view层次结构分散到单独的xib中去。因为加载一个xib的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。Storyboards相对好一点,storyboard仅在需要时实例化一个view controller。与之相对,代码UI具有最好的代码重用性,如果是写一些可以高度重用的控件提供给其他开发者使用,毫无疑问最好的选择应该是使用代码来完成UIView的子类。

对于一些自定义的view,往往也会使用单个xib并从main bundle进行加载的方式来载入。xib帮助完成view的创建、布局和与file owner的关系映射等一些列工作,xib的文件都是view的内容。xib的问题在于文件内容过于复杂,可读性很差,即使只是简单打开没有编辑也有可能造成变化而导致合并和提交的苦难。xib中的设置往往并非最终设置,在代码中你将有机会覆盖你在xib文件中进行的UI设计。XIB没有逻辑判断,也很难在运行时进行配置,而反之使用代码确是无所不能的。如果选择xib,那么要尽量将xib的工作和代码的工作隔离开来,能够使用xib完成的内容就统一使用xib来做,尽量保持必要的、较少的IBOutlet和IBAction。

StoryBoard是一组viewController对应的xib,以及它们之间的转换方式的集合。在StoryBoard中不仅可以看到每个ViewController的布局样式,也可以明确地知道各个ViewController之间的转换关系。相对于单个的xib,其代码需求更少,也由于集合了各个xib,使得对于界面的理解和修改的速度也得到了更大提升。StoryBoard中不允许有单个view的存在,因此很多时候我们需要借助于单个的xib来自定义UI。StoryBoard更强调的是一种层次结构,是在全局的视角上来组织UI设计和迁移。而对于单个的view,更多的会注重于重用和定制,而与整个项目的流程没有太大关系。

在iOS10来临的今天,如果不是对性能要求极高的项目,这些差别已经不是很大。

选择正确的数据结构

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

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

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

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

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

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

自学可以到这里,网站vi里包含了当前最全的数据结构模型,想深入学习的可以到这里自我巩固,这是一个视频网站,文字版的可以到 Algorithms 这里学习。

后台处理

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

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

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

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

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

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

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

  • 设备旋转消息 UIDeviceOrientationDidChangeNotification

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

  • 电池变化 UIDeviceBatteryLevelDidChangeNotification 和 UIDeviceBatteryStateDidChangeNotification

  • 用户默认设置变化 NSUserDefaultsDidChangeNotification

  • 本地化语言变化 NSCurrentLocaleDidChangeNotification 等。

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

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

    处理内存警告

    iOS下每个app可用的内存是被限制的,如果一个app使用的内存超过了这个阀值,则系统会向该app发送Memory Warning消息。收到消息后,app必须尽可能多的释放一些不必要的内存,否则OS会关闭app。

    如果app收到了内存警告,它就需要尽可能释放更多的内存。最佳方式是移除缓存、图片对象和其他一些可以重创建对象的强引用。一般使用如下方法:

    • 在app delegate中使用applicationDidReceiveMemoryWarning:方法

    • 在自定义UIViewController的子类(subclass)中覆盖didReceiveMemoryWarning

    • 注册并接收 UIApplicationDidReceiveMemoryWarningNotification的通知,一旦收到这类通知,你就需要释放任何不必要的内存使用。

    View Controller中loadView在每一次使用这个property并且为nil的时候被调用,用以产生一个有效的(手工维护views,必须重写该方法)。view 控制器收到didReceiveMemoryWarning的消息时, 默认的实现是检查当前控制器的view是否在使用。 如果它的view不在当前正在使用的view hierarchy里面,且你的控制器实现了loadView方法,那么这个view将被release, loadView方法将被再次调用来创建一个新的view。

    UIViewController的默认行为是移除一些不可见的view,删掉一些额外的数据结构。一个有图片缓存的app可以移除不在屏幕上显示的图片,这样对内存警报的处理是很必要的。

    iOS6之后需要在didReceiveMemoryWarning方法里释放当前不再使用的资源:

    iOS5.0之前didReceiveMemoryWarining 会判断当前ViewController的view是否显示在window上,如果没有显示在window上,则didReceiveMemoryWarining 会自动将viewcontroller 的view以及其所有子view全部销毁,然后调用viewcontroller的viewdidunload方法,销毁对象在viewDidUnload方法中进行。

    didReceiveMemoryWarning方法内部首先会检测控制器的view在不在屏幕上,不在屏幕上就会尝试销毁控制器的view,否则不销毁。当需要再次使用控制器的view时,又会调用loadView方法来创建view,执行生命周期方法 。

    iOS缓存优化

    在当今的开发中,缓存越发的显得重要,在很大层面上缓存能对系统性能带来很大的提升。cache 是一种常见的空间换时间提升性能的机制,可以用在相当多的场合。尽量 cache 那些可重复利用的对象,缓存那些不大可能改变但是需要经常读取的东西,比如table cell、date/number formatters、远端服务器的响应、图片、甚至计算结果(UITableView的行高)等。

    NSURLConnection

    NSURLConnection默认会缓存资源在内存或者存储中根据它所加载的HTTP Headers。你甚至可以手动创建一个NSURLRequest然后使它只加载缓存的值。一个NSURLRequest的图片缓存示例如下:

    可以通过 NSURLConnection 获取一个URL request, AFNetworking也一样的。如果你需要缓存其它不是HTTP Request的东西,你可以用NSCache。NSCache和NSDictionary类似,不同的是系统回收内存的时候它会自动删掉它的内容。

    由于GET请求一般用来查询数据,POST请求一般是发大量数据给服务器处理(变动性比较大),因此一般只对GET请求进行缓存,而不对POST请求进行缓存。另外,需要服务器定义数据是否发生变化,allHeaderFields里可以查找到是否修改了信息,公司服务器没有定义的话,就不能够判断读取的缓存数据是否需要刷新。

    NSURLCache

    iOS中缓存技术用到了NSURLCache类。原理是一个NSURLRequest对应一个NSCachedURLResponse,使用缓存技术把要缓存的数据都保存下来。NSURLCache的常见用法有:

    1.获得全局缓存对象(没必要手动创建)

    iOS对NSURLRequest提供了7种缓存策略:

    要想对某个GET请求进行数据缓存,非常简单。只要设置了缓存策略,系统会自动利用NSURLCache进行数据缓存。

    NSCache

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

    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: 方法,缓存将要删除对象时调用,不能在此方法中修改缓存。仅仅用于后台的打印,以便于程序员的测试。

    两种图片加载方式

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

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

    补充:CPU缓存简析

    CPU的流水线(pipeline)并发性简单来说,Intel Pentium处理器有两条流水线U和V,每条流水线可各自独立地读写缓存,所以可以在一个时钟周期内同时执行两条指令。但这两条流水线不是对等的,U流水线可以处理所有指令集,V流水线只能处理简单指令。

    CPU指令通常被分为四类,第一类是常用的简单指令,像mov, nop, push, pop, add, sub, and, or, xor, inc, dec, cmp, lea,可以在任意一条流水线执行,只要相互之间不存在依赖性,完全可以做到指令并发。第二类指令需要同别的流水线配合,像一些进位和移位操作,这类指令如果在U流水线中,那么别的指令可以在V流水线并发运行,如果在V流水线中,那么U流水线是暂停的。第三类指令是一些跳转指令,如cmp,call以及条件分支,它们同第二类相反,当工作在V流水线时才能通U流水线协作,否则只能独占CPU。第四类指令是其它复杂的指令,一般不常用,因为它们都只能独占CPU。

    如果是汇编级别编程,要达到指令级别并发,必须要注重指令之间的配对。尽量使用第一类指令,避免第四类,还要在顺序上减少上下文依赖。

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

    Windows系统为每个进程都分配了4g的虚拟内存地址,内核占了2g,剩余的空间有栈区、堆区、全局数据区和代码区等,总结起来就是动态数据区、代码区和静态数据区。C++中每个程序的内存分配为代码区、全局数据区、堆区和栈区。C++的内存分配方式有三种:

    1.从静态存储区域分配

    编译时就分配好,运行期间一直存在,如全局变量、静态变量

    2.在栈上分配

    函数内局部变量,函数执行完后自动释放,内置于处理器的指令集中,效率很高,内存容量有限

    3.在堆上分配(动态内存分配)

    malloc或new申请,free或delete释放

    局部变量效率高是因为局部变量在函数体中被多次引用时,该变量会被放入到缓存里,所以每次读取数据,只要其存在在缓存中就不会去访问内存,但其实如果你在函数体中对局部变量和静态变量做同样次数的引用时,执行时间是一样长的。

    程序设计要尽量满足局部性原理,局部性原理又分为时间局部性和空间局部性,时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。空间局部性是指一旦程序访问了某个存储单元,则不久之后。其附近的存储单元也将被访问。附近即具有连续地址。

    C语言中应该尽量减少静态变量的引用,这是因为静态变量存储在全局数据段,在一个被反复调用的函数体内,引用该变量需要对缓存多次唤入唤出,而如果是分配在堆栈上的局部变量,函数每次调用CPU只要从缓存中就能找到它了,因为堆栈的重复利用率高。循环体内的代码要尽量精简,因为代码是放在指令缓存里的,而指令缓存都是一级缓存,只有几K字节大小,如果对某段代码需要多次读取,而这段代码又跨越一个L1缓存大小,那么缓存优势将荡然无存。

    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这类库的原因。

    Core Data

    CoreData优劣分析

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

    CoreData多线程安全

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

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

    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 中进行操作时,使用以下方法:

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

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

    FMDB

    示例

    FMDB库的地址在,FMDB使用起来并不复杂,根据文档中的ReadMe就能很快的入手了。FMDB是一款简洁、易用的封装库,导入工程时需要使用 lib 依赖包。FMDB同时兼容ARC和非ARC工程,会自动根据工程配置来调整相关的内存管理代码。

    它有三个重要类:

    多线程操作

    不要在多个线程中同时使用一个FMDatabase实例,为每个线程创建一个FMDatabase对象,使用 FMDatabaseQueue 在多线程中进行处理。FMDatabaseQueue不需要调用open,因为查看代码发现,Queue已经open了。

    FMDatabaseQueue通过内部创建一个Serial的dispatch_queue_t来处理inDatabase和inTransaction传入的Blocks。

    因为队列是串行执行的,因此inDatabase的block不能嵌套使用。

    使用事务:

    在更新量比较大的时候(比如更新1000条或10000条),事务处理更快一些。

    Yapdatabase

    Yapdatabase的网址是,和FMDB一样也是建立在sqlite数据库上的一层访问层,使用key-value方式存储、获取数据,并且具有强大的插件机制和Views、Secondary Indexs、Full Text Search等,使用Objective-C的api上手简单。

    由GitHub上的YapDatabase文档我们知道,YapDatabase有如下特性:

    • 并发:当另外一个线程访问修改数据库的时候,可以并发的读取数据。在后台线程写入数据,不用担心阻塞主线程,并且可以同时在多个线程读取数据。

    • 内嵌缓存:可以在数据库中缓存对象,sqlite缓存的是字节流,而你可以使用yapdatabse来直接缓存对象,这样内置的缓存对象特性可以使你省去了序列化对象的过程,你可以更快的处理对象。

    • 集合:一个key不够时可以采用collection & key。

    • 元数据(Metadata): 可以直接存储像下载下来的数据如NSData这样的东西。

    • 视图:当需要对数据过滤、分组、排序时,使用 YapDatabase 不需要写复杂的sql语句。 YapDatabase 还可以更新它们,自动更新自己,方便的实现动画效果。

    • 索引:使用索引可以快速定位一些反复访问的数据。

    • 高性能(Performance): 在主线程读取成千上万的对象都不会掉帧。

    • 全文搜索:建立在sqilte的FTS 上,使用最小的代价快速的找到你需要的数据。

    • 关系:你可以在对象上建立关系,建立级联删除规则。

    • 扩展:不仅仅是key-value,还具备很多扩展的数据结构,可以扩展自己的数据结构。

    使用,创建数据库(本文示例来自官方demo):

    YapDatabaseViewGrouping可以起到过滤的作用,比如某个section中所有的内容。YapDatabaseView提供子集,分组和排序3个功能,相当于数据库中的where, group by,order by 语句,和coredata中的NSFetchedResultsController 类似,view会随着数据的更新,自动更新对应的内容。

    异步填充:

    注册YapDatabaseView:

    使用YapDatabaseView:

    可以使用Long-Lived Read Transactions和YapDatabaseModifiedNotification做UI同步。

    更多好玩的功能可以到里进行涉猎。

    网络请求优化

    当前的应用更多的是一种前端框架,内容依赖于远端资源或是第三方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。

    文/吴白(简书作者)

    原文链接:

    更多精彩内容请关注“e安在线”微信公众号

    1.《ios开发如何做缓存处理》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

    2.《ios开发如何做缓存处理》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

    3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/keji/3248559.html