在raywenderlick上面看到一篇介紹Realm的文章,試用了一下,確實比CoreData方便不少,當(dāng)然使用Realm會加大app的體積,官方文檔上說的是Realm庫文件大小在1M左右。這篇筆記對Realm的使用做一個簡單的總結(jié),raywenderlick上面的文章我也用Objective-C重新實現(xiàn)了一遍。
1 Realm架構(gòu)
Realm是一個移動端數(shù)據(jù)庫,專門針對移動APP設(shè)計,不僅適用于iOS,也適用于Android,目前最新版本是1.0.2,我這用的是0.9.8版本。它底層并不依賴SQLite,有自己的一套存儲引擎。官網(wǎng)地址https://realm.io/cn/,有非常詳細的中文文檔,也支持CocoaPods,所以在項目中只要在Podfile中加入Realm然后 pod install就行。以下代碼描述都是針對iOS,其他語言請參照官方文檔。
Realm是一個類MVCC數(shù)據(jù)庫,每個連接的線程在特定的時刻都有一個數(shù)據(jù)庫的快照。MVCC在設(shè)計上采用了和Git一樣的源文件管理算法,也就是說你的每個連接線程就好比在一個分支(也就是數(shù)據(jù)庫的快照)上工作,但是你并沒有得到一個完整的數(shù)據(jù)庫拷貝。Realm和一些真正的MVCC數(shù)據(jù)庫如MySQL是不同的,Real在某個時刻只能有一個寫操作,且總是操作最新的數(shù)據(jù)版本,不能在老版本操作。

