美文网首页
Realm migration + Coredata -> Re

Realm migration + Coredata -> Re

作者: 谁动了我的芝麻糖 | 来源:发表于2019-03-24 00:28 被阅读0次

    先要闲扯几句最近遇到的这个麻烦,算是我以下做数据迁移的原因。想看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,不知为何。

    相关文章

      网友评论

          本文标题:Realm migration + Coredata -> Re

          本文链接:https://www.haomeiwen.com/subject/vznevqtx.html