iOS學(xué)習(xí)筆記(11)-Realm初探

在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ù)版本,不能在老版本操作。

圖1 Realm數(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ù)。

查詢對象的基本方法是 RLMObjectallObjects方法,查詢的結(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é)

    1. 涉及數(shù)據(jù)更新的操作必須在transactionWithBlock中完成(或者用
      [realm beginWriteTransaction][realm commitWriteTransaction])。
    1. 注意一對多關(guān)系中的協(xié)議的宏定義以及反向鏈接中的變量定義。
    1. 多線程不能共享Realm對象。多線程調(diào)用transactionWithBlock更新數(shù)據(jù)時,后調(diào)用的線程會阻塞直到前一個線程完成數(shù)據(jù)更新,如果是查詢數(shù)據(jù)則不受影響。
  • 4)一個線程的transacWithBlock如果沒有執(zhí)行完成,則數(shù)據(jù)更新并沒有寫到磁盤上,因此這個時候在其他線程中看到的數(shù)據(jù)還是它的快照版本,并不是新的數(shù)據(jù)。見多線程例子。

6 代碼

7 參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容