realm之于iOS

Realm是除了CoreData和Sqlite之外的第三个选择,一个近几年兴起的全新的数据库方案,一直保持着活跃的更新,而且引起了iOS开发圈广泛的关注。Realm到底好不好用,又有哪些闪光点呢?下面通过一个实际的demo综合比较CoreData和Realm在使用体验上的差别。

Realm

Realm For iOS正式诞生于2014年左右,一开始就引起了不小的关注,两年多的快速迭代使其日渐成熟。从下图可以看出其活跃度:

不了解Realm的同学可以先简单看下其开发文档和我之前关于移动端数据库的一篇介绍。看完之后不难发现,其实realm的真正对手是CoreData,二者都志在替开发者解决db端存储之外,更提供model层的搭建。CoreData和Realm的官方demo代码都有在Controller当中直接存取数据的例子。

比如CoreData这篇NSFetchedResultsController的官方教程,教大家如何将CoreData提供的model和view做绑定。

比如Realm这篇使用Realm搭建Search Controller的教程,也是教如何在Controller当中直接使用Realm的model。

所以Realm一开始的设计就像是在瞄准CoreData,二者都不是简单的存储解决方案,都希望方便开发者使用,能以对象化的思维去使用数据库。

Sqlite,CoreData,Realm三者之间的关系可以做个简单的比喻,Sqlite提供的是渔网,CoreData和Realm则是直接已经将鱼摆上餐桌,一旦拿起刀叉就没有后悔的余地,至于合不合胃口只能开发者自己去品尝了。

所以下文将主要对比CoreData和Realm二者之间的差异,至于Realm与Sqlite的差异可对照CoreData和Sqlite。

Realm VS CoreData

接下来我们会创建一个demo project,从数据库当中获取user列表展示,同时还提供插入和删除的操作,我们将从项目创建到业务使用,各方面来对照下二者的实际差异。

初始化创建database-CoreData

我们创建一个名为DBProfileDemo项目,如果勾选了使用CoreData,会在创建之后生成一个DBProfileDemo.xcdatamodeld文件,选中该文件可以通过以下图形化的编辑器建立表及一些约束。

为了创建user列表,我们先通过图形化界面操作,建立UserEntity表,及相关的attribute。

<img src=”http://www.mrpeak.cn/images/realm02.png” width=484>

编辑完之后实际上启动demo实际上就完成了初始化的工作,CoreData所需要的几个关键元素都替你准备好了,比如我们创建一个新Entity所需要以下对象:

guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            return
        }
let context = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: DBConstant.Entity.User, in: context)!
let user = NSManagedObject(entity: entity, insertInto: context)

NSPersistentContainer, NSManagedObjectContext可以通过AppDelegate获取,不需要开发者做额外的配置,Entity可以通过runtime以String为参数获取到,到这步都还比较简单,但我们一般需要建立自己的model类,方便后期添加一些业务方法,CoreData也提供了一套机制生成model类。

Xcode 8之前我们是通过选中DBProfileDemo.xcdatamodeld文件,然后通过菜单Editor->Create NSManagedObject Subclass来生成文件。

Xcode 8新提供了新的codegen机制,自动生成相关的model类,生成方式有以下几种:

这里的操作就有些“巧妙”了,我们要先选择Class Definitio,build项目,Xcode会在Derived Data一个很深的目录里生成我们的目标文件:UserEntity+CoreDataClass.swift, UserEntity+CoreDataProperties.swift,将这两个文件拷贝出来添加至项目里,再将Codegen选成Manual/None,否则会出现文件重复的编译错误。以后如果添加了新的attribute,需要再选Category/Extension,重复上述文件的手动操作,最后再改回Manual/None。

以上的代码生成虽然都是auto的,但是一点都不cool!Xcode和Finder来回切换,codegen模式跟着改,Not Cool!model类的创建和维护这一步无法给五星好评。再看看realm的表现。

初始化创建database-Realm

Realm由于没有集成到Xcode当中,需要下载对应的framework,或者通过pod安装。导入framework之后还可以安装一个Xcode插件,用于创建属于realm的model,其实这个插件非必须,完全可以通过创建普通类文件的方式来建立新的realm model。导入realm相关文件之后,就可以直接开始使用realm了。

