Swift 单例中的线程安全问题

Share

单例是常见的一种设计模式。最近在编写单例代码的时候,发现公司很多同事的 Swift 单例写法都是这样的,

extension NSObject {
    @discardableResult
    static func kep_synchronized<T>(_ lock: AnyObject, closure: () -> T) -> T {
        objc_sync_enter(lock)
        defer { objc_sync_exit(lock) }

        return closure()
    }
}

class DefaultDict: NSObject {
    private static var manager: DefaultDict?
    static var sharedManager: DefaultDict {
        get {
            var newShared = manager
            kep_synchronized(self) {
                if newShared == nil {
                    newShared = DefaultDict()
                    manager = newShared
                }
            }
            return newShared!
        }
    }
}

先是对 NSObject 进行了拓展,加了个锁方法(应该是互斥锁)。随后在单例类里面加了两个属性,一个是实际上的单例属性(初始是nil),另一个是只提供 get 方法的只读属性,并且在这个只读属性里面加上了锁。

一开始我以为这个锁是为了线程安全加上的,但后来研究了一下发现这个锁其实是没有必要的。

下面是实验代码(注意不要在 Playground 里面试验,直接新建工程,把代码写在 viewDidAppear里面):

extension NSObject {
    @discardableResult
    static func kep_synchronized<T>(_ lock: AnyObject, closure: () -> T) -> T {
        objc_sync_enter(lock)
        defer { objc_sync_exit(lock) }

        return closure()
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let count = 10000

        for index in 0..<count {
            DefaultDict.sharedManager.set(value: index, key: String(index))
        }

        DispatchQueue.concurrentPerform(iterations: count) { index in
            print("value will read with index \(String(index))")
            if let n = DefaultDict.sharedManager.object(key: String(index)) as? Int {
                print("value read with index \(String(n))")
            }
        }

        DefaultDict.sharedManager.reset()

        DispatchQueue.concurrentPerform(iterations: count) { index in
            print("value will set with index \(String(index))")
            DefaultDict.sharedManager.set(value: index,key: String(index))
            print("value set with index \(String(index))")
        }
    }
}

对类 DefaultDict 的写法分为以下几种:

  1. 直接声明一个静态常量属性存储单例(static let),对单例属性 dict 的处理不放在串行队列里面
class DefaultDict {
    private var dict:[String: Any] = [:]
    public static let sharedManager = DefaultDict()
    private init() {
        
    }
    
    public func set(value: Any, key: String) {
        dict[key] = value
    }
    
    public func object(key: String) -> Any? {
        dict[key]
    }
    
    public func reset() {
        print("reset")
        dict.removeAll()
    }
}
  1. 使用我司常见的单例加锁初始化方式,但同样不把对属性 dict 的处理放串行队列里面
class DefaultDict: NSObject {
    private var dict:[String: Any] = [:]
    private static var manager: DefaultDict?
    static var sharedManager: DefaultDict {
        get {
            var newShared = manager
            kep_synchronized(self) {
                if newShared == nil {
                    newShared = DefaultDict()
                    manager = newShared
                }
            }
            return newShared!
        }
    }
    
    override init() {
        super.init()
    }
    
    public func set(value: Any, key: String) {
        dict[key] = value
    }
    
    public func object(key: String) -> Any? {
        dict[key]
    }
    
    public func reset() {
        print("reset")
        dict.removeAll()
    }
}
  1. 使用我司常见的单例加锁初始化方式,但把对属性 dict 的处理放互斥锁里面
class DefaultDict: NSObject {
    private var dict:[String: Any] = [:]
    private static var manager: DefaultDict?
    static var sharedManager: DefaultDict {
        get {
            var newShared = manager
            kep_synchronized(self) {
                if newShared == nil {
                    newShared = DefaultDict()
                    manager = newShared
                }
            }
            return newShared!
        }
    }
    
    override init() {
        super.init()
    }
    
    public func set(value: Any, key: String) {
        Self.kep_synchronized(self) {
            self.dict[key] = value
        }
    }
    
    public func object(key: String) -> Any? {
        var result: Any?
        Self.kep_synchronized(self) {
            result = self.dict[key]
        }
        return result
    }
    
