肇鑫的技术博客

业精于勤,荒于嬉

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系列均如此

Xcode中,项目的语言和翻译之间的关系

系统选择语言的机制

如果想要卖出更多的程序,你的程序就必须支持多国语言。iOS和macOS在处理程序时,语言部分的模型是这样的:

  1. 用户可以设置多个他懂得的语言,语言的优先级别是从上到下。
  2. 系统打开某个程序时,按照用户设置的语言偏好,依次查找程序是否提供了该语言的界面,如果是,就加载该语言,并打开程序;如果不是,就加载程序员指定的默认语言。

举例:
假设小明的设定的语言偏好顺序为简体中文、繁体中文、英文,程序A提供的语言为繁体中文、英文(默认)。那么小明打开程序A时,程序A会显示繁体中文的界面。
假设雅克布系统设定的语言偏好为法文,那么他打开程序A时,由于程序A没有提供法文的翻译,且程序A的默认语言为英文,所以雅克布看到的程序A界面就是英文的。

中文开发者面对的问题

程序员面对的问题更复杂一些。传统来说,程序员一般选择英文进行开发,然后再翻译成中文和其它语言。这个流程是经过检验的。可是对于中文开发者来说,直接用中文开发界面,然后再翻译成英文或其它界面,在查看时会更加直观。不过,传统上一般认为,如果你直接用中文开发,那么遇到上面举例中的雅克布的情况,由于默认开发的语言已经是中文,雅克布可能会看到一个自己不懂的中文界面的情况。

小结:中文开发者面对的问题是:

  1. 要使用中文开发
  2. 当用户偏好的语言,程序未能提供时,默认显示英文的界面

要解决这个问题,首先得知道Xcode中语言和翻译相关的模型。

Xcode中的语言模型

  1. Xcode采用Base Internationalization的方式。默认情况下,Base Internationalization为英文。
  2. 用户可以在项目的Info.plist中指定程序成默认语言,默认是英文。
  3. 用户添加多种语言的翻译。

解决方案

  1. 新建一个Cocoa程序项目
  2. 在Finder中,找到你的项目的.xcodeproj文件,右键点击,选显示包内容
  3. 双击打开project.pbxproj文件
  4. 在打开的文件中,搜索developmentRegion,默认对应的值为English,将其改为zh-Hans,并保存。这里修改的是Base Internationalization的语言,Xcode中显示为Development Language.
  5. 回到新建的项目,打开项目中的Info.plist,查看Localization native development region项,默认应该是en。如果确认为en,则不用修改。这个项的键是CFBundleDevelopmentRegion。代表程序的默认界面语言。
  6. 点击项目文件,添加新语言Chinese(Simplified),你会看到简体中文后面的括号里写着Development Language。这代表设置成功了。