0

In the past few weeks, about Coredata, I added the database backup function. After going online, I received feedback from users that the app opened with a white screen and could not be started normally.

By investigating the reason why the App cannot be started, it is that there is no callback after loadPersistentStores is executed.

This problem has never been reported by users before, until I added the code for data backup, so I suspect it has something to do with data backup.

Below is the code for data backup


func backupSync(completionBlock: @escaping ((Bool) -> Void) = { _ in }) {
        do {
            let backupDir = self.createFolder(name: self.backupFolderName, baseUrl: self.getDocumentsDirectory())
            let backupTime = "backup_\(Date().timeIntervalSince1970)"
            
            let backupFile = try CoreDataStack.shared.backup(fileName: backupTime)
            let backupPath = backupDir + "/" + backupFile.lastPathComponent
            
            // copy file
            
            if !FileManager.default.fileExists(atPath: backupPath) {
                try FileManager.default.copyItem(at: backupFile, to: URL(fileURLWithPath: backupPath))
            }
            
            // zip files with password
            
            let zipPath = backupDir + "/\(backupTime).zip"
            SSZipArchive.createZipFile(atPath: zipPath, withFilesAtPaths: [backupPath], withPassword: self.password)
            
            // delete copy file
            
            if FileManager.default.fileExists(atPath: backupPath) {
                try FileManager.default.removeItem(at: URL(fileURLWithPath: backupPath))
            }
            
            "\(zipPath)".debugLog("Local backup")
            
            completionBlock(true)
            
        } catch {
            completionBlock(false)
            print(error)
        }
    }

func backup(fileName: String) throws -> URL {
        let backupfile = try persistentContainer.persistentStoreCoordinator.backupPersistentStore(atIndex: 0, fileName: fileName).fileURL
        return backupfile
    }

func backupPersistentStore(atIndex index: Int, fileName: String) throws -> TemporaryFile {
        precondition(persistentStores.indices.contains(index), "Index \(index) doesn't exist in persistentStores array")
        let sourceStore = persistentStores[index]
        let backupCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)

        let intermediateStoreOptions = (sourceStore.options ?? [:])
            .merging([NSReadOnlyPersistentStoreOption: true],
                     uniquingKeysWith: { $1 })
        let intermediateStore = try backupCoordinator.addPersistentStore(
            ofType: sourceStore.type,
            configurationName: sourceStore.configurationName,
            at: sourceStore.url,
            options: intermediateStoreOptions
        )

        let backupStoreOptions: [AnyHashable: Any] = [
            NSReadOnlyPersistentStoreOption: true,
            NSSQLitePragmasOption: ["journal_mode": "DELETE"],
            // Minimize file size
            NSSQLiteManualVacuumOption: true,
            ]

        let backupFilename = fileName + ".sqlite"
        let backupFile = try TemporaryFile(creatingTempDirectoryForFilename: backupFilename)
        try backupCoordinator.migratePersistentStore(intermediateStore, to: backupFile.fileURL, options: backupStoreOptions, withType: NSSQLiteStoreType)
        return backupFile
}

When to back up

  1. sceneDidEnterBackground use Operation with UIApplication.shared.beginBackgroundTask
class BackUpOpeartion: Operation {
   
  override func main() {
    LocalBackupManager.shared.backupSync()
  }
   
  static func canHandler() -> Bool {
    let backupStrategy = LocalUserDefaults.standard.backupUiModel
    let lastBackUpDate = LocalUserDefaults.standard.lastBackUpDate
     
    let now = Date()
    let sixHour: TimeInterval
     
    #if DEBUG
    sixHour = TimeInterval(0.15 * 60 * 60)
    #else
    sixHour = TimeInterval(backupStrategy.backupFrequency) // 6h
    #endif
     
    let backupDate = lastBackUpDate + sixHour
     
    // Clean the database at most once per week.
    guard now > backupDate else {
      "It's not time to backup".debugLog()
      return false
    }
     
    return true
  }
   
  static func doWhenEnterBackground() {
    if !canHandler() {
      return
    }
     
    var bgtToken = UIBackgroundTaskIdentifier.invalid
    bgtToken = UIApplication.shared.beginBackgroundTask(expirationHandler: {
      UIApplication.shared.endBackgroundTask(bgtToken)
    })
     
    guard bgtToken != .invalid else{
      return
    }
     
    DispatchQueue.global(qos: .utility).async {
      LocalBackupManager.shared.backupSync { succeed in
        DispatchQueue.main.async {
          if succeed {
            LocalUserDefaults.standard.lastBackUpDate = Date()
          }
          UIApplication.shared.endBackgroundTask(bgtToken)
        }
      }
    }

  }
   
}
  1. BGProcessingTask in AppDelegate
func registerBackgroundTasks() {
    BGTaskScheduler.shared.register(forTaskWithIdentifier: dbBackupId, using: nil) { task in
      self.backupDb(task: task as! BGProcessingTask)
    }
  }

func scheduleDbBackup() {
    if !BackUpOpeartion.canHandler() {
      return
    }
     
    let request = BGProcessingTaskRequest(identifier: dbBackupId)
    request.requiresNetworkConnectivity = false
    request.requiresExternalPower = false
     
    do {
      try BGTaskScheduler.shared.submit(request)
      "submit DbBackup".debugLog()
    } catch {
      print("Could not schedule app refresh: \(error)")
    }
  }

func backupDb(task: BGProcessingTask) {
     
    let queue = OperationQueue()
    queue.maxConcurrentOperationCount = 1
     
    let operation = BackUpOpeartion()
     
    task.expirationHandler = {
      queue.cancelAllOperations()
    }
     
    operation.completionBlock = {
      let success = !operation.isCancelled
      if success {
        LocalUserDefaults.standard.lastBackUpDate = Date()
      }
      task.setTaskCompleted(success: success)
    }
  }

Since before the online backup, there is no feedback about the white screen of the app failing to start, so I feel it is related to the backup

I don't know under what circumstances it would cause CoreDataStack loadPersistentStores no completionHandler callback

can someone help me? Thanks!

wanbo
  • 868
  • 9
  • 24
  • Is the background mode enabled? just curious if the app is being killed. Add lots of `Logger` messages to see in the console how long the app is alive – user1046037 Nov 17 '22 at 16:41
  • @user1046037 The background mode is enabled. According to the log investigation, once the app is stuck, it will not be completely closed through the App Switcher. You can only restart the phone to end the app process. – wanbo Nov 18 '22 at 09:11

0 Answers0