我们新建一个UserModel类:

import Foundation
import RealmSwift

class UserModel: Object {
    dynamic var name = ""
    dynamic var signagure = ""
    dynamic var userID: Int64 = 0
}

并不需要像Xcode一样在编辑器当中去显式的建立一个表,可以认为model文件生成之后其对应的表也就随之存在了。如果我们想插入一个记录到db中,可以直接执行如下代码:

let user = UserModel()

let realm = try! Realm()

try! realm.write {
    realm.add(user)
}

对象的操作只需要一个realm对象即可,对应于CoreData当中的NSManagedObjectContext。而且开发者可以直接在UserModel当中添加domain logic,并不需要像CoreData一样生成两个文件XXX+CoreDataClass.swift, XXX+CoreDataProperties.swift,每次添加新的property直接修改model类就可以了。而且可以看出和CoreData相比realm的代码非常简洁。

所以在初始化创建环境Realm可以说是完胜CoreData。

写操作及性能对比-CoreData

先来看下CoreData完成一次记录创建所需的代码量:

//write CoreData
func insertNewUserWithCoreData(name: String) {
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
        return
    }

    let context = appDelegate.persistentContainer.viewContext
    let entity = NSEntityDescription.entity(forEntityName: DBConstant.Entity.User, in: context)!
    let user = NSManagedObject(entity: entity, insertInto: context)

    user.setValue(name, forKey: "name")
    
    do {
        try context.save()
    } catch let err as NSError {
        print("save err : \(err)")
    }
}

CoreData做记录插入的时候一定需要一个NSEntityDescription对象,而不能直接创建一个UserEntity对象,我猜测是因为需要通过NSEntityDescription来得知entity本身和其他entity的relation,Description类似于获取table schema的概念,这点使用起来不太直观。

再看下CoreData做100,000次插入所需时间,取10个样本的平均值。

//test write performance for CoreData
func testWriteForCoreData(nameToSave: String) -> Double {
    let start = DispatchTime.now()

    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
        return 0
    }
    let context = appDelegate.persistentContainer.viewContext
    let entity = NSEntityDescription.entity(forEntityName: DBConstant.Entity.User, in: context)!

    for index in 0..<100000 {
        let name = nameToSave + String(index)

        let user = NSManagedObject(entity: entity, insertInto: context)
        user.setValue(name, forKey: "name")
    }
    do {
        try context.save()
    } catch let err as NSError {
        print("save err : \(err)")
    }

    let end = DispatchTime.now();
    let cost = Double(end.uptimeNanoseconds-start.uptimeNanoseconds)/1_000_0000_000
    return cost
}

var avg: Double = 0
for _ in 0...9 {
    avg += self.testWriteForCoreData(nameToSave: nameToSave)
}
print("write cost for CoreData: \(avg/10)")

结果是:write cost for CoreData: 0.01137706024

写操作及性能对比-Realm

Realm插入一条记录所需代码:

//write Realm
func insertNewUserWithRealm(name: String) {
    let user = UserModel()
    user.name = name

    let realm = try! Realm()

    try! realm.write {
        realm.add(user)
    }
}

可以看出对比CoreData代码简洁不少,而且是直接对Model进行操作,不需要引入额外的对象。

再对比写操作性能,每次10000条记录,10个样本的平均值

//test write performance for Realm
func testWriteForRealm(nameToSave: String) -> Double {
    let start = DispatchTime.now()

    let realm = try! Realm()

    try! realm.write {
        for index in 0..<100000 {
            let user = UserModel()
            user.name = nameToSave + String(index)
            realm.add(user)
        }
    }

    let end = DispatchTime.now();
    let cost = Double(end.uptimeNanoseconds-start.uptimeNanoseconds)/1_000_0000_000
    return cost
}


for _ in 0...9 {
    avg += self.testWriteForRealm(nameToSave: nameToSave)
}
print("write cost for Realm: \(avg/10)")

结果是:write cost for Realm: 0.00741579052

上述结果是没有建立index的情况,如果针对userID建立index,测试的结果为:

write cost for CoreData: 0.01643883225 write cost for Realm: 0.01866273956

