先要闲扯几句最近遇到的这个麻烦,算是我以下做数据迁移的原因。想看realm啥的跳过这段吧
事情要追溯到2018年快到年末,因为要加一个小小功能,终于决定要全面测试已经完成一年多的swift版本的代码,并且打算用它来替换Objc那版上线了。老实讲我心里没啥底,毕竟Objc版本的App store上线3年多,发行过几十版,用户量也不小了。突然要换上这个,心里...。而且我没有在Swift版里用coredata,而是用了易于上手的realm,况且,我还没来得及完成数据迁移。新版上线3个月后,用户抱怨,以前的数据怎么都看不见了!!!。不得已,我需要尽快完成coredata->realm的数据迁移,而且,这的是自己坑自己啊。
我的坑体现在两点:
1)Objc版里用了coredata,swift版里用的是realm;
2)我用了相同的类名...
就这第二点是个真正的大坑。
本来事情很简单:
1)把原来objc代码中的coredata model文件:xxx. xcdatamodeld 添加到swift工程中,代码如下:
// 在Class AppDelegate 中添加如下函数:
// MARK: MINE Core Data stack
public func createMainContext() -> NSManagedObjectContext {
let modelUrl = Bundle.main.url(forResource: "myproject", withExtension: "momd")
guard let model = NSManagedObjectModel.init(contentsOf: modelUrl!) else {fatalError("model not found")}
let psc = NSPersistentStoreCoordinator(managedObjectModel: model)
let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = psc
if let dataPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last {
print(dataPath)
let path = dataPath.appending("/myproject.sqlite")
do {
try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: URL(fileURLWithPath: path), options: nil)
} catch {
print(error)
}
}
return managedObjectContext
}
2)launch结束后,找个时机读一下,写到Realm中就好了呀
// 创建一个MergeData的类,单例,使用NSManagedObjectContext直接loaddata就好
class MergeData {
fileprivate var context: NSManagedObjectContext?
static let sharedInstance = MergeData()
fileprivate init() {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
context = appDelegate.createMainContext() //获取托管对象总管
}
}
// Message 是原来objc中coredata的一个model
fileprivate func mergeDataFromCoredata() {
if let messages = loadData("Message") as? [Message] {
messages.forEach({ (message) in
// 把message中的每个字段读出来,对应到realm相应的model字段上,再创建一个新的realm object写到数据库里就好了
})
}
}
}
可是,这下问题来了,上面写『就好了』真的有这么简单么? NONONO
这是第一个要解决的。前面我说了,swift版本的代码里,没有了coredata。那么就不会有coredata数据模型对应的数据结构。也就是,如果coredata里有个model名字是Message,那我就必须按照原来Objc里的,一样创建一个Message结构出来。于是,从objc文件拷贝了原来的Message.h,还得写个Message.m,当然,.m文件除了
#import "Message.h"
@implementation Message
@end
这3句,什么也不用写了。
当然,这就属于混编了,需要在Bridiging-Header.h中加入.h。加上Message类以后,在loadData()后,就能转换出来Message数组了,这些就是保存在手机coredata中的Message了。接下来使用这些读出来的Message对象的key-value,初始化一个新的Realm对象并add到新数据库里。
接下来,又遇到问题了。因为我的Swift里也有一个类名字Message。这就惨了。虽然Objc在runtime时Message肯定不叫Message,Swift里Message也会变成xxx-Message,但是此处loadData()后要转换的,肯定是个NSManagedObject类型的对象,而swift版本工程里的Message却是RLMObject。
折腾了半天在类型前面加上@objc等各种方法,也没解决。不知道怎么使objc和swift有同名类(也许是我没找到正确的方法,如果真的可以这样,下面的坑我就不用跳了)
然后就只有一个方法,改名。Objc中coredata的model肯定不能改了,要靠它来load旧数据呢,只能改Swift工程中的model名了。改名的事,本事也挺简单。惨就惨在,我的这个同名类,在Realm里也是个model...改了名,Realm也不认识它了。这要是一版从来没有上过线的代码也就算了,改名而已。可是,偏偏已经上线3个多月。那么,可想而知,用户是会产生数据存进来的。
那么,现在有两条路了,要么丢掉这3个月的中间版本数据,要么丢掉旧数据。但是,这会让用户非常非常反感了。尤其是会有重要用户的抱怨。于是,我不得已继续想办法吧。接下来只能是Realm数据迁移migration了。这就比较简单了, 只需要在migration时创建新对象就可以了
// Class MergeData中加入函数:
fileprivate func copyDataFromOldRealm() {
let realmConfig = RLMRealmConfiguration.default()
realmConfig.schemaVersion = 1
realmConfig.migrationBlock = { migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
migration.enumerateObjects("Message", block: { (oldObj, newObj) in
migration.createObject(RMessage.className(), withValue: oldObj as Any)
})
}
}
RLMRealmConfiguration.setDefault(realmConfig)
RLMRealm.default()
}
实际上,真的没那么美好。
1,首先,realm是事务的,它的单条写入效率不是太高。
2,其次,migration.createObject()创建对象的效率非常慢,表现在闪屏时间很长,因为在一个个创建object
于是,在我的这个使用场景里,我能想到的解决办法包括:
1,从coredata中获取数据以后,不每条数据调用一次add,而是100条批量执行addOrUpdate
2,考虑到3个月的数据量不会大到离谱,在migrationBlock中,只是把旧的realm数据库中的数据读取出来,保存在数组中,而不通过migration.createObject()直接创建对象
// 类似如下语句,一个个获取旧realm中的key-value
let fileName: String = oldObj!["fileName"] as! String
然后也是跟写入coredata数据一样,批量执行。
3,在开始创建对象写新数据库时,加入loading提示用户等待,这样大概会友好一些吧
本来打算创建数据对象这部分放在后台线程做的,但是realm线程不安全,而且需要打开的都是同一个realm对象,也没进行深入的研究。
以上就是这次事故的一个总结。结论是,及时规避风险,切记头脑发热。我大概测试了一下,coredata+realm一共接近50000条数据,需要用不到60秒来完成迁移。我无法衡量是否会让用户满意,但是我真的已经一言难尽了。
// ============
补充:
1,数据量大概5~6W,停留在launchscreen时间很长,没有找到在闪屏页面上加动态风火轮或者加动图的方法,就在launchscreen和homepage之间加了一个vc,在这个VC里加入风火轮和提示。
2, 100条数据写一次数据库,依然不怎么快;
3,验证了一下,万条数据以上查询确实比coredate效率高
4,realm的migration即使在DespatchQueue_async中进行,也是读不到数据的,猜测跟线程有关系,但都是mainThread,不知为何。