恰当的数据结构,能让代码产生质的飞跃。本篇总结下在开发中对‘队列’的一点体会。
有限资源的访问
先进先出(FIFO)是队列的本质属性,所以当遇到这样的需求时,比如实现一个FIFO的cache,那么使用队列自然不需要太多思考。
本篇主要总结下在App中使用队列来控制对‘有限的资源’的访问。
队列在iOS框架中随处可见,比如主线程的runloop会把所有等待处理的Event放在队列里,GCD的serial queue本身就是一个队列,还有NSOperationQueue等。这些例子中有一个共同的模式,就是对一个有限资源的使用,runloop是资源,线程是资源。
而在多线程编程中,我们常常使用锁来控制对资源的访问。从某种角度来看,锁也是操作系统提供的存放访问线程的队列。
道理很简单,更多的时候需要思考的是:什么是有限的资源,什么是资源的访问者。
举一个例子:在iOS中,弹窗(alert view)最多只能显示一个,比如定位城市更新、订单状态改变等。于是我们使用了队列来控制对弹窗的使用(swift伪代码,省略了一些细节):
public class WindowManager {
private var _queue: Queue<(string, string)="">
private var isShowing: Bool
public func show(title: String, message: String) {
onMainQueue {
if isShowing {
_queue.enqueue((title, message))
} else {
showInternal(title, message)
}
}
}
public func dismiss() {
onMainQueue {
dismissInternal()
// schedule pending windows
if _queue is not empty {
let (title, message) = _queue.dequeue()
showInternal(title, message)
}
}
}
private func showInternal(title: String, message: String) {
isShowing = true
showAlertView()
}
private func dismissInternal() {
isShowing = false
dismissAlertView()
}
// execute block on main queue
private func onMainQueue(block: () -> Void) {
if NSThread.isMainThread() {
block()
} else {
dispatch_async(dispatch_get_main_queue(), block)
}
}
}
(string,>
后来需求变复杂了些,比如当前在登录界面时,弹窗需要等登录界面退出再显示。
比较差的解决方案:
1. 在要弹出alert view的时候先检查当前是不是在登录界面,是的话就把参数进队列;
2. 在登录界面消失的时候,再去检查弹窗系统的队列。
这种方法耦合了弹窗系统和登录界面,可维护性和可扩展性比较差。
我们把弹窗的概念扩展了一下:登录界面下不能显示弹窗,其实是登录界面占用了窗口资源,因此它也可以看做是一种弹窗。于是我们用闭包替换具体弹窗的显示和消失,代码更新成如下(伪代码,省略一些细节):
public class WindowManager {
private var _queue: Queue< (() -> Void, () -> Void) > // (showBlock, dismissBlock)
private var activeDismissBlock: ( () -> Void )?
public func show(showBlock: () -> Void, dismissBlock: () -> Void) {
onMainQueue {
if activeDismissBlock != nil {
// some window already show
_queue.enqueue( (showBlock, dismissBlock) )
} else {
showInternal(showBlock, dismissBlock)
}
}
}
public func dismiss() {
onMainQueue {
dismissInternal()
// schedule pending windows
if _queue is not empty {
let (showBlock, dismissBlock) = _queue.dequeue()
showInternal(showBlock, dismissBlock)
}
}
}
private func showInternal(showBlock: () -> Void, dismissBlock: () -> Void) {
showBlock()
activeDismissBlock = dismissBlock
}
private func dismissInternal() {
activeDismissBlock?()
activeDismissBlock = nil
}
// execute block on main queue
private func onMainQueue(block: () -> Void) {
if NSThread.isMainThread() {
block()
} else {
dispatch_async(dispatch_get_main_queue(), block)
}
}
}
事实证明这种抽象是有十分有效的,后来添加更多种类的弹窗,比如广告页面、新手引导页面等,可以无缝接入。
资源访问的优化
使用队列可以优化对资源的使用,在调度下一个使用请求时,可以检查在队列中等待的请求,删掉一些不必要的操作。一个比较明显的例子就是用户在某个极端的时间内连续触发了‘赞’和‘取消赞’的操作,那么相邻的‘赞’和‘取消赞’就可以互相抵消。
另外一种情况是由于各种原因,会有很多重复的请求,比如不同的业务模块短时间内都发送了更新某个缓存的请求,通过队列我们可以把重复的请求去掉。
资源池
将‘资源’抽象成一个接口,那么只要‘资源池’实现了这个接口,也可以被看成是一个资源。
Generalize
我门可以设计几个接口,泛化这个模式,把与具体业务相关的逻辑剥离出去。 定义资源的接口如下:
public protocol Resource {
// whether resource is available
var available: Bool { get }
// call this observer block when available value change
var availableObserver: ( () -> Void )? { get set }
// add visitor
func addVisitor(visitor: Visitor)
// remove visitor
func removeVisitor(visitor: Visitor)
}
定义资源使用者接口:
public protocol Visitor {
// access resource. call completion when access finished
func access(resource: Resource, completion: () -> Void )
// whether is duplicate to another visitor
func isDuplicate(anotherVisitor: Visitor) -> Bool
// whether is opposite to another visitor
func isOpposite(anotherVisitor: Visitor) -> Bool
}
使用者队列:
public class VisitorQueue {
private var _queue: Queue
private var _resource: Resource
public init(resource: Resource) {
_queue = Queue()
_resource = resource
// set schedule as resource's available observer
_resource.availableObserver = { [weak self] in
self?.schedule()
}
}
public func enqueue(visitor: T) {
if _resource.available {
grantAccess(visitor)
} else {
_queue.enqueue(visitor)
}
}
private func schedule() {
while _resource.available && !_queue.isEmpty() {
let visitor = _queue.front()!
_queue.dequeue()
// do optimizations
if let nextVisitor = _queue.front() {
// remove duplicate
if visitor.isDuplicate(nextVisitor) {
continue
}
// remove together with opposite
if visitor.isOpposite(nextVisitor) {
_queue.dequeue()
continue
}
}
grantAccess(visitor)
}
}
private func grantAccess(visitor: Visitor) {
_resource.addVisitor(visitor)
visitor.access(_resource) {
self._resource.removeVisitor(visitor)
}
}
}
总结
计算机界有句名言: “All problems in computer science can be solved by another level of indirection”,翻译成中文就是:所有的计算机问题都可以通过添加另一层抽象来解决。上面的例子中,我们通过一步步的抽象,把只能解决一个具体问题的方法,逐步应用到更广的问题。这就是‘抽象’的魅力,它让代码变的简洁,变的可以复用,降低维护的难度,减少宝贵的时间成本,让程序员的生活变的更幸福!=v=