Realm數(shù)據(jù)庫使用了零拷貝技術(shù),這是與CoreData及其他數(shù)據(jù)庫完全不同的地方。
通常的數(shù)據(jù)庫操作是這樣的,數(shù)據(jù)存儲在磁盤的數(shù)據(jù)庫文件中,我們的查詢請求會轉(zhuǎn)換為一系列的SQL語句,創(chuàng)建一個數(shù)據(jù)庫連接。數(shù)據(jù)庫服務(wù)器收到請求,通過解析器對SQL語句進行詞法和語法語義分析,然后通過查詢優(yōu)化器對SQL語句進行優(yōu)化,優(yōu)化完成執(zhí)行對應(yīng)的查詢,讀取磁盤的數(shù)據(jù)庫文件(有索引則先讀索引),返回對應(yīng)的數(shù)據(jù)內(nèi)容并存儲到內(nèi)存中,數(shù)據(jù)還需要序列化成內(nèi)存可存儲的格式,最后數(shù)據(jù)還要轉(zhuǎn)換成語言層面的類型,比如Objective-C的對象等。
而Realm完全不同,它的數(shù)據(jù)庫文件是通過memory-mapped,也就是說數(shù)據(jù)庫文件本身是映射到內(nèi)存中的,Realm訪問文件偏移就好比文件已經(jīng)在內(nèi)存中一樣(這里的內(nèi)存是指虛擬內(nèi)存),它允許文件在沒有做反序列化的情況下直接從內(nèi)存讀取,提高了讀取效率。
2 Realm與多線程
零拷貝架構(gòu)也使得Realm可以自動更新對象和查詢。在一個查詢中更新對象,在另外一個查詢中可以馬上讀取到更新的內(nèi)容。多線程同時更新數(shù)據(jù)也是一樣,可以即時更新對象的內(nèi)容。正是因為對象的自動更新,所以Realm中也是不允許多線程之間的對象共享,因為如果多線程共享Realm對象,會導(dǎo)致數(shù)據(jù)的不一致性,雖然通過加鎖是可以保證數(shù)據(jù)一致性的,但是會增加開銷。
因此,在使用Realm的時候,不要在多個線程之間共享對象。如果要在另外一個線程獲取同樣的數(shù)據(jù),請重新執(zhí)行查詢。 多線程更新數(shù)據(jù)的操作后面會有例子演示。
3 Realm的幾個基本概念
Realm Objective-C支持的數(shù)據(jù)類型有BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData 以及 被特殊類型標(biāo)記的 NSNumber,注意:Realm不支持auto_increment類型。Realm中涉及的幾個類和概念如下:
RLMRealm
這是Realm數(shù)據(jù)庫框架的核心,它是一個訪問底層數(shù)據(jù)庫的指針,有點類似CoreData中的ManagedObjectContext對象。在代碼中可以通過 [RLMRealm defaultRealm]獲取。
RLMObject
這是Realm的對象模型。自己定義的對象要繼承該類,然后可以定義自己的屬性。也可以定義主鍵(覆寫 + (NSString *)primaryKey方法)和索引(覆寫+ (NSArray<NSString *> *)indexedProperties方法)等。
Relationships
-
多對一關(guān)系或一對一關(guān)系
對于多對一或者一對一的關(guān)系,只需要聲明一個RLMObject子類類型的屬性即可。你可以簡單的通過這個屬性實現(xiàn)關(guān)系的綁定。
``` //多對一或者一對一關(guān)系代碼示例 // Dog.h @interface Dog : RLMObject // 其余屬性聲明... @property Person *owner; @end //綁定代碼 Person *jim = [[Person alloc] init]; Dog *rex = [[Dog alloc] init]; rex.owner = jim; ``` -
一對多關(guān)系
對于一對多的關(guān)系,則需要聲明一個RLMArray類型的屬性。另外,還要在一的類中加入一個協(xié)議聲明。RLM_ARRAY_TYPE 宏創(chuàng)建了一個協(xié)議,從而允許 RLMArray<Dog> 語法的使用。
//Dog.h @interface Dog : RLMObject // 屬性聲明... @end RLM_ARRAY_TYPE(Dog) // 定義一個 RLMArray<Dog>類型 // Person.h @interface Person : RLMObject // 其余的屬性聲明... @property RLMArray<Dog *><Dog> *dogs; @end //綁定代碼 RLMResults<Dog *> *someDogs = [Dog allObject]; [jim.dogs addObjects:someDogs]; [jim.dogs addObject:rex]; -
反向關(guān)系
在前面的例子中,加入dog的時候,我們只是綁定了一個人可以有多只狗,但是并沒有指定這只狗的主人是誰。為了自動加入狗與人的綁定關(guān)系,需要用反向關(guān)系來解決,在Dog.h中聲明一個RLMLinkingObjects對象,然后在實現(xiàn)代碼中加入反向關(guān)系的鏈接函數(shù),這樣,當(dāng)我們在加入dogs的時候,會自動設(shè)置好這只狗的主人(owners)屬性。
@interface Dog : RLMObject @property NSString *name; @property NSInteger age; @property (readonly) RLMLinkingObjects *owners; @end @implementation Dog + (NSDictionary *)linkingObjectsProperties { return @{ @"owners": [RLMPropertyDescriptor descriptorWithClass:Person.class propertyName:@"dogs"], }; } @end
Write Transactions
Realm中所有涉及數(shù)據(jù)更改的操作如insert,delete,update等,必須在一個write事務(wù)中執(zhí)行。
[[RLMRealm defaultRealm] transactionWithBlock:^{
......
}];
Queries
查詢操作很簡單,不需要在一個write事務(wù)中執(zhí)行。Realm所有的查詢操作都是延遲加載的,只有當(dāng)屬性被訪問的時候,才會讀取相應(yīng)的數(shù)據(jù)。查詢結(jié)果并不是數(shù)據(jù)的拷貝,修改查詢結(jié)果(在寫入事務(wù)中)會直接修改硬盤上的數(shù)據(jù)。
查詢對象的基本方法是 RLMObject的allObjects方法,查詢的結(jié)果是一個RLMResults<RLMObject *>對象。還可以條件查詢并對結(jié)果排序,代碼如下:
RLMResults<Dog *> *dogs = [Dog allObjects]; // 從默認的 Realm 數(shù)據(jù)庫中,檢索所有狗狗
// 使用斷言字符串查詢
RLMResults<Dog *> *tanDogs = [Dog objectsWhere:@"color = '白色' AND name BEGINSWITH '小'"];
// 使用 NSPredicate 查詢
NSPredicate *pred = [NSPredicate predicateWithFormat:@"color = %@ AND name BEGINSWITH %@", @"白色", @"小"];
tanDogs = [Dog objectsWithPredicate:pred];
// 排序名字以“大”開頭的黑色狗狗
RLMResults<Dog *> *sortedDogs = [[Dog objectsWhere:@"color = '黑色' AND name BEGINSWITH '大'"] sortedResultsUsingProperty:@"name" ascending:YES];
Results
與其他數(shù)據(jù)庫不同的是,Realm并沒有提供LIMIT之類的關(guān)鍵字來限制一次加載的數(shù)據(jù)量。因為Realm中的查詢是延遲加載的,只有在查詢結(jié)果被使用到的時候,才會讀取數(shù)據(jù)庫文件去加載對象,所以并不用LIMIT來實現(xiàn)分頁。
Migration
當(dāng)我們需要修改數(shù)據(jù)模型時,比如增減屬性,屬性重命名等,都進行數(shù)據(jù)遷移。后面的一節(jié)會有實例介紹。
4 實例
首先創(chuàng)建一個Podfile,下載Realm庫。為了查看Realm數(shù)據(jù)庫的數(shù)據(jù),推薦下載一個官方的工具Realm Browser,通過這個工具我們只要到對應(yīng)數(shù)據(jù)庫的目錄下,點擊default.realm就可以看到數(shù)據(jù)庫的所有數(shù)據(jù)了。
#Podfile
platform :ios, ‘9.0’
use_frameworks!
target ‘RealmStart’ do
pod 'Realm', '~> 0.98'
end
數(shù)據(jù)庫文件的目錄可以通過[RLMRealm defaultRealm].configuration.fileURL)得到,然后在文件管理器通過前往對應(yīng)目錄就可以了(注:Mac默認隱藏了用戶目錄的Library目錄,可以通過菜單欄的前往或者快捷鍵Command+Shift+g打開前往對話框。數(shù)據(jù)庫文件默認是沒有加密的,如果需要加密,可以參照官方文檔-加密這一節(jié)內(nèi)容。另外,Realm還提供了Xcode插件,可以去官網(wǎng)下載并安裝,方便建立數(shù)據(jù)模型。
我這里新建了三個類,分布是Dog,Person以及Company。其中Dog和Person是多對多的關(guān)系,即一只狗可以屬于多個人,而一個人可以有多只狗。Person和Company為多對一的關(guān)系,即一個人只能屬于一個公司,一個公司可以有很多人。
代碼如下:
//Dog.h
#import <Realm/Realm.h>
@interface Dog : RLMObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *color;
@property (nonatomic, readonly) RLMLinkingObjects *owners;
@end
RLM_ARRAY_TYPE(Dog) //必須加這個宏定義
//Dog.m
#import "Dog.h"
#import "Person.h"
@implementation Dog
//反向鏈接
+ (NSDictionary *)linkingObjectsProperties {
return @{
@"owners": [RLMPropertyDescriptor descriptorWithClass:Person.class propertyName:@"dogs"],
};
}
@end
//Person.h
#import <Realm/Realm.h>
#import "Company.h"
#import "Dog.h"
@interface Person : RLMObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic) NSInteger age;
@property (nonatomic, strong) Company *company;
@property (nonatomic, strong) RLMArray<Dog *><Dog> *dogs; //一對多
@end
//Person.m
#import "Person.h"
@implementation Person
//為屬性name加索引
+ (NSArray<NSString *> *)indexedProperties {
return @[@"name"];
}
@end
//Company.h
#import <Realm/Realm.h>
@interface Company : RLMObject
@property (nonatomic, strong) NSString *name;
@end
//Company.m
#import "Company.h"
@implementation Company
@end
然后在AppDelegate.m中加入測試代碼如下:
/**
清理數(shù)據(jù)庫文件,為測試環(huán)境做準備。
*/
- (void)cleanRealm {
NSFileManager *manager = [NSFileManager defaultManager];
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
NSArray<NSURL *> *realmFileURLs = @[
config.fileURL,
[config.fileURL URLByAppendingPathExtension:@"lock"],
[config.fileURL URLByAppendingPathExtension:@"management"],
];
for (NSURL *URL in realmFileURLs) {
NSError *error = nil;
[manager removeItemAtURL:URL error:&error];
if (error) {
NSLog(@"clean realm error:%@", error);
}
}
}
/**
添加測試數(shù)據(jù)
*/
- (void)addInitDataToRealm {
Company *company = [[Company alloc] init];
company.name = @"GOOGLE";
Person *person = [[Person alloc] init];
person.name = @"張三";
person.age = 28;
person.company = company;
Dog *dog1 = [[Dog alloc] init];
dog1.name = @"小黑";
dog1.color = @"黑色";
Dog *dog2 = [[Dog alloc] init];
dog2.name = @"小狗子";
dog2.color = @"黑色";
Dog *dog3 = [[Dog alloc] init];
dog3.name = @"大白";
dog3.color = @"白色";
[person.dogs addObject:dog1];
[person.dogs addObject:dog2];
[person.dogs addObject:dog3];
RLMRealm *realm = [RLMRealm defaultRealm];
[realm transactionWithBlock:^{
[realm addObject:person];
}];
}
/**
查詢測試
*/
- (void)queryRealm {
RLMResults<Dog *> *dogs = [[Dog objectsWhere:@"color = '黑色' AND name BEGINSWITH '小'"] sortedResultsUsingProperty:@"name" ascending:YES];
for (Dog *dog in dogs) {
NSLog(@"dog:%@, owners:%@", dog, dog.owners);
}
}
/**
更新測試
*/
- (void)updateRealm {
NSPredicate *pred = [NSPredicate predicateWithFormat:@"color = %@ AND name BEGINSWITH %@", @"白色", @"大"];
RLMResults *dogs = [Dog objectsWithPredicate:pred];
[[RLMRealm defaultRealm] transactionWithBlock:^{
for (Dog *dog in dogs) {
dog.color = @"新的顏色";
}
}];
}
/**
多線程測試
*/
- (void)multithreadRealm {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
NSLog(@"start async");
RLMResults *results = [Person objectsWhere:@"name = '張三' "];
if (results.count > 0) {
Person *person = results[0];
NSLog(@"outer block, name:%@", person.name);
}
[[RLMRealm defaultRealm] transactionWithBlock:^{
NSLog(@"in async block");
RLMResults *results = [Person objectsWhere:@"name = '張三' "];
if (results.count > 0) {
Person *person = results[0];
person.name = @"王麻子";
NSLog(@"change name to wangmazi");
}
}];
if (results.count > 0) {
Person *person = results[0];
NSLog(@"async person:%@, tid=%@", person.name, [NSThread currentThread]);
}
});
NSArray *names = @[@"張三", @"李四"];
[[RLMRealm defaultRealm] transactionWithBlock:^{
int i = 0;
while (i < 2) {
NSString *name = names[i];
RLMResults *results = [Person objectsWhere:@"name = %@", name];
if (results.count > 0) {
Person *person = results[0];
if ([person.name isEqualToString:@"李四"]) {
person.name = @"王五";
NSLog(@"change name to wangwu");
} else {
person.name = @"李四";
NSLog(@"change name to lisi");
}
sleep(3);
}
i++;
}
}];
}
/**
多線程輸出:
2016-07-16 20:34:31.103 RealmStart[32013:1565410] change name to lisi
2016-07-16 20:34:32.104 RealmStart[32013:1565455] start async
2016-07-16 20:34:32.108 RealmStart[32013:1565455] outer block, name:張三
2016-07-16 20:34:34.172 RealmStart[32013:1565410] change name to wangwu
2016-07-16 20:34:37.248 RealmStart[32013:1565455] in async block
*/
如果我們要修改數(shù)據(jù)模型,比如Company增加一個屬性age,這個時候就需要遷移數(shù)據(jù)。那么除了在Company.h中加入屬性聲明外,還要在app開始加入遷移代碼,設(shè)置數(shù)據(jù)庫版本。遷移數(shù)據(jù)的代碼很簡單,如下,每次修改數(shù)據(jù)模型都要修改版本號。如果是增加索引,測試發(fā)現(xiàn)可以不遷移數(shù)據(jù),而如果是更改屬性名字,則需要加入遷移代碼保證新舊屬性的數(shù)據(jù)遷移,更多用法可以參加官網(wǎng)文檔。
- (void)migrateRealm {
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion){
if (oldSchemaVersion < 1) {
//什么都不做
}
};
[RLMRealmConfiguration setDefaultConfiguration:config];
[RLMRealm defaultRealm];
}
5 總結(jié)
- 涉及數(shù)據(jù)更新的操作必須在transactionWithBlock中完成(或者用
[realm beginWriteTransaction]和[realm commitWriteTransaction])。
- 涉及數(shù)據(jù)更新的操作必須在transactionWithBlock中完成(或者用
- 注意一對多關(guān)系中的協(xié)議的宏定義以及反向鏈接中的變量定義。
- 多線程不能共享Realm對象。多線程調(diào)用transactionWithBlock更新數(shù)據(jù)時,后調(diào)用的線程會阻塞直到前一個線程完成數(shù)據(jù)更新,如果是查詢數(shù)據(jù)則不受影響。
- 4)一個線程的transacWithBlock如果沒有執(zhí)行完成,則數(shù)據(jù)更新并沒有寫到磁盤上,因此這個時候在其他線程中看到的數(shù)據(jù)還是它的快照版本,并不是新的數(shù)據(jù)。見多線程例子。
6 代碼
本文示例的代碼地址:
https://github.com/shishujuan/ios_study/tree/master/realm/RealmStart參考資料3的Objective-C實現(xiàn)的代碼地址:
https://github.com/shishujuan/ios_study/tree/master/realm/RealmDemo