可以看出二者在写操作性能上大致接近,不建index,realm略微胜出,建index,CoreData表现稍好,但这点性能的差异对实际项目的影响几乎可以忽略,Realm的优势依旧是在代码的表达上,realm更为清晰。

读操作及性能对比-CoreData

批量读取记录代码:

//query CoreData
func fetchAllUserWithCoreData() -> [NSManagedObject] {
    var userList = [NSManagedObject]()
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
        return userList
    }
    let context = appDelegate.persistentContainer.viewContext
    let req = NSFetchRequest<NSManagedObject>(entityName: DBConstant.Entity.User)

    do {
        userList = try context.fetch(req)
    } catch let err as NSError {
        print("err \(err)")
    }

    return userList
}

读操作通过一个对象NSFetchRequest来完成,同时需要传入entity的名字。

性能方面,我们读取上面写入的100,000条记录,读10次取平均值。

//test read performance for CoreData
func testReadForCoreData() -> Double {
    let start = DispatchTime.now()
    var userList = [NSManagedObject]()
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
        return 0
    }
    let context = appDelegate.persistentContainer.viewContext
    let req = NSFetchRequest<NSManagedObject>(entityName: DBConstant.Entity.User)

    do {
        userList = try context.fetch(req)
    } catch let err as NSError {
        print("err \(err)")
    }

    for user in userList {
        let name = user.value(forKey: "name")
    }

    let end = DispatchTime.now();
    let cost = Double(end.uptimeNanoseconds-start.uptimeNanoseconds)/1_000_0000_000
    return cost
}

var avg: Double = 0
for _ in 0...9 {
    avg += self.testReadForCoreData()
}
print("read cost for CoreData: \(avg/10)")

输出为:read cost for CoreData: 0.00231904814

读操作及性能对比-Realm

批量读取记录:

//query Realm
func fetchAllUserWithRealm() -> [UserModel] {
    let realm = try! Realm()

    let userList = realm.objects(UserModel.self)

    return Array(userList)
}

代码非常干净,不需要引入额外的对象来描述查询请求。

性能对比,100,000条记录,10次取平均值:

//test read performance for Realm
func testReadForRealm() -> Double {
    let start = DispatchTime.now()

    let realm = try! Realm()

    let users = realm.objects(UserModel.self)

    let userList = Array(users)
    for user in userList {
        let name = user.name
    }

    let end = DispatchTime.now();
    let cost = Double(end.uptimeNanoseconds-start.uptimeNanoseconds)/1_000_0000_000
    return cost
}

for _ in 0...9 {
    avg += self.testReadForRealm()
}
print("read cost for Realm: \(avg/10)")

输出为:read cost for Realm: 0.00098051783

以上结果是不建立Index的case,如果针对userID建立Index,100,000次查询,测试结果为:

read cost for CoreData: 0.0783214620999998 read cost for Realm: 0.098253140500001

如果没有Index,Realm测试中批量read的性能大致是CoreData的2.3倍,针对userID建立Index情况下CoreData性能略微胜出,但在API友好度上依旧是Realm胜出。

实际上关于二者读写的性能对比,Realm官方在2014年做过一份数据对比,宣称其无论insert还是query性能都是CoreData的10倍左右。我这次都是使用Realm和CoreData的最新版本,实际测试结果二者表现是接近的,或者这两年Realm和CoreData都做过一些底层更新。

多线程设计

一言以蔽之,在多线程设计上,Realm面临和CoreData同样的困境,这种困境究其根本是源自于二者对model layer的相同处理。

如果想要把CoreData的写操作都异步到子线程当中,那么需要在子线程当中使用自己的NSManagedObjectContext,需要遵循以下两个规则:

  • 不同的线程要建立自己的NSManagedObjectContext,维护各自的object graph。
  • NSManagedObject不能跨线程传递使用,只要通过传递NSManagedObjectID,再通过ID去从各自的Context中获取Object。

在Realm的世界里,和Context对应的概念是Realm对象,同样realm对象也不能跨线程使用,也不能在不同的线程之间共享一份model。Realm的官方表述如下:

Sharing Realm instances across threads is not supported. Realm instances accessing the same Realm file must also all use the same Realm.Configuration.

