面试题(十四)
唐巧前辈说这些都是 iOS 的基础问题,应该对此深入的了解。当初看到时,大部分回答不上来,因为平时没有好好思考整理过。这里大部分的概念大多会在学习 OC 的过程中遇到过,但还是得经过写代码才能有更深的理解。反正我当初看那些设计模式是云里雾里,每个字都认识,就是不知道说的什么。即使现在,有些东西,我也不是很理解。
Objective-C 底层
Objective-C runtime library:Objective-C 的对象模型,Block 的底层实现结构,消息发送,消息转发,category,method 实现,class load。
runtime 我在平时很少涉及到,没有系统学习过,而且很多次看了不久就忘了,所以这里给出一些不错的文章的链接供参考。这几个问题在《iOS 7 Programming Pushing the Limits》都有过深入的解读(我有电子版,是盗版,这里给出这本书在 Github 的地址,工作后我会把去年看过的盗版书全部补偿买回,没有 iOS 8 的版本,不知道是不是由于盗版太多导致的)。另外,唐巧前辈撰文讨论过前两者:
唐巧在后记中也提到了 iOS 64-bit 带来的变化:
那么就来看看 Session 404 Advanced in Objective-C ,从36分起讲相关的东西,喔,看不懂,那还是看看这个吧,在《iOS 7 Programming Pushing the Limits》的 Further Reading: objc_explain_Non-pointer_isa 部分谈论了这个问题。
内容非常翔实,特别是关于 Block 类型的部分,强烈建议做下文章开头提到的测试:Objective-C Blocks Quiz。
3. 消息发送和消息转发
消息发送比较好理解,先了解下 runtime 吧,可以查看官方文档《Objective-C Runtime Guide》。之前学习其他语言的时候还没有关注过调用函数的背后发生了什么,在 Objective-C 中,在对象上调用方法称为发送消息,比如[receiver message];这行代码,编译的时候编译器将之转换为对 底层 C 函数objc_msgSend 的调用:objc_msgSend(receiver, selector);在运行时,调用哪个方法则完全由 runtime 决定,甚至在运行时可以替换调用的方法,这是 Objective-C 被称为动态语言的根本原因。对于消息转发,说实话我现在还不知道这个的应用场景,看到的大部分博客都是说消息转发给了你补救措施来应对没有没有实现的方法防止 Crash 或者实现类似多继承的机制,我有个疑惑,干嘛不实现那个方法,而要在代价很大的转发机制里处理呢。在《Effective Objective-C 2.0》一书第 12 条 tip 中用 @dynamic 演示了实现动态方法解析的例子来说明消息转发的意义,老实说,我还是没有理解这个的意义。这里有个对官方文档的中文翻译和一些注解。
4. Implement of category and method
找到了来自这位比我厉害得多的90后:《刨根问底Objective-C Runtime(3)- 消息 和 Category》(文章原来的链接放进来跟简书的处理有冲突,这里给的是博客地址,而不是这篇文章的具体地址,不过很好找)。
5. Class load
可以看这篇博客:《Objective-C Class Loading and Initialization》,看了下作者的 Github,原来是我以前 follow 过的国外程序员,看人家的 repo 和星星,质量有保障,再看博客文章列表,有很多深入底层的内容,一座宝矿啊。另外在《Effective Objective-C 2.0》书中第51节《精简 initialize 与 load 的实现》中也讨论了这个问题,当初看完一头雾水,如今终于能看懂啦。
Core Data: 大量数据多线程同步
这个问题我已经单独成篇放到这里了,添加了更多的基础知识和介绍。
第一步:搭建 Core Data 多线程环境
这个问题首先要解决的是搭建 Core Data 多线程环境。NSManagedObjectContext 不是线程安全的,你不能随便地开启一个后台线程访问 managed object context 进行数据操作就管这叫支持多线程了。Core Data 对多线程的支持比较好,NSManagedObjectContext 在初始化时可以指定并发模式,有三种选项:
1.NSConfinementConcurrencyType
这种模式是用于向后兼容的,使用这种模式时你应该保证不能在其他线程使用 context,但这点很难保证,不推荐使用。此模式在 iOS 9中已经被废弃。
2.NSPrivateQueueConcurrencyType
在一个私有队列中创建并管理 context。
3.NSMainQueueConcurrencyType
其实这种模式与第 2 种模式比较相似,只不过 context 与主队列绑定,也因此与应用的 event loop 很亲密。当 context 与 UI 更新相关的话就使用这种模式。
搭建多线程 Core Data 环境的方案一般如下,创建一个 NSMainQueueConcurrencyType 的 context 用于响应 UI 事件,其他涉及大量数据操作可能会阻塞 UI 的就使用 NSPrivateQueueConcurrencyType 的 context。环境搭建好了,如何实现多线程操作?官方文档《Using a Private Queue to Support Concurrency》为我们做了示范,在 private queue 的 context 中进行操作时,应该使用以下方法:
func performBlock(_ block: () -> Void)//在私有队列中异步地执行 Blcok func performBlockAndWait(_ block: () -> Void)//在私有队列中执行 Block 直至操作结束才返回
要在不同线程中使用 managed object context 时,不需要我们创建后台线程然后访问 managed object context 进行操作,而是交给 context 自身绑定的私有队列去处理,我们只需要在上述两个方法的 Block 中执行操作即可。而且,在 NSMainQueueConcurrencyType 的 context 中也应该使用这种方法执行操作,这样可以确保 context 本身在主线程中进行操作。
第二步:数据的同步操作
多 context 同步最简单的方案如下:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "backgroundContextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: backgroundContext) func backgroundContextDidSave(notification: NSNotification){ mainContext.performBlock(){ mainContext.mergeChangesFromContextDidSaveNotification(notification) } }
NSManagedObjectContext 在执行保存操作后会发出 NSManagedObjectContextDidSaveNotification,包含了 context 所有的变化信息,包括新增的、更新的以及删除的对象的信息;而 mergeChangesFromContextDidSaveNotification(_ notification) 方法则用于合并其他 context 中发生的变化。
如果 context 并未观察其他 context 的 NSManagedObjectContextDidSaveNotification通知,且保存时,persistent store 已经被其他 context 更改过,那么很可能存在差异,此时同步就有了以下几种选择:选择保存 context 中的版本或者使用 persistent store 的版本替换 context 的版本,又或是将两者的版本融合。这种同步方式由 NSManagedObjectContext 的 mergePolicy属性决定。
1.NSErrorMergePolicy
默认策略,有冲突时保存失败,persistent store 和 context 都维持原样,并返回错误信息,是唯一反馈错误信息的合并策略。
2.NSMergeByPropertyStoreTrumpMergePolicy
当 persistent store 和 context 里的版本有冲突,persistent store 里的版本有优先权, context 里使用 persistent store 里的版本替换,但是 context 里没有冲突的变化则不会受到影响。
3.NSMergeByPropertyObjectTrumpMergePolicy
与上面相反,context 里的版本有优先权,persistent store 里使用 context 里的版本替换,但是 persistent store 里没有冲突的变化不受影响。
4.NSOverwriteMergePolicy
用 context 里的版本强制覆盖 persistent store 里的版本。
5.NSRollbackMergePolicy
放弃 context 中的所有变化并使用 persistent store 中的版本进行替换。
同步是件很复杂的事情,实际上还是需要根据实际需要来选择同步方案。上面两种方案中第一种概念简单实现容易,第二种比较复杂相对危险,需要谨慎选择同步策略。还有一点需要注意,如果需要跨线程使用 managed object,那么不要直接在其他 context 里使用该 managed object,而应该通过该对象的 objectID 将该对象 fetch 到 context 里。
最后,搞定大量数据
多线程和同步问题解决,最后的难点:大量数据。大量数据意味着需要我们关注内存占用和性能,写代码时需要记得以下规则:
1.尽可能缓存需要的数据,不相关的数据保持 faults状态。
2.fetch 时尽可能精准,少引入不相关的数据。
3.构建多 context 时尽量将同类 managed object 集中,最大限度减少合并需求。
4.提升操作效率,对Asynchronous Fetch, Batch Update,Batch Delete 等新特性尽可能利用。
多线程编程
在 iOS 编程中,这几种情况下需要处理多线程:UI 事件必须在主线程里进行,其他的可以放在后台进行;而进行一些耗时长或阻塞线程的任务,最后放进后台线程里进行。iOS 的多线程技术有这么几种:线程,GCD 和 NSOperationQueue。线程这种技术比较复杂,而多线程编程向来是「复杂必死」,推荐尽可能使用后二者,但线程有个后二者没有的优势:能够精确保证任务执行的时间。GCD 全称是 Grand Central Dispatch, 是 libdispatch 这个库的外部代号,基于 C 的底层来实现;而NSOperationQueue,通称操作队列,是基于 GCD 实现的。GCD 能做的 NSOperationQueue 基本上都能做,而且还有些 GCD 中不易实现的特性,如挂起、取消任务,虽然在 iOS 8 中,GCD 也提供了取消任务的功能,但在 GCD 中任务的挂起和取消都有较大的局限性;虽然大多数情况下应该使用抽象级别更高的 API,也就是 NSOperationQueue,但处理一般的后台任务我偏爱 GCD,主要是 GCD 搭配 Blcok 使用简单,非常方便。如何选择,下面两个链接对此问题的讨论值得一看:
StackOverflow: NSOperation vs. Grand Central Dispatch
Blog: When to use NSOperation vs. GCD
另外,还推荐这些文章:objc 的并发编程专题《Concurrent Programming》 及中文翻译版本;雷纯锋的博客《iOS 并发编程之 Operation Queues》;NSHipster 的《NSOperation》。
设计模式
评价 Delegate, Notification, KVO 几种设计模式的优缺点
我不觉得这个问题是个好问题,与其比较这几个设计模式的优缺点,不如谈它们各自的特点比较好,因为它们是为了解决某类问题才设计出来的,有各自适合的使用场景。另外,给个 iOS 中设计模式的介绍:iOS Design Patterns。
为什么出题目都喜欢把这三个设计模式拿来对比呢?Notification 和 KVO都是用于协助对象间的通信:某个对象监听某个事件的发生,当某个事件发生时,该对象会得到通知然后做出响应。这几句话大概是以前看过的书本上说的。如果你以前没接触过设计模式,第一次学习时总是能够看到事件、响应这类模糊的词汇,看得你云里雾里,好吧,我说的是我。 但 delegate,应该说没有监听的功能,而是当事件发生或时机到了,要求 delegate 对象做点什么。刚开始学习 OC 的时候,一本书中将 delegate比喻为助手,那时候不怎么理解,现在觉得这个比喻十分恰当。虽然delegate 模式在 OC 中随处可见,在UIViewController 类中广泛存在,但在开发 FaceAlbum 的过程中只遇到过一次自定义 protocol/delegate 的情况,后来还是用 KVO 取代了。相对于 Notification 和 KVO 模式,使用 delegate 模式你会明确知道对象的 delegate 能干什么,因为要成为 某个对象的delegate,该对象得遵守指定的 protocol,protocol 指定了 delegate 对象需要实现的方法。
Notification和 KVO两者都需要监听事件的对象(早期看见事件就犯晕,如今写来觉得用这个词挺顺手的)去注册,delegate则需要 delegate 对象遵守指定的 protocol;Notification 中监听者向一个单例对象NSNotificationCenter注册,NSNotificationCenter类似一个广播中心,接受任何对象的注册,后者则向要监听的对象注册,一对一,这两者都不需要对象之间有联系,而 delegate 则需要通信的对象通过变量联系;NSNotification模式里监听的对象与被监听的对象通信是通过 NSNotificationCenter 这个中介,而KVO 里,不能说两者是直接通信的,我没有了解过过 KVO 是如何实现通信的,从表面上看两者就那么心灵感应一般,这是系统替我们实现的,而delegate,由于通过变量连接,直接向 delegate 发送消息即可,在这点上,NSNotification不需要通信双方知道对方,而后两者则不然;在响应事件时,NSNotification和 KVO 模式里都是在注册时指定响应方法,而 delegate 则在 protocol 里预定义了响应方法。
说了这么多,不直观,说个实际场景,比如在 UICollectionView 里选择 cell 的时候,希望 title 能够跟踪选中 cell 的数量。这里用NSNotification和 KVO 都能实现,但是我更喜欢 KVO,感觉更优雅,因为使用NSNotification模式的话,选中一个 cell 的时候要在选择的方法里手动发布通知,而 KVO,只要对观察的属性实现 KVO 兼容的方法就可以了;而delegate,自己做自己的 delegate,呃。而面对一些系统里的事件,比如键盘的出现与消失,图片库的变化,使用NSNotification更加自然,因为 KVO 限于对对象属性的跟踪。