基于CloudKit与NSPersistentCloudKitContainer获取公有库记录创建者CKRecord
CoreData+CloudKit 用户信息获取优化问题
背景
- 使用
NSPersistentCloudKitContainer管理私有/公有CloudKit数据库,CoreData模型包含Local(对应私有库)、Cloud(对应公有库)两个配置,均包含List、ListItem、Like三类对象 - List与ListItem为一对多关联(含反向关联),ListItem与Like为一对多关联(含反向关联),私有数据库功能运行正常
- 用户可通过
isPublic属性设置List/ListItem的公开状态:设为true时在公有库创建对应对象副本;设为false时将公有库中对应对象的isTrashed设为true(暂未确定实际删除时机) - 通过
NSFetchedResultsController加载并监控公有库中的所有ListItem
问题
当前通过UITableView+NSFetchedResultsController展示公有库ListItem时,每个自定义UITableViewCell通过以下步骤获取并展示创建者用户名:
- 调用
NSPersistentCloudKitContainer.record(for:)获取NSManagedObject的creatorUserRecordID - 调用
CKContainer.default().publicCloudDatabase.record(for:)获取该ID对应的User CKRecord - 读取User CKRecord的
username字段
存在以下疑问:
- 这种每个Cell单独发起异步请求的方式是否合理?若合理,是否需要实现缓存机制?
- 异步请求失败会导致用户名空白,是否需要提前预取NSFetchedResultsController中所有对象的用户名?但预取数千未展示对象会大幅增加加载时间
- 有没有更优的方式获取CKRecord创建者信息?是否应该创建自定义User NSManagedObject?
- 还需展示每个点赞用户的用户名,这会导致海量API请求:
(1 + x) * y(x为单条ListItem的点赞数,y为ListItem总数),例如展示100个平均20赞的ListItem会产生2100次请求,远超CloudKit 40请求/秒的限制 - 目前认为最优方案是在CoreData模型中创建自定义User对象,利用
NSPersistentCloudKitContainer自动关联关系的特性,是否正确?
示例代码(UITableViewCell配置)
public func configure(listItem: ListItem, environment: Environment) { self.listItem = listItem self.environment = environment self.userRecordId = self.environment?.coreDataStack.creatorUserRecordID(for: listItem) self.fetchUserTask?.cancel() self.fetchUserTask = Task { guard let userRecordId = self.userRecordId, let userRecord = try? await CKContainer.default().publicCloudDatabase.record(for: userRecordId) else { self.userNameLabel.text = "I'm server poor. You can't view this information." return } if !Task.isCancelled { Task { @MainActor in self.userRecord = userRecord self.userNameLabel.text = self.userRecord?["username"] } } } self.contentTextView.text = listItem.title self.dateCreatedLabel.text = DateFormatter.localizedString( from: listItem.modifiedAt!, dateStyle: .medium, timeStyle: .short ) let likes = self.listItem?.likes self.likeButton.setTitle("\(likes?.count ?? 0)", for: .normal) if let likes = likes?.allObjects as? [Like] { for like in likes { guard let userRecordId = self.environment?.coreDataStack.creatorUserRecordID(for: like) else { self.likeButton.isEnabled = false continue } if userRecordId.recordName == CKCurrentUserDefaultName { self.likeButton.isEnabled = false break } } } }
解决方案
1. 当前Cell异步请求方式的合理性与缓存
当前每个Cell单独发起请求的方式不合理,核心问题是请求量过大,极易触发CloudKit的请求频率限制,同时会产生大量重复请求(比如同一用户的多个ListItem被展示时,会重复拉取该用户的User记录)。
必须实现缓存机制,建议采用内存缓存+磁盘缓存结合的方案:
- 内存缓存:用
NSCache<CKRecord.ID, CKRecord>存储已拉取的User记录,快速返回缓存结果 - 磁盘缓存:将User记录持久化到本地CoreData的Local配置中,App重启后无需重新拉取
2. 预取策略优化
不要预取所有未展示对象的用户名,而是采用滚动预取:
- 监听UITableView的滚动事件,当Cell即将进入可视区域时(比如提前3-5个Cell),批量发起请求拉取这些Cell对应的用户信息
- 使用CloudKit的批量查询接口
fetchRecords(withIDs:)代替单个查询,减少请求次数(一次请求最多可拉取100条记录)
3. 最优方案:自定义User NSManagedObject + CloudKit自动关联
这确实是最彻底的解决方案,具体步骤:
- 在CoreData模型的Cloud配置中添加
User实体,设置其CloudKit对应的记录类型为_user(CloudKit内置的用户记录类型) - 给
ListItem和Like实体添加creator关联(一对一,指向User),并在CoreData模型编辑器中开启CloudKit自动关联(勾选"CloudKit"选项中的"Associate Records") NSPersistentCloudKitContainer会自动将ListItem/Like的creatorUserRecordID关联到对应的User实体,无需手动拉取User记录- 给
User实体添加username属性,映射CloudKit User记录中的username字段,直接从CoreData中读取即可,无需额外API请求
4. 点赞用户信息的优化
采用自定义User实体后,Like的creator直接关联到User,展示点赞用户用户名时只需从CoreData中读取关联的User对象即可,完全避免额外API请求。同时可通过CoreData的批量查询一次性获取所有需要的User数据,配合NSFetchedResultsController的批量加载机制,性能大幅提升。
代码优化示例
// 全局用户缓存实现 class UserCache { static let shared = UserCache() private let cache = NSCache<CKRecord.ID, CKRecord>() func user(for recordID: CKRecord.ID) -> CKRecord? { return cache.object(forKey: recordID) } func cacheUser(_ user: CKRecord) { cache.setObject(user, forKey: user.recordID) } // 批量获取用户,使用CloudKit批量接口减少请求次数 func fetchUsers(withIDs recordIDs: [CKRecord.ID]) async throws -> [CKRecord.ID: CKRecord] { let operation = CKFetchRecordsOperation(recordIDs: recordIDs) operation.database = CKContainer.default().publicCloudDatabase let result = try await operation.result() let records = result.records records.values.forEach { cacheUser($0) } return records } } // 修改Cell的configure方法 public func configure(listItem: ListItem, environment: Environment) { self.listItem = listItem self.environment = environment // 优先从缓存获取用户信息 if let userRecordId = environment?.coreDataStack.creatorUserRecordID(for: listItem) { if let cachedUser = UserCache.shared.user(for: userRecordId) { userNameLabel.text = cachedUser["username"] as? String } else { // 发起批量请求(可合并多个Cell的请求进一步优化) fetchUserTask = Task { do { let records = try await UserCache.shared.fetchUsers(withIDs: [userRecordId]) if let userRecord = records[userRecordId], !Task.isCancelled { await MainActor.run { userNameLabel.text = userRecord["username"] as? String } } } catch { await MainActor.run { userNameLabel.text = "无法加载用户信息" } } } } } else { userNameLabel.text = "无法加载用户信息" } // 基础内容配置 contentTextView.text = listItem.title dateCreatedLabel.text = DateFormatter.localizedString( from: listItem.modifiedAt!, dateStyle: .medium, timeStyle: .short ) // 点赞数与状态配置 let likesCount = listItem.likes?.count ?? 0 likeButton.setTitle("\(likesCount)", for: .normal) likeButton.isEnabled = true if let likes = listItem.likes?.allObjects as? [Like] { likeButton.isEnabled = !likes.contains { like in guard let userRecordId = environment?.coreDataStack.creatorUserRecordID(for: like) else { return false } return userRecordId.recordName == CKCurrentUserDefaultName } } }
内容的提问来源于stack exchange,提问作者Jonathan
相关产品推荐
相关产品推荐

