消失的它:揭开 CoreData 托管对象神秘的消失之谜(下)

概述

使用 CoreData 作为 App 持久存储“定海神针”的小伙伴们想必都知道,我们需要将耗时的数据库查询操作乖巧的放到后台线程中,以便让主线程负责的 UI 获得风驰电掣般地享受。

不过,如何将后台线程中查询获得的托管对象稳妥的传送至主线程中,这却是一个问题。稍有不慎,原本“老实本分的”托管对象可能会立即消失的无影无踪,让一切前功尽弃。

在本篇博文中,您将学到如下内容:

  1. ”强行续命“:一次失败的尝试
  2. 又一次失败的尝试
  3. 披荆斩棘:破解消失之谜
  4. 另一种解决之道

相信学完本系列课程后,小伙伴们倘若再遇到类似的问题都会胸有成竹,迎刃而解。

那还等什么呢?让我们马上开始探案大冒险吧,Let‘s go!!!;)


3. ”强行续命“:一次失败的尝试

我们从上篇中的讨论可知,一种解决思路是延长 traces 对象的生命周期。在 Swift 语言里这可以通过专门的 withExtendedLifetime 函数来优雅的解决:

顾名思义,withExtendedLifetime 函数专门用来延长指定对象实例宝贵的生存时长,毕竟“好死不如赖活着”:

container.performBackgroundTask { bgContext in
    let traces = try! counter.querySortedTraces(30, context: bgContext)

    withExtendedLifetime(traces) {
        DispatchQueue.main.async {
            countTraces = traces
        }
    }
}

再次执行代码,遗憾的是这种方法的结果和之前如出一辙,traces 对象们依然被奇怪的“杀死”了。

4. 又一次失败的尝试

回到之前的代码里,小伙伴们注意到我们是在主线程中异步将 traces 的内容赋给视图中 countTraces 属性的。

一个合理的猜测是:正是由于异步操作导致 traces 对象被提前释放了。

所以,另一个想法自然应运而生,我们将异步换为同步操作不就大功告成了么:

container.performBackgroundTask { bgContext in
    let traces = try! counter.querySortedTraces(30, context: bgContext)
    
    print("#1: \(traces.map {$0.count})")
    // 使用同步操作在主线程里更新 countTraces 的内容
    DispatchQueue.main.sync {
        print("#2: \(traces.map {$0.count})")
        countTraces = traces
        print("#3: \(countTraces.map {$0.count})")
    }
}

当大家信心满满的预览上述代码的运行结果时,请注意我们即将再次被啪啪打脸:

从上图可以看到:我们图表中的 count 计数仍然还是“可耻”的呈一条直线显示,但奇怪的是,Xcode 底部调试控制台中 3 次输出的结果都是正确的。

这到底是“肿”么回事呢?

5. 披荆斩棘:破解消失之谜

原来在 SwiftUI 中,CoreData 托管对象得以生存的根本条件是其所属的上下文对象还活着。

换句话说,如果上下文对象自己先行“驾鹤西去”,那么它其中包含的所有托管对象也会立即“一命归西”。

回到上面两种失败的解决方法中去,我们可以看到:

  • 第一种方法虽然延长了 traces 本身的寿命,但其背后的 bgContext 后台上下文对象却会在 container.performBackgroundTask 闭包执行结束后烟消云散;
  • 第二种方法与此类似,我们同样无法改变 bgContext 本身在 Chart 显示对应 CountTrace 对象时早已死去这一事实;

所以,综上所述,要想确保计数在图表中被正确显示,我们要将所需的数据单独保存下来。这样无论托管上下文对象“是死是活”,我们都无需再看它的脸色了:

@State private var countsData = [(date: Date, count: Int32)]()

Chart {
    ForEach(Array(countsData.enumerated()), id: \.offset) { i, trace in
        PointMark(x: .value("索引", i), y: .value("计数", trace.count))
    }
    
    ForEach(Array(countsData.enumerated()), id: \.offset) { i, trace in
        LineMark(x: .value("索引", i), y: .value("计数", trace.count))
            .foregroundStyle(.blue.opacity(0.33))
    }
}

let container = Model.shared.controller.container
container.performBackgroundTask { bgContext in
        
    let traces = try! counter.querySortedTraces(30, context: bgContext)
    
    let data = traces.map {
        ($0.date!, $0.count)
    }
    
    DispatchQueue.main.async {
        countsData = data
    }
}

在上面的代码中,我们在原有基础上主要做了以下几点改变:

  1. 使用 countsData 元组数组属性,而不是之前的 CountTrace 数组来存放计数记录;
  2. 在后台线程查询到所有 traces 记录后,将其转换为 data 元组数组;
  3. 最后回到主线程里更新 countsData 属性的内容;

运行代码,不出所料整个世界变得清净和纯粹了:

6. 另一种解决之道

看完上面的解决方案之后,有的小伙伴可能觉得实现托管对象到对应元组的转换有点麻烦,能不能依然直接使用 CountTrace 托管对象来保存图表所需的绘制数据呢?

答案是肯定的!同时解决办法也非常简单:我们只需保证那个后台上下文对象不死即可。

首先,在视图中新建一个属性来保存后台上下文对象:

@State private var bgContext: NSManagedObjectContext?

接着,在 container.performBackgroundTask 闭包执行时将后台托管上下文的引用赋予 bgContext 属性:

let container = Model.shared.controller.container
container.performBackgroundTask { bgContext in
    self.bgContext = bgContext    
    // 其它代码从略...
}

这种方法的运行结果自然也是 OK 的。

不过不是很推荐这种方法,因为它会强行在视图中保留一个后台上下文对象,而且这些上下文中的所有托管对象都会活的很长很幸福,但是它们造成的内存压力可就让我们这些秃头码农们不是那么幸福了。

至此,我们已经完全解开了文章开头那个“离奇古怪”的谜团,棒棒哒!💯

总结

在本篇博文中,我们揭开了 SwiftUI 托管对象“离奇失踪”这一迷案,并最终给出完美的解决方案。

感谢观赏,再会啦!8-)

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容