肇鑫的技术博客

业精于勤,荒于嬉

开发者备份和恢复Realm数据库

最近在研究带参数的捷径。因为是SiriKit相关,需要把Realm放在应用组里。这样就不能通过Xcode下载容器了。几天时间里,已经有了两次不小心删除掉数据库,而不得不通过备份恢复手机的经历。我开始研究一下如何导出和恢复数据。

思路

因为目的只是为了方便开发,而不是交给用户操作,过程简便,代码容易是重点。

首先想到的是通过长按某个按钮实现导出,然后出了问题就将导出的数据库放在应用里,应用启动时检测是否有数据,如果有,就替换,然后再正常启动。

实现

具体实现的时候,遇到了一个问题,分享出来的URL,并没有写成文件,而是写成了纯文本。

@objc private func exportRealm(_ sender:Any) {
    let sourceURL = Realm.Configuration.defaultConfiguration.fileURL!
    
    // copy realm to cache and rename it to current date and time
    let fm = FileManager.default
    let filename = { () -> String in
        let df = DateFormatter()
        df.dateFormat = "yyyy.MM.dd_HH.mm.ss"
        return "any_counter_2.backup.at.\(df.string(from: Date())).realm"
    }()
    let cacheFolderURL = fm.urls(for: .cachesDirectory, in: .userDomainMask).first!
    let destinationURL = URL(fileURLWithPath: filename, isDirectory: false, relativeTo: cacheFolderURL)
    
    do {
        try fm.copyItem(at: sourceURL, to: destinationURL)
    } catch {
        return
    }

    // export
    let activityViewController = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
    activityViewController.completionWithItemsHandler = { (_, isCompleted, _, activityError) -> Void in
        guard activityError == nil || (activityError! as NSError).code == 3072 else { // Error Domain=NSCocoaErrorDomain Code=3072 \"操作已被取消。\"
            fatalError("\(activityError!)")
        }

        // clear cache after success
        try? fm.removeItem(at: destinationURL)
    }
    present(activityViewController, animated: true, completion: nil)
}

这个问题花费了我不少时间去寻找解决方案。最后我突发奇想解决了。实际上,就是和URL有关。将12行

let destinationURL = URL(fileURLWithPath: filename, isDirectory: false, relativeTo: cacheFolderURL)

替换为

let destinationURL = URL(fileURLWithPath: cacheFolderURL.path + "/" + filename, isDirectory: false)

,问题解决。

类似的事情很早就遇到过,比如

import Foundation

let folder = "/foo"
let file = "bar"

let folderURL = URL(fileURLWithPath: folder, isDirectory: true)
let u1 = URL(fileURLWithPath: file, isDirectory: false, relativeTo: folderURL)
print(u1.path) // /foo/bar

let u2 = URL(fileURLWithPath: folder + "/" + file, isDirectory: false)
print(u2.path) // /foo/bar

print(u1 == u2) // false
print(u1.path == u2.path) // true

即两个path相同的URL,仅仅是因为构造方法的不同,它们就不相等。这其实是很不直观的,人们在没遇到之前,可能就会认为path相等的两个URL就是相等的,但是实际上系统认为不是。

这里也是这样,通过relativeTo构造的URL,iOS系统仅能将它存成一个纯文本。但是直接构造的URL就没有这个问题。

我虽然不认同,但是只能记住。

Realm的坑(四)

之前的三个坑都是很久远的事情了。今天要填一个新坑。

我们知道,Realm.objects(_)返回的值是Results<T>,由于Realm自身的特性,Results<T>是很有用的,它是lazy动态的。因此,在使用Realm的时候,我们更喜欢使用Results<T>而不是Array

但是Results<T>有一个问题,就是它的排序功能十分有限。它最基本的排序只有一个函数,即sorted(byKeyPath:ascending:)。也就是说,Results<T>只能按照Realm对象的动态属性排序,而不能使用我们经常用到的closure排序。

分析

这就使得我们在有些时候,为了排序的方便,要么增加磁盘占用,添加额外的动态属性;要么,就只能使用Array类型。

值得一提的是,有时,如果是涉及到当前时间这一类的排序变量,即便我们使用额外的动态属性,也是不能排序的。因为这个值一直在变。因此就只能使用Array类型。

didSet不执行的问题

特别的,我们需要注意,在Realm的动态属性中,didSet是不会执行的,这应该是和Objective-C的运行时相关。因此,如果需要使用didSet,就需要改成其它的方式。

参考资料

相关

CGImageSource对照片自动旋转问题的解决

前两天用咕唧分享照片的时候,我发现分享出去的照片,与我查看时的角度不一致,被转了90°。这是一个需要解决的问题。

分析问题1

一开始,我以为是咕唧的隐私保护功能导致的。为了保护隐私,咕唧默认会移除照片中的GPS信息和Exif信息。此外,用户可以在设置中,选择分享这些信息。

我们知道,平常拍照片,你的手机可能是横着拍,也可能是竖着拍,偶尔可能倒过来拍。但是不管你拍照时,手机处于何种角度,系统在显示该照片的时候,都会正确的显示该照片为当时你在屏幕上见到的样子。因为系统会读取照片属性中的方向信息,从而知道你拍照时手机的状态,这样就能正确的显示。

我首先就是认为Exif信息被咕唧删除了,系统没有了参照,只能按照默认的方式处理,因而与实际的方式造成了偏差。

解决问题1

我创建了一个测试项目,然后将那张出问题的照片作为资源,做同样的缩小操作,之后查看照片属性。结果我发现其实方向Orientation属性并没有保存在Exif信息,而是在图片的属性中。也就是说,事实上,咕唧缩小的图片是包含这个信息的。之前的分析是错误的。

那么是什么原因导致了包含了正确方向信息的照片,实际显示的反而是错误的呢?

分析问题2

排除了一切不可能,剩下的就是唯一的可能。

我们知道,系统在读取照片的时候,会根据方向Orientation做正确的处理。也就是说,CGImageSourceCreateImageAtIndex(_:_:_:)获得的照片本身就是正确方向的。那么这个方向正确的照片。在通过CGImageDestination重新保存时,就应该改变Orientation值为默认值,而不是继续保存原始值。

解决问题2

如果上面的分析正确,思路就很简单了。保存之前,检查方向Orientation是否为默认值,如果不是,则修改为默认值。

注意:是只要CGImageSource加载了图片,就会造成这个问题。因此,不仅缩小照片有这个问题,仅仅是去除Exif或者GPS信息,都会存在这个问题,同样需要修改方向Orientation为默认值。

思考

我在想要不要把这个问题,作为bug报告给苹果。思考的结果是不要。因为如果苹果修复了这个问题,将新照片变成与原始照片的方向一致。那么之前有做过特殊处理的应用,照片的方向就又都会从正确变成错误。这相当于是API不延续了。

为了保证之前的兼容性,好多时候,只能遗留不正确的代码。这个就是API设计出问题的代价。

参考资料