文本比较、排序与数据迁移的优化策略
在数据处理过程中,文本比较、排序以及数据迁移是常见的操作。下面将详细介绍这些操作的优化方法和具体实现。
文本比较与排序
在处理文本数据时,比较和排序是常见需求。通过优化这些操作,可以显著提升应用的性能。
文本比较优化
通过为
name_normalized
属性添加索引,可直接利用 SQLite 进行比较,从而加快搜索速度。对于约 4000 个城市的列表,搜索速度比原始版本快 10 到 15 倍,且数据集越大,提速效果越明显。
文本排序策略
在某些情况下,需要按文本属性对对象进行排序。此时,应使用支持 Unicode 且特定于区域设置的排序规则。不过,Foundation 框架进行字符串排序是一项开销较大的操作,且必须在内存中执行,SQLite 无法提供帮助。因此,在选择排序方法时,需要考虑数据集大小和数据变化频率。
-
简单排序方法
:在通过 fetch 请求从 Core Data 请求项目时,可使用
NSSortDescriptor让 SQLite 进行排序。例如:
let sd = NSSortDescriptor(key: #keyPath(City.name), ascending: true)
request.sortDescriptors = [sd]
但这种方法按编码的字节值排序,可能不符合国际用户的期望。
- 内存排序 :对于少量对象,可在内存中进行排序。例如:
cities.sort { (cityA, cityB) -> Bool in
cityA.name.localizedStandardCompare(cityB.name) == .orderedAscending
}
不过,这种排序方式开销较大,对约 4000 个城市进行排序大约需要 300 毫秒。为避免每次访问城市集合时都重新排序,可将排序后的数组始终保存在内存中,但需要确保该数组与持久存储保持同步。
-
更新排序数组
:当城市数组发生变化或城市名称更改时,需要重新排序。可通过监听
.NSManagedObjectContextObjectsDidChange通知来检测变化,并根据插入、更新或刷新的对象判断是否需要重新排序。示例代码如下:
let hasInsertedOrRefreshedCities = { () -> Bool in
let isCity: (NSManagedObject) -> Bool = { $0.entity === City.entity }
return note.refreshedObjects.contains(where: isCity)
|| note.insertedObjects.contains(where: isCity)
}()
let hasCitiesWithUpdatedName = { () -> Bool in
let nameKey = #keyPath(City.name)
return note.updatedObjects.contains { object in
let isCity = object.entity === City.entity
let updatedName = object.changedValuesForCurrentEvent()[nameKey] != nil
return isCity && updatedName
}
}()
为提高更新缓存的速度,可利用
NSArray
的内置功能,如使用二分查找确定新对象的插入位置,以及通过记录提示对象来加速重新排序。示例代码如下:
class CitiesSortedByName {
var hint: Data?
var sortedCities: NSArray?
let comparator: Comparator = { cityA, cityB in
guard let cityA = cityA as? City
else { fatalError("Object is not a 'City'") }
guard let cityB = cityB as? City
else { fatalError("Object is not a 'City'") }
return cityA.name.localizedStandardCompare(cityB.name)
}
typealias CComparator =
@convention(c) (AnyObject, AnyObject, UnsafeMutableRawPointer) -> Int
let cComparator: CComparator = { (cityA, cityB, _) in
guard let cityA = cityA as? City
else { fatalError("Object is not a 'City'") }
guard let cityB = cityB as? City
else { fatalError("Object is not a 'City'") }
let r = cityA.name.localizedStandardCompare(cityB.name)
return r.rawValue
}
func didInsertCities(_ inserted: Set<City>) {
guard let sorted = sortedCities else { return }
let mutableSorted = NSMutableArray(array: sorted)
for city in inserted {
let range = NSMakeRange(0, mutableSorted.count)
let index = mutableSorted.index(of: city, inSortedRange: range,
options: .insertionIndex, usingComparator: comparator)
mutableSorted.insert(city, at: index)
}
sortedCities = mutableSorted
}
func didChangeCityNames() {
guard let oldArray = sortedCities else { return }
if let hint = hint {
sortedCities = oldArray.sortedArray(cComparator as!
@convention(c) (Any, Any, UnsafeMutableRawPointer?) -> Int,
context: nil, hint: hint) as NSArray?
} else {
sortedCities = oldArray.sortedArray(cComparator as!
@convention(c) (Any, Any, UnsafeMutableRawPointer?) -> Int,
context: nil) as NSArray?
}
hint = sortedCities!.sortedArrayHint
}
}
-
持久化排序数组
:由于本地化字符串排序开销较大,可考虑持久化排序后的数组。但不同版本的 Foundation 框架可能导致排序结果不同,且用户切换区域设置时,数组需要重新排序。持久化排序列表的简单方法是创建一个具有有序多对多关系的所有者实体。例如,创建一个
SortedCityOwner实体,它与City具有有序多对多关系。这样,插入和更新城市的成本会增加,但启动后检索排序后的城市列表会更快。
数据迁移
随着应用的发展,数据模型可能需要适应新的需求,这就涉及到数据迁移。
模型版本管理
在应用开发过程中,最初可能只使用一个数据模型版本。但当应用投入生产后进行数据模型更改时,需要创建新的模型版本。在 Xcode 中,可通过打开
.xcdatamodel
文件,选择
Editor > Add Model …Version
来创建新的模型版本,并指定版本名称和基础数据模型。
Core Data 的数据模型文件
.xcdatamodeld
是一个包,可包含多个代表不同版本数据模型的
.xcdatamodel
包。编译应用时,数据模型会被编译成
.momd
包,其中包含每个模型版本的
.mom
文件以及当前模型版本的优化文件变体
.omo
。此外,
.momd
包还包含
VersionInfo.plist
文件,其中指定了当前模型版本并包含所有实体的版本哈希值。Core Data 依靠这些哈希值来确定存储文件与托管对象模型之间的兼容性。
当模型更改不影响 SQLite 数据库结构时,可能需要为属性或实体指定唯一的哈希修改器,以确保 Core Data 认为数据模型已更改。
迁移过程
定义新的模型版本后,需要考虑如何将数据从旧模型迁移到新模型。迁移通过映射模型来定义旧模型与新模型之间的映射关系,描述哪些实体或属性应被复制、重命名、转换等。创建映射模型有两种基本方法:轻量级迁移和自定义映射模型。
-
自动迁移
:在以下情况下,Core Data 会默认尝试自动迁移:
-
使用
NSPersistentContainer设置堆栈。 -
调用协调器的
addPersistentStore方法并传入NSPersistentStoreDescription。
若通过调用协调器的addPersistentStore(ofType:...)方法添加持久存储,需要在选项字典中将NSMigratePersistentStoresAutomaticallyOption设置为true来指示 Core Data 执行自动迁移。若同时将NSInferMappingModelAutomaticallyOption设置为true,Core Data 在找不到映射模型时会尝试推断一个。新的NSPersistentStoreDescriptionAPI 提供了shouldMigrateStoreAutomatically和shouldInferMappingModelAutomatically两个标志,默认均为true。
-
使用
自动迁移虽然简单,但存在一些局限性。它总是从旧模型版本到新模型版本一步完成迁移,需要为所有可能的组合提供映射模型,扩展性不佳,且无法独立迁移数据子集。
- 手动迁移 :为获得更多灵活性,可手动控制迁移过程。通过迭代将现有存储从当前版本转换为后续模型版本,实现渐进式迁移。这种方法在模型频繁更改时扩展性更好。
为了更清晰地管理模型版本,可定义一个枚举来列出所有模型版本,并为其添加符合
ModelVersion
协议的辅助功能。例如:
enum Version: String {
case version1 = "Moody"
case version2 = "Moody 2"
}
extension Version: ModelVersion {
static var all: [Version] { return [.version2, .version1] }
static var current: Version { return .version2 }
var name: String { return rawValue }
var modelBundle: Bundle { return Bundle(for: Mood.self) }
var modelDirectoryName: String { return "Moody.momd" }
var successor: Version? {
switch self {
case .version1: return .version2
default: return nil
}
}
}
通过扩展
ModelVersion
协议,可实现获取映射模型和迁移步骤的方法。例如:
extension ModelVersion {
public func mappingModelsToSuccessor() -> [NSMappingModel]? {
guard let nextVersion = successor else { return nil }
guard let mapping = NSMappingModel(from: [modelBundle],
forSourceModel: managedObjectModel(),
destinationModel: nextVersion.managedObjectModel())
else { fatalError("no mapping from \(self) to \(nextVersion)") }
return [mapping]
}
public func migrationSteps(to version: Self) -> [MigrationStep] {
guard self != version else { return [] }
guard let mappings = mappingModelsToSuccessor(),
let nextVersion = successor else
{
fatalError("couldn't find mapping models")
}
let step = MigrationStep(source: managedObjectModel(),
destination: nextVersion.managedObjectModel(),
mappings: mappings)
return [step] + nextVersion.migrationSteps(to: version)
}
}
最后,可编写一个迁移函数来执行迁移操作:
public func migrateStore<Version: ModelVersion>(
from sourceURL: URL, to targetURL: URL, targetVersion: Version,
deleteSource: Bool = false)
{
guard let sourceVersion = Version(storeURL: sourceURL as URL) else {
fatalError("unknown store version at URL \(sourceURL)")
}
var currentURL = sourceURL
let migrationSteps = sourceVersion.migrationSteps(to: targetVersion)
for step in migrationSteps {
let manager = NSMigrationManager(sourceModel: step.source,
destinationModel: step.destination)
let destinationURL = URL.temporary
for mapping in step.mappings {
try! manager.migrateStore(from: currentURL,
sourceType: NSSQLiteStoreType, options: nil,
with: mapping, toDestinationURL: destinationURL,
destinationType: NSSQLiteStoreType, destinationOptions: nil)
}
if currentURL != sourceURL {
NSPersistentStoreCoordinator.destroyStore(at: currentURL)
}
currentURL = destinationURL
}
try! NSPersistentStoreCoordinator.replaceStore(at: targetURL,
withStoreAt: currentURL)
if (currentURL != sourceURL) {
NSPersistentStoreCoordinator.destroyStore(at: currentURL)
}
if (targetURL != sourceURL && deleteSource) {
NSPersistentStoreCoordinator.destroyStore(at: sourceURL)
}
}
在打开持久存储时,可使用该迁移函数。例如,当
NSPersistentContainer
的
loadPersistentStores
调用失败时,可启动迁移过程:
public func createMoodyContainer(migrating: Bool = false,
progress: Progress? = nil,
completion: @escaping (NSPersistentContainer) -> ())
{
moodyContainer.loadPersistentStores { _, error in
if error == nil {
DispatchQueue.main.async { completion(moodyContainer) }
} else {
guard !migrating else { fatalError("was unable to migrate store") }
DispatchQueue.global(qos: .userInitiated).async {
migrateStore(from: storeURL, to: storeURL,
targetVersion: MoodyModelVersion.current,
deleteSource: true,
progress: progress)
createMoodyContainer(migrating: true, progress: progress,
completion: completion)
}
}
}
}
总结
文本比较、排序和数据迁移是数据处理中的重要操作。在处理这些操作时,需要根据具体需求和场景选择合适的方法。对于文本比较和排序,可通过添加索引、优化排序算法和持久化排序数组等方法提高性能。对于数据迁移,需要合理管理模型版本,并根据迁移复杂度选择自动迁移或手动迁移。
关键要点
- 始终对用户可搜索的文本使用规范化字符串。
- 仅对规范化字符串属性添加索引。
-
比较规范化字符串时使用
[n]修饰符。 - 对面向用户的文本进行排序开销较大。
操作流程总结
文本排序操作流程
| 步骤 | 操作 | 代码示例 |
|---|---|---|
| 1 |
使用
NSSortDescriptor
让 SQLite 排序
|
let sd = NSSortDescriptor(key: #keyPath(City.name), ascending: true); request.sortDescriptors = [sd]
|
| 2 | 内存排序 |
cities.sort { (cityA, cityB) -> Bool in cityA.name.localizedStandardCompare(cityB.name) == .orderedAscending }
|
| 3 | 更新排序数组 |
监听
.NSManagedObjectContextObjectsDidChange
通知,判断是否需要重新排序
|
| 4 | 持久化排序数组 | 创建具有有序多对多关系的所有者实体 |
数据迁移操作流程
| 步骤 | 操作 | 代码示例 |
|---|---|---|
| 1 | 创建新的模型版本 |
在 Xcode 中选择
Editor > Add Model …Version
|
| 2 | 自动迁移 |
设置
NSMigratePersistentStoresAutomaticallyOption
和
NSInferMappingModelAutomaticallyOption
为
true
|
| 3 | 手动迁移 | 定义枚举列出所有模型版本,实现迁移方法和迁移函数 |
流程图
graph TD
A[开始] --> B{是否需要文本排序}
B -- 是 --> C[选择排序方法]
C --> D{是否小数据集}
D -- 是 --> E[内存排序]
D -- 否 --> F[使用 NSSortDescriptor 排序]
B -- 否 --> G{是否需要数据迁移}
G -- 是 --> H[创建新模型版本]
H --> I{选择迁移方式}
I -- 自动迁移 --> J[设置迁移选项]
I -- 手动迁移 --> K[定义模型版本枚举]
K --> L[实现迁移方法和函数]
G -- 否 --> M[结束]
通过以上方法和流程,可有效优化文本比较、排序和数据迁移操作,提升应用的性能和可维护性。
文本比较、排序与数据迁移的优化策略
文本排序与数据迁移的性能考量
在实际应用中,文本排序和数据迁移的性能表现至关重要。以下将进一步探讨它们在不同场景下的性能特点及应对策略。
文本排序性能分析
不同的文本排序方法在性能上存在显著差异。简单排序方法虽然实现简单,但对于国际用户可能无法提供符合预期的排序结果。内存排序虽然能满足特定需求,但开销较大,尤其是在处理大量数据时。
为了更直观地了解不同排序方法的性能,我们可以进行一个简单的性能测试。以下是一个测试不同排序方法处理 4000 个城市数据所需时间的示例代码:
import Foundation
// 模拟 4000 个城市数据
let cities = (0..<4000).map { _ in
let randomName = UUID().uuidString
return City(name: randomName)
}
// 使用 NSSortDescriptor 排序
let request = NSFetchRequest<City>()
let sd = NSSortDescriptor(key: #keyPath(City.name), ascending: true)
request.sortDescriptors = [sd]
let start1 = Date()
// 模拟执行排序操作
let _ = try? cities.sorted(using: [sd])
let end1 = Date()
let time1 = end1.timeIntervalSince(start1)
// 内存排序
let start2 = Date()
let sortedCities = cities.sorted { (cityA, cityB) -> Bool in
cityA.name.localizedStandardCompare(cityB.name) == .orderedAscending
}
let end2 = Date()
let time2 = end2.timeIntervalSince(start2)
print("NSSortDescriptor 排序时间: \(time1) 秒")
print("内存排序时间: \(time2) 秒")
通过这个测试,我们可以清晰地看到不同排序方法的性能差异。在实际应用中,应根据数据量和用户需求选择合适的排序方法。
数据迁移性能分析
数据迁移的性能同样受到多种因素的影响,如迁移方式、数据量大小等。自动迁移虽然简单,但在处理复杂的模型变更和大量数据时可能会遇到性能瓶颈。手动迁移则可以通过渐进式迁移的方式,逐步处理数据,提高迁移的灵活性和性能。
为了优化数据迁移的性能,我们可以采取以下策略:
-
分批迁移
:对于大量数据,可将数据分成多个批次进行迁移,减少单次迁移的数据量,降低内存压力。
-
异步迁移
:在后台线程中执行迁移操作,避免阻塞主线程,提高用户体验。
-
优化映射模型
:合理设计映射模型,减少不必要的属性复制和转换,提高迁移效率。
实际应用案例
以下是一个结合文本排序和数据迁移的实际应用案例,帮助我们更好地理解这些技术在实际场景中的应用。
假设我们正在开发一个旅游应用,需要管理城市信息。随着应用的发展,我们可能需要对城市数据进行排序和迁移。
文本排序应用
在应用中,用户可能希望按照城市名称对城市列表进行排序。我们可以采用前面介绍的排序方法,根据数据量和用户需求选择合适的排序方式。例如,当数据量较小时,可使用内存排序;当数据量较大时,可使用
NSSortDescriptor
让 SQLite 进行排序。
// 内存排序示例
let smallCities = (0..<100).map { _ in
let randomName = UUID().uuidString
return City(name: randomName)
}
let sortedSmallCities = smallCities.sorted { (cityA, cityB) -> Bool in
cityA.name.localizedStandardCompare(cityB.name) == .orderedAscending
}
// NSSortDescriptor 排序示例
let largeCities = (0..<4000).map { _ in
let randomName = UUID().uuidString
return City(name: randomName)
}
let largeRequest = NSFetchRequest<City>()
let largeSd = NSSortDescriptor(key: #keyPath(City.name), ascending: true)
largeRequest.sortDescriptors = [largeSd]
let sortedLargeCities = try? largeCities.sorted(using: [largeSd])
数据迁移应用
随着应用功能的不断增加,城市数据模型可能需要进行更新。例如,我们可能需要添加新的属性来描述城市的特色景点。这时,就需要进行数据迁移。
// 创建新的模型版本
// 在 Xcode 中选择 Editor > Add Model …Version
// 手动迁移示例
enum CityModelVersion: String {
case version1 = "CityModelV1"
case version2 = "CityModelV2"
}
extension CityModelVersion: ModelVersion {
static var all: [CityModelVersion] { return [.version2, .version1] }
static var current: CityModelVersion { return .version2 }
var name: String { return rawValue }
var modelBundle: Bundle { return Bundle(for: City.self) }
var modelDirectoryName: String { return "CityModel.momd" }
var successor: CityModelVersion? {
switch self {
case .version1: return .version2
default: return nil
}
}
}
// 迁移函数
public func migrateCityStore(
from sourceURL: URL, to targetURL: URL, targetVersion: CityModelVersion,
deleteSource: Bool = false
) {
guard let sourceVersion = CityModelVersion(storeURL: sourceURL as URL) else {
fatalError("unknown store version at URL \(sourceURL)")
}
var currentURL = sourceURL
let migrationSteps = sourceVersion.migrationSteps(to: targetVersion)
for step in migrationSteps {
let manager = NSMigrationManager(sourceModel: step.source,
destinationModel: step.destination)
let destinationURL = URL.temporary
for mapping in step.mappings {
try! manager.migrateStore(from: currentURL,
sourceType: NSSQLiteStoreType, options: nil,
with: mapping, toDestinationURL: destinationURL,
destinationType: NSSQLiteStoreType, destinationOptions: nil)
}
if currentURL != sourceURL {
NSPersistentStoreCoordinator.destroyStore(at: currentURL)
}
currentURL = destinationURL
}
try! NSPersistentStoreCoordinator.replaceStore(at: targetURL,
withStoreAt: currentURL)
if (currentURL != sourceURL) {
NSPersistentStoreCoordinator.destroyStore(at: currentURL)
}
if (targetURL != sourceURL && deleteSource) {
NSPersistentStoreCoordinator.destroyStore(at: sourceURL)
}
}
总结与展望
文本比较、排序和数据迁移是数据处理中不可或缺的环节。通过合理选择排序方法和迁移方式,我们可以提高应用的性能和可维护性。在实际应用中,应根据具体需求和场景,灵活运用这些技术,不断优化应用的性能。
未来,随着数据量的不断增加和用户需求的不断变化,文本排序和数据迁移技术也将不断发展。例如,可能会出现更高效的排序算法和更智能的迁移工具,帮助开发者更轻松地处理复杂的数据操作。
操作流程补充
文本排序性能优化流程
| 步骤 | 操作 | 代码示例 |
|---|---|---|
| 1 | 评估数据量 | 根据实际数据量判断大小 |
| 2 | 选择排序方法 |
数据量小使用内存排序,数据量大使用
NSSortDescriptor
排序
|
| 3 | 性能测试 | 编写测试代码,记录不同排序方法的时间 |
| 4 | 优化排序 | 根据测试结果,选择最优排序方法 |
数据迁移性能优化流程
| 步骤 | 操作 | 代码示例 |
|---|---|---|
| 1 | 分析数据量和变更复杂度 | 判断数据量大小和模型变更复杂度 |
| 2 | 选择迁移方式 | 简单变更和小数据量可使用自动迁移,复杂变更和大数据量使用手动迁移 |
| 3 | 优化迁移策略 | 采用分批迁移、异步迁移、优化映射模型等策略 |
| 4 | 测试迁移性能 | 编写测试代码,记录迁移时间和内存使用情况 |
流程图补充
graph TD
A[开始] --> B{是否需要文本排序}
B -- 是 --> C[评估数据量]
C -- 小数据量 --> D[内存排序]
C -- 大数据量 --> E[使用 NSSortDescriptor 排序]
D --> F[性能测试]
E --> F
F --> G{是否满足性能要求}
G -- 是 --> H{是否需要数据迁移}
G -- 否 --> I[优化排序方法]
I --> F
B -- 否 --> H
H -- 是 --> J[分析数据量和变更复杂度]
J -- 简单变更和小数据量 --> K[自动迁移]
J -- 复杂变更和大数据量 --> L[手动迁移]
K --> M[优化迁移策略]
L --> M
M --> N[测试迁移性能]
N --> O{是否满足性能要求}
O -- 是 --> P[结束]
O -- 否 --> Q[调整迁移策略]
Q --> M
H -- 否 --> P
通过以上的分析和实践,我们可以更好地掌握文本排序和数据迁移的技术,提高应用的性能和用户体验。在实际开发中,应根据具体情况灵活运用这些方法,不断优化应用的性能。
超级会员免费看
1287

被折叠的 条评论
为什么被折叠?



