浅谈队列在移动开发中的应用

恰当的数据结构,能让代码产生质的飞跃。本篇总结下在开发中对‘队列’的一点体会。

有限资源的访问

先进先出(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)
        }
    }
}

后来需求变复杂了些,比如当前在登录界面时,弹窗需要等登录界面退出再显示。

比较差的解决方案:
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=