肇鑫的技术博客

业精于勤,荒于嬉

Swift程序内部对于锁的处理

我们在同步时,经常会遇到不同来源的数据同时到达的问题,这个时候,我们就需要先锁定资源,然后再对数据依次进行处理。

这里,我们通过一个开关来简单的模拟这个锁定的过程,当开关打开时,点击按钮的信息会被延后;在开关关闭时,延后的消息会执行。

s

方案一:忙等待

忙等待是最简单的处理方式。程序反复检测开关是否关闭,如果不是,则继续检测;如果是,则执行任务。

@IBAction func printHello(_ sender: Any) {
    // 忙等待
    DispatchQueue(label: "default").async {
        while true {
            if self.shouldWaitSwitch.isOn { continue }
            
            print("你好!")
            break
        }
    }
}

@IBOutlet weak var shouldWaitSwitch: UISwitch!

忙等待的优点是实现简单。缺点是忙等待会一直占用CPU。如果预期等待的时间会很长,则应避免使用忙等待。

方案二:延时处理

为避免忙等待一直占用CPU的问题,我们可以使用延迟处理的方式,即创建一个计时器,每间隔一定的时间,就检查一次状态,看可不可以执行。如果可执行,则执行,并终止计时器;如果不能执行,则等待下一次的查看。

@IBAction func printHello(_ sender: Any) {
    // 延迟
    if shouldWaitSwitch.isOn {
        Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [unowned self] (timer) in
            if self.shouldWaitSwitch.isOn { return }
            
            print("你好!")
            timer.invalidate()
        })
    }
    else {
        print("你好!")
    }
}

@IBOutlet weak var shouldWaitSwitch: UISwitch!

延迟处理的好处是大大降低了等待时的CPU的占用。缺点是,当开关变为关闭时,不会立即执行,而是可能会有一段间隔。如果时间间隔设置不当,或者开关切换得很频繁,则可能产生饥饿现象。

方案三:消息绑定

消息绑定在开关状态改变时,立即执行相应操作。

