You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在Combine的Future取消时执行自定义代码?实现数据库连接自动关闭

解决Combine中Future取消时执行自定义代码(关闭数据库连接)的问题

你遇到的核心问题是:当前的Future实现里,数据库连接在发送实例给订阅者后立刻被关闭,不管订阅是否取消,这导致订阅者拿到的数据库实例已经不可用了。要解决这个问题,我们需要把数据库连接的生命周期和订阅的生命周期绑定起来——在订阅正常完成或者被取消时,再关闭数据库连接

下面是具体的实现方案,完美匹配你想要的Combine风格重写:

方案1:使用Future + handleEvents

这个方案通过handleEvents监听订阅的取消和完成事件,确保数据库连接在合适的时机关闭:

private func withDatabaseFTSContext() -> AnyPublisher<FMDatabase?, Never> {
    // 持有数据库实例,用于后续关闭操作
    var database: FMDatabase?
    
    return Future<FMDatabase?, Never> { [weak self] promise in
        guard let self = self else {
            promise(.success(nil))
            return
        }
        self.queue.async {
            guard let db = self.database else {
                promise(.success(nil))
                return
            }
            // 初始化数据库连接和分词器
            db.open()
            let simpleTokenizer = FMSimpleTokenizer(locale: nil)
            FMDatabase.registerTokenizer(simpleTokenizer, withKey: "simple")
            db.installTokenizerModule()
            
            database = db
            // 将数据库实例发送给订阅者
            promise(.success(db))
        }
    }
    .handleEvents(
        // 订阅被取消时关闭数据库
        receiveCancel: {
            self.queue.async {
                database?.close()
                database = nil
            }
        },
        // 订阅正常完成时也关闭数据库
        receiveCompletion: { _ in
            self.queue.async {
                database?.close()
                database = nil
            }
        }
    )
    .eraseToAnyPublisher()
}

关键细节说明:

  • 我们用一个捕获的database变量持有打开的连接,这样在后续的事件回调中可以访问它。
  • 所有数据库操作(打开、关闭)都在同一个队列self.queue中执行,保证线程安全(FMDatabase通常不是线程安全的)。
  • receiveCancel会在订阅者调用cancel()时触发,receiveCompletion会在流正常结束时触发,两种情况都会关闭数据库连接,避免资源泄漏。

方案2:结合Deferred实现多订阅隔离

如果你希望每个订阅都创建独立的数据库连接(和原withDatabaseFTSContext方法的行为一致),可以用Deferred包装上面的逻辑,确保每次订阅都会重新初始化连接:

private func withDatabaseFTSContext() -> AnyPublisher<FMDatabase?, Never> {
    return Deferred {
        var database: FMDatabase?
        
        return Future<FMDatabase?, Never> { [weak self] promise in
            guard let self = self else {
                promise(.success(nil))
                return
            }
            self.queue.async {
                guard let db = self.database else {
                    promise(.success(nil))
                    return
                }
                db.open()
                let simpleTokenizer = FMSimpleTokenizer(locale: nil)
                FMDatabase.registerTokenizer(simpleTokenizer, withKey: "simple")
                db.installTokenizerModule()
                
                database = db
                promise(.success(db))
            }
        }
        .handleEvents(
            receiveCancel: {
                self.queue.async {
                    database?.close()
                    database = nil
                }
            },
            receiveCompletion: { _ in
                self.queue.async {
                    database?.close()
                    database = nil
                }
            }
        )
    }
    .eraseToAnyPublisher()
}

Deferred的作用:

Deferred会延迟内部Future的创建,直到有订阅者订阅这个Publisher时才初始化。这样每个订阅都会得到一个全新的数据库连接,不会出现多个订阅共享同一个连接的问题。

使用示例

你可以像这样使用这个Publisher:

let cancellable = withDatabaseFTSContext()
    .sink(receiveValue: { database in
        guard let db = database else { return }
        // 在这里执行数据库操作,比如查询FTS数据
        let results = db.executeQuery("SELECT * FROM ...", withArgumentsIn: [])
        // 处理查询结果
    })

// 当需要取消订阅时(比如页面销毁),调用cancel(),此时数据库会被自动关闭
// cancellable.cancel()

这样实现后,不管是订阅正常完成还是主动取消,数据库连接都会被正确关闭,完美替代了你原来的闭包风格方法。

内容的提问来源于stack exchange,提问作者WildCat

火山引擎 最新活动