    public func reset() {
        print("reset")
        Self.kep_synchronized(self) {
            self.dict.removeAll()
        }
    }
}
  1. 直接声明一个静态常量属性存储单例(static let),把对单例属性 dict 的处理放在串行队列里面
class DefaultDict: NSObject {
    private var dict:[String: Any] = [:]
    
    private let serialQueue = DispatchQueue(label: "serialQueue")
    
    static var sharedManager =  DefaultDict()
    
    override init() {
        super.init()
    }
    
    public func set(value: Any, key: String) {
        serialQueue.sync {
            self.dict[key] = value
        }
    }
    
    public func object(key: String) -> Any? {
        var result: Any?
        serialQueue.sync {
            result = self.dict[key]
        }
        return result
    }
    
    public func reset() {
        print("reset")
        serialQueue.sync {
            self.dict.removeAll()
        }
    }
}

上面几种不同写法的运行结果是:

  1. 互斥锁和串行队列都不用。崩溃
  2. 用互斥锁,单例写法为我司的 get 和 optional 结合的方式
    1. 只在获取单例全局属性的地方,也就是 sharedManager 加上互斥锁。会出现崩溃
    2. 在改变属性值的地方也加上互斥锁。不会崩溃
  3. 用串行队列同步任务,单例写法为直接赋值。不会崩溃
  4. 单例获取的地方用互斥锁(get 和 optional 结合),改变值的地方用串行队列。不会崩溃

所以,在获取单例静态变量的地方加锁并不能保证他是线程安全的,只能保证多个线程获取到的单例静态变量是同一个。但是这种写法应该是多余的。Swift 中 let 声明的变量都是线程安全的,所以直接用static let sharedManager = DefaultDict()这种方式声明单例就可以了。

如果是为了懒加载,而使用我司那种 get 和 optional 结合的方式声明单例的话,那更没必要了。因为 Swift 中的全局常量和变量都是懒加载的

Global constants and variables are always computed lazily, in a similar manner to Lazy Stored Properties. Unlike lazy stored properties, global constants and variables don’t need to be marked with the lazy modifier.

想要保证对某个属性值的操作是线程安全的,就只能对这些操作加锁或者放到串行队列同步任务里面

Read more

在 AI 时代,做 iOS 逆向分析有哪些高效手段

在 AI 时代,做 iOS 逆向分析有哪些高效手段

AI 发展如此迅猛,传统的代码手艺活已经被 AI 取代了很大一部分。除了生成代码外,AI 在其他的研发流程也能提升很多效率,例如这次要说的 AI 逆向分析。 在手工时代,我们逆向分析一个 iOS app,通常是使用 Hopper 或者 IDA 等软件查看解密的 Mach-O,然后利用软件提供的能力加上人工去分析逻辑流程。这个过程还要求我们懂一些基础的汇编语法。有时候还要去猜测哪些类哪些方法是负责某个逻辑,靠试验去验证。 现在来到了大模型时代,AI 不仅能自己通过一些简单的工具去分析 ipa 和 Mach-O ,也能使用更准更稳的 MCP、Skill 工具来做深度的分析。 Hopper MCP Hopper 在新版本支持了 MCP Server。 把 framework、Mach-O 或者解密后的 IPA 直接丢给它,

By Gray
Cinder:基于 Swift Macro 与 Mach-O Section 的解耦注册机制

Cinder:基于 Swift Macro 与 Mach-O Section 的解耦注册机制

Cinder(Github) 是一个无需逐模块启动注册、支持字符串、类型和函数调用的 Swift 编译期注册框架。 Cinder 是什么 在 iOS app 里借助 Mach-O 注册能力做一些二进制的插桩操作并不罕见。以往这些操作大都使用 objc runtime ,借助 __attribute__((used)) 和 __attribute__((section("name"))) 实现。在 Swift 上实现有一些难度,因为 Swift runtime 能力弱,以及早期的版本没有类似 objc 的 __attribute__ 能力。 * 在 Swift 5.10 版本,Apple 提供了 @_used 和

By Gray