也就意味着,如果你在app的应用层直接使用CoreData或者Realm的model实例,在操作对应model的时候,你需要十分清楚model是在哪个线程创建的,哪里创建才能哪里使用。这显然很不符合对象化的思维,给model的使用带来了额外的负担。

当然Realm作为新生事物,还是做了一些改良。比如在CoreData当中多个线程的context如果需要同步object graph,需要显式的merge context或者建立parent-children context的层级结构。在realm世界,如果realm对象所处的线程拥有runloop,每个runloop开始的时候会自动去同步一次,没有runloop的工作线程需要显式的调用Realm.refresh()(调用写操作的时候也会自动同步一次)。这种做法的确比CoreData在使用上负担更小。

Realm特色

Realm将所有和开发者的交互都放在model类里面,不需要像CoreData一样在图形编辑器和类文件二者之间切换。

一对一的关系:

class Dog: Object {
    dynamic var owner: Person?
}

一对多的关系:

class Person: Object {
    let dogs = List<Dog>()
}

建立索引:

override static func indexedProperties() -> [String] {
    return ["userID"]
}

建立Primary Key:

override static func primaryKey() -> String? {
    return "id"
}

以上这些都定义在model类文件当中,一目了然。

Realm的集合操作更灵活,支持Chaining。

let usersB = realm.objects(UserModel.self).filter("name BEGINSWITH 'B'")
let usersBC = usersB.filter("sigature BEGINSWITH 'C'")

Realm还支持Notification模式,这对于CoreData来说是个全新的设计,这个通知不仅仅是KVO层面,Realm还可以在更大的粒度上对数据的变化做出监听,比如监听单个object,或者object的集合变化。我们可以通过如下代码监听对象的变化:

let token = realm.addNotificationBlock { notification, realm in
    viewController.updateUI()
}

不要小看这个Notification机制,它的核心思想和当下流行的Reactive Programming同出一宗,如果大面积使用,可以搭建一个基于用户行为驱动的App架构。

我之前写过博客介绍如何搭建数据驱动型的iOS App架构,所以看到Realm的Notification机制之后倍感亲切。总体来说,数据的变化按照粒度大小可以依次分为:property变化,object变化,object集合变化。粒度越小,变化的频率越高,意味着会触发更多的UI线程刷新操作,如果不仔细的设计可能会导致意外的性能问题。

Realm相较于CoreData还有个优势,Android端和iOS端可以采用一致的存储设计,甚至可以通过工具或者脚本生成两个端相同的model,model层的设计一致会更多的驱使两个端在业务设计上也更接近,在遇到问题和寻求解决方案时避免踩两次坑。

总结:

Realm和CoreData相比较,二者的最新版本在性能上似乎并没有太大的差异,不过Realm的API设计和总体使用体验比CoreData要好很多,对象化的设计上更简洁,学习曲线没有CoreData陡。对App架构的影响上,二者的设计思路非常接近,都是通过对象化的接口来完成数据的存储和model layer的搭建,CoreData在架构设计遇到的问题Realm并没有太大的改善。

我个人对于CoreData或者Realm的使用建议是:不要在应用层(Controller中)大量引入CoreData和Realm相关的代码,将其通过另一层封装隔离在单独的model layer,所有应用层使用到的model都通过model layer做一次转换,避免Controller直接对第三方的model产生依赖,这样即使遇到问题,在做数据迁移的时候会少很多工作量。

至于在Sqlite,CoreData,Realm三者之间如何选择?

个人建议:

对于已经使用CoreData或者Sqlite方案的App,没必要迁移至Realm。

对于新项目,如果规模小,数据存储和查询复杂度小,可以使用Realm提升model layer的搭建速度和开发体验。

如果新项目业务复杂度高,数据读写频繁,直接采用Realm还是存在一定的风险,团队如果在sqlite上已经有一定的技术积累,还是应该采用更成熟的sqlite方案,自己完成model layer的搭建。如果是在CoreData和Realm之间做选择,我推荐Realm,更容易上手,代码也更简洁,Realm团队对于问题处理的态度也非常积极迅速,总体来说,Realm是比CoreData更优秀的方案,对开发者更友好。

上述测试项目代码就不放github了,感兴趣的同学可以在我公众号MrPeakTech回复realm获取下载链接。

欢迎关注公众号:MrPeakTech