如何在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