@IBAction func printHello(_ sender: Any) {
    // 消息绑定
    if shouldWaitSwitch.isOn {
        if let _ = shouldWaitSwitch.actions(forTarget: self, forControlEvent: .valueChanged) { // already set
            return
        }
        
        shouldWaitSwitch.addTarget(self, action: #selector(printHello(_:)), for: .valueChanged)
    }
    else {
        print("你好!")
    
        if let _ = shouldWaitSwitch.actions(forTarget: self, forControlEvent: .valueChanged) { // if set
            shouldWaitSwitch.removeTarget(self, action: #selector(printHello(_:)), for: .valueChanged)
        }
    }
}

@IBOutlet weak var shouldWaitSwitch: UISwitch!

消息绑定的优点是CPU占用低、响应迅速。缺点是实现起来比较复杂。特别是,一旦绑定了某个操作,这个操作在完成后,要尽量取消绑定,或者使其在开关状态发生改变时,刚好成为不满足运行条件的状态。


队列

上面解决问题的方案,都是只能执行一次打印功能。如果我们需要记录每一次的按钮点击,上面的方案就不适用了。这时我们就需要额外维护一个队列,来记录每一次的点击。这里通过扩展消息绑定的代码来实现这个队列功能。

当开关开启时,如果有任务来,我们就向队列插入一个任务。当开关关闭时,我们执行这个任务。在执行任务之前,我们对于队列进行锁定,此时不能插入新任务,由于我们确认执行时间会很短,所以插入这个新任务的等待这里采用了忙等待。当任务执行完毕后,队列锁定被打开。

@IBAction func printHello(_ sender: Any) {
    // 队列,以消息绑定为例
    if shouldWaitSwitch.isOn {
        while true {
            let isSuccess = WaitingTaskQueue.append(task: "你好!")
            if isSuccess { break } else { continue }
        }
        
        if let _ = shouldWaitSwitch.actions(forTarget: WaitingTaskQueue.self, forControlEvent: .valueChanged) { // already set
            return
        }

        shouldWaitSwitch.addTarget(WaitingTaskQueue.self, action: #selector(WaitingTaskQueue.dealingTasks), for: .valueChanged)
    }
    else {
        print("你好!")
        
        if let _ = shouldWaitSwitch.actions(forTarget: WaitingTaskQueue.self, forControlEvent: .valueChanged) { // if set
            shouldWaitSwitch.removeTarget(WaitingTaskQueue.self, action: #selector(WaitingTaskQueue.dealingTasks), for: .valueChanged)
        }
    }
}

@IBOutlet weak var shouldWaitSwitch: UISwitch!

class WaitingTaskQueue {
    private static var firstItemPosition = 0
    private static var queue:[String] = []
    private static var isLocked = false
    
    @objc static func dealingTasks() {
        guard !isLocked else {
            return
        }
        
        isLocked = true
        
        var relativePosition = 0
        while relativePosition < queue.count {
            print("\(queue[relativePosition]), \(firstItemPosition + relativePosition)")
            relativePosition += 1
        }
        queue.removeAll(keepingCapacity: true)
        firstItemPosition += relativePosition
        
        isLocked = false
    }
    
    static func append(task:String) -> Bool { // is success
        guard !isLocked else {
            return false
        }
        
        queue.append(task)
        
        return true
    }
}

结论

采用消息通知,结合忙等待是最好的方式。延迟技术,存在较大的局限性,例如容易造成饥饿或者任务的执行顺序无序等问题,应该仅在特定的条件下使用。

"Closure cannot implicitly capture a mutating self parameter"问题的处理

struct中,如果我们在closure中使用self,就会得到Closure cannot implicitly capture a mutating self parameter的错误提示。比如:

struct Foo {
    var bar = 10
    
    mutating func changeBar() {
        let closure = {
            self.bar = 50 // Closure cannot implicitly capture a mutating self parameter
        }
        
        closure()
    }
}

并且由于Foo的类型是struct,我们也没发在closure里添加截获列表。那么是不是就必须使用class了?答案是否定的。有两种方式可以解决这个问题。

方案一:为closure增加一个inout类型的参数

struct Foo {
    var bar = 10
    
    mutating func changeBar() {
        let closure = { (s:inout Foo) -> () in
            s.bar = 50
        }
        
        closure(&self)
    }
}

根据inout类型的说明,我们知道,实际上这相当于增加了一个隐藏的临时变量,self被复制,然后在closure中使用,完成后,再复制回self。也就是说,这个方法有额外的内存开销。如果是struct较大的情形,这么做并不划算。

方案二:使用UnsafeMutablePointer<Pointee>

这次采用直接指针的方式对于struct来进行操作,采用指针的好处是self不会被多次复制,性能较高。缺点是你需要自行确定你的代码的安全。

struct Foo {
    var bar = 10
    
    mutating func changeBar() {
        let selfPointer = UnsafeMutablePointer(&self)
        
        let closure = {
            selfPointer.pointee.bar = 50
        }
        
        closure()
    }
}

结论

Closure cannot implicitly capture a mutating self parameter错误的原因是在进出closure之后,self的一致性没办法得到保证,所以编译器默认不允许在structclosure中使用self。如果我们确定这么做是安全的,就可以通过上面的两种方式解决这个问题。其中,方法二的性能更好一些。

struct Foo {
    var bar = 10
    
    mutating func changeBar() {
        let closure = {
            self.bar = 50 // Closure cannot implicitly capture a mutating self parameter
        }
        
        closure()
    }
}

注意
这里可以记一下指针和swift变量之间的关系:
UnsafePointer对应let
UnsafeMutablePointer对应var
AutoreleasingUnsafeMutablePointer对应unowned UnsafeMutablePointer,用于inout的参数类型
UnsafeRawPointer对应let Any,raw系列都是对应相应的Any类型
UnsafeBufferPointernon-owning的类型(unowned),用于collectionelements, buffer系列均如此

NSLocalizedString的特殊用法

一般情况下,我们使用NSLocalizedString来加工需要翻译的字符串,如:

let says = NSLocalizedString("Hello World!", comment: "hello world")

一般情况这样就够了。如果你的字符串里包含了变量,这个就不能用了。比如:

let count = 10
let says = NSLocalizedString("It runs \(count) times", comment: "run times")

says即使你翻译了,生成的程序也不能正确地显示。这是因为,目前版本的NSLocalizedString不支持Swift的这种用法。它先把says变成“It runs 10 times",然后查找是否有翻译与其匹配,显然是没有的。

在这里,我们需要使用StringlocalizedStringWithFormat方法。

let newSays = String.localizedStringWithFormat(NSLocalizedString("It runs %d times", comment: "new run times"), count)

然后就可以了。这么做很不Swift,但是,这个是目前唯一可用的办法。