Realm資料庫 從入門到“放棄”

一縷殤流化隱半邊冰霜發表於2019-03-02

前言

由於最近專案中在用Realm,所以把自己實踐過程中的一些心得總結分享一下。

Realm是由Y Combinator公司孵化出來的一款可以用於iOS(同樣適用於Swift&Objective-C)和Android的跨平臺移動資料庫。目前最新版是Realm 2.0.2,支援的平臺包括Java,Objective-C,Swift,React Native,Xamarin。

Realm官網上說了好多優點,我覺得選用Realm的最吸引人的優點就三點:

  1. 跨平臺:現在很多應用都是要兼顧iOS和Android兩個平臺同時開發。如果兩個平臺都能使用相同的資料庫,那就不用考慮內部資料的架構不同,使用Realm提供的API,可以使資料持久化層在兩個平臺上無差異化的轉換。

  2. 簡單易用:Core Data 和 SQLite 冗餘、繁雜的知識和程式碼足以嚇退絕大多數剛入門的開發者,而換用 Realm,則可以極大地學習成本,立即學會本地化儲存的方法。毫不吹噓的說,把官方最新文件完整看一遍,就完全可以上手開發了。

  3. 視覺化:Realm 還提供了一個輕量級的資料庫檢視工具,在Mac Appstore 可以下載“Realm Browser”這個工具,開發者可以檢視資料庫當中的內容,執行簡單的插入和刪除資料的操作。畢竟,很多時候,開發者使用資料庫的理由是因為要提供一些所謂的“知識庫”。

Realm資料庫 從入門到“放棄”

“Realm Browser”這個工具除錯起Realm資料庫實在太好用了,強烈推薦。

如果使用模擬器進行除錯,可以通過


[RLMRealmConfiguration defaultConfiguration].fileURL複製程式碼

列印出Realm 資料庫地址,然後在Finder中⌘⇧G跳轉到對應路徑下,用Realm Browser開啟對應的.realm檔案就可以看到資料啦.

如果是使用真機除錯的話“Xcode->Window->Devices(⌘⇧2)”,然後找到對應的裝置與專案,點選Download Container,匯出xcappdata檔案後,顯示包內容,進到AppData->Documents,使用Realm Browser開啟.realm檔案即可.

自2012年起, Realm 就已經開始被用於正式的商業產品中了。經過4年的使用,逐步趨於穩定。

目錄

  • 1.Realm 安裝
  • 2.Realm 中的相關術語
  • 3.Realm 入門——如何使用
  • 4.Realm 使用中可能需要注意的一些問題
  • 5.Realm “放棄”——優點和缺點
  • 6.Realm 到底是什麼?
  • 7.總結

一. Realm 安裝

使用 Realm 構建應用的基本要求:

  1. iOS 7 及其以上版本, macOS 10.9 及其以上版本,此外 Realm 支援 tvOS 和 watchOS 的所有版本。
  2. 需要使用 Xcode 7.3 或者以後的版本。

注意 這裡如果是純的OC專案,就安裝OC的Realm,如果是純的Swift專案,就安裝Swift的Realm。如果是混編專案,就需要安裝OC的Realm,然後要把 Swift/RLMSupport.swift 檔案一同編譯進去。

RLMSupport.swift這個檔案為 Objective-C 版本的 Realm 集合型別中引入了 Sequence 一致性,並且重新暴露了一些不能夠從 Swift 中進行原生訪問的 Objective-C 方法,例如可變引數 (variadic arguments)。更加詳細的說明見官方文件

安裝方法就4種:

一. Dynamic Framework

注意:動態框架與 iOS 7 不相容,要支援 iOS 7 的話請檢視“靜態框架”。

  1. 下載最新的Realm發行版本,並解壓;
  2. 前往Xcode 工程的”General”設定項中,從ios/dynamic/、osx/、tvos/
    或者watchos/中將’Realm.framework’拖曳到”Embedded Binaries”選項中。確認Copy items if needed被選中後,點選Finish按鈕;
  3. 在單元測試 Target 的”Build Settings”中,在”Framework Search Paths”中新增Realm.framework的上級目錄;
  4. 如果希望使用 Swift 載入 Realm,請拖動Swift/RLMSupport.swift
    檔案到 Xcode 工程的檔案導航欄中並選中Copy items if needed
  5. 如果在 iOS、watchOS 或者 tvOS 專案中使用 Realm,請在您應用目標的”Build Phases”中,建立一個新的”Run Script Phase”,並將

bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework/strip-frameworks.sh"複製程式碼

這條指令碼複製到文字框中。 因為要繞過APP商店提交的bug,這一步在打包通用裝置的二進位制釋出版本時是必須的。

二.CocoaPods

Realm資料庫 從入門到“放棄”

在專案的Podfile中,新增pod 'Realm',在終端執行pod install。

三.Carthage

1.在Carthage 中新增github "realm/realm-cocoa",執行carthage update
。為了修改用以構建專案的 Swift toolchain,通過 --toolchain引數來指定合適的 toolchain。--no-use-binaries引數也是必需的,這可以避免 Carthage 將預構建的 Swift 3.0 二進位制包下載下來。 例如:


carthage update --toolchain com.apple.dt.toolchain.Swift_2_3 --no-use-binaries複製程式碼

2.從 Carthage/Build/目錄下對應平臺資料夾中,將 Realm.framework
拖曳到您 Xcode 工程”General”設定項的”Linked Frameworks and Libraries”選項卡中;

3.iOS/tvOS/watchOS: 在您應用目標的“Build Phases”設定選項卡中,點選“+”按鈕並選擇“New Run Script Phase”。在新建的Run Script中,填寫:


/usr/local/bin/carthage copy-frameworks複製程式碼

在“Input Files”內新增您想要使用的框架路徑,例如:


$(SRCROOT)/Carthage/Build/iOS/Realm.framework複製程式碼

因為要繞過APP商店提交的bug,這一步在打包通用裝置的二進位制釋出版本時是必須的。

四.Static Framework (iOS only)
  1. 下載 Realm 的最新版本並解壓,將 Realm.framework 從 ios/static/資料夾拖曳到您 Xcode 專案中的檔案導航器當中。確保 Copy items if needed 選中然後單擊 Finish
  2. 在 Xcode 檔案導航器中選擇您的專案,然後選擇您的應用目標,進入到 Build Phases 選項卡中。在 Link Binary with Libraries 中單擊 + 號然後新增libc++.dylib

二. Realm 中的相關術語

為了能更好的理解Realm的使用,先介紹一下涉及到的相關術語。

RLMRealm:Realm是框架的核心所在,是我們構建資料庫的訪問點,就如同Core Data的管理物件上下文(managed object context)一樣。出於簡單起見,realm提供了一個預設的defaultRealm( )的便利構造器方法。

RLMObject:這是我們自定義的Realm資料模型。建立資料模型的行為對應的就是資料庫的結構。要建立一個資料模型,我們只需要繼承RLMObject,然後設計我們想要儲存的屬性即可。

關係(Relationships):通過簡單地在資料模型中宣告一個RLMObject型別的屬性,我們就可以建立一個“一對多”的物件關係。同樣地,我們還可以建立“多對一”和“多對多”的關係。

寫操作事務(Write Transactions):資料庫中的所有操作,比如建立、編輯,或者刪除物件,都必須在事務中完成。“事務”是指位於write閉包內的程式碼段。

查詢(Queries):要在資料庫中檢索資訊,我們需要用到“檢索”操作。檢索最簡單的形式是對Realm( )資料庫傳送查詢訊息。如果需要檢索更復雜的資料,那麼還可以使用斷言(predicates)、複合查詢以及結果排序等等操作。

RLMResults:這個類是執行任何查詢請求後所返回的類,其中包含了一系列的RLMObject物件。RLMResults和NSArray類似,我們可以用下標語法來對其進行訪問,並且還可以決定它們之間的關係。不僅如此,它還擁有許多更強大的功能,包括排序、查詢等等操作。

三.Realm 入門——如何使用

由於Realm的API極為友好,一看就懂,所以這裡就按照平時開發的順序,把需要用到的都梳理一遍。

1. 建立資料庫

- (void)creatDataBaseWithName:(NSString *)databaseName
{
    NSArray *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *path = [docPath objectAtIndex:0];
    NSString *filePath = [path stringByAppendingPathComponent:databaseName];
    NSLog(@"資料庫目錄 = %@",filePath);

    RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
    config.fileURL = [NSURL URLWithString:filePath];
    config.objectClasses = @[MyClass.class, MyOtherClass.class];
    config.readOnly = NO;
    int currentVersion = 1.0;
    config.schemaVersion = currentVersion;

    config.migrationBlock = ^(RLMMigration *migration , uint64_t oldSchemaVersion) {
       // 這裡是設定資料遷移的block
        if (oldSchemaVersion < currentVersion) {
        }
    };

    [RLMRealmConfiguration setDefaultConfiguration:config];

}複製程式碼

建立資料庫主要設定RLMRealmConfiguration,設定資料庫名字和儲存地方。把路徑以及資料庫名字拼接好字串,賦值給fileURL即可。

objectClasses這個屬性是用來控制對哪個類能夠儲存在指定 Realm 資料庫中做出限制。例如,如果有兩個團隊分別負責開發您應用中的不同部分,並且同時在應用內部使用了 Realm 資料庫,那麼您肯定不希望為它們協調進行資料遷移您可以通過設定RLMRealmConfiguration的 objectClasses屬性來對類做出限制。objectClasses一般可以不用設定。

readOnly是控制是否只讀屬性。

還有一個很特殊的資料庫,記憶體資料庫。

通常情況下,Realm 資料庫是儲存在硬碟中的,但是您能夠通過設定inMemoryIdentifier而不是設定RLMRealmConfiguration中的 fileURL屬性,以建立一個完全在記憶體中執行的資料庫。


RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];config.inMemoryIdentifier = @"MyInMemoryRealm";RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];複製程式碼

記憶體資料庫在每次程式執行期間都不會儲存資料。但是,這不會妨礙到 Realm 的其他功能,包括查詢、關係以及執行緒安全。

如果需要一種靈活的資料讀寫但又不想儲存資料的方式的話,那麼可以選擇用記憶體資料庫。(關於記憶體資料庫的效能 和 類屬性的 效能,還沒有測試過,感覺效能不會有太大的差異,所以記憶體資料庫使用場景感覺不多)

使用記憶體資料庫需要注意的是:

  1. 記憶體資料庫會在臨時資料夾中建立多個檔案,用來協調處理諸如跨程式通知之類的事務。 實際上沒有任何的資料會被寫入到這些檔案當中,除非作業系統由於記憶體過滿需要清除磁碟上的多餘空間。

  2. 如果某個記憶體 Realm 資料庫例項沒有被引用,那麼所有的資料就會被釋放。所以必須要在應用的生命週期內保持對Realm記憶體資料庫的強引用,以避免資料丟失。

2. 建表

Realm資料模型是基於標準 Objective‑C 類來進行定義的,使用屬性來完成模型的具體定義。

我們只需要繼承 RLMObject或者一個已經存在的模型類,您就可以建立一個新的 Realm 資料模型物件。對應在資料庫裡面就是一張表。


#import <Realm/Realm.h>

@interface RLMUser : RLMObject

@property NSString       *accid;
//使用者註冊id
@property NSInteger      custId;
//姓名
@property NSString       *custName;
//頭像大圖url
@property NSString       *avatarBig;
@property RLMArray<Car> *cars;

RLM_ARRAY_TYPE(RLMUser) // 定義RLMArray<RLMUser>


@interface Car : RLMObject
@property NSString *carName;
@property RLMUser *owner;
@end

RLM_ARRAY_TYPE(Car) // 定義RLMArray<Car>

@end複製程式碼

注意,RLMObject 官方建議不要加上 Objective-C的property attributes(如nonatomic, atomic, strong, copy, weak 等等)假如設定了,這些attributes會一直生效直到RLMObject被寫入realm資料庫。

RLM_ARRAY_TYPE巨集建立了一個協議,從而允許 RLMArray語法的使用。如果該巨集沒有放置在模型介面的底部的話,您或許需要提前宣告該模型類。

關於RLMObject的的關係

1.對一(To-One)關係

對於多對一(many-to-one)或者一對一(one-to-one)關係來說,只需要宣告一個RLMObject子類型別的屬性即可,如上面程式碼例子,@property RLMUser *owner;

2.對多(To-Many)關係
通過 RLMArray型別的屬性您可以定義一個對多關係。如上面程式碼例子,@property RLMArray *cars;

3.反向關係(Inverse Relationship)

連結是單向性的。因此,如果對多關係屬性 RLMUser.cars連結了一個 Car例項,而這個例項的對一關係屬性 Car.owner又連結到了對應的這個 RLMUser例項,那麼實際上這些連結仍然是互相獨立的。



@interface Car : RLMObject
@property NSString *carName;
@property (readonly) RLMLinkingObjects *owners;
@end

@implementation Car
+ (NSDictionary *)linkingObjectsProperties {
    return @{
             @"owners": [RLMPropertyDescriptor descriptorWithClass:RLMUser.class propertyName:@"cars"],
             };
}
@end複製程式碼

這裡可以類比Core Data裡面xcdatamodel檔案裡面那些“箭頭”

Realm資料庫 從入門到“放棄”


@implementation Book

// 主鍵
+ (NSString *)primaryKey {
    return @"ID";
}

//設定屬性預設值
+ (NSDictionary *)defaultPropertyValues{
    return @{@"carName":@"測試" };
}

//設定忽略屬性,即不存到realm資料庫中
+ (NSArray<NSString *> *)ignoredProperties {
    return @[@"ID"];
}

//一般來說,屬性為nil的話realm會丟擲異常,但是如果實現了這個方法的話,就只有name為nil會丟擲異常,也就是說現在cover屬性可以為空了
+ (NSArray *)requiredProperties {
    return @[@"name"];
}

//設定索引,可以加快檢索的速度
+ (NSArray *)indexedProperties {
    return @[@"ID"];
}
@end複製程式碼

還可以給RLMObject設定主鍵primaryKey,預設值defaultPropertyValues,忽略的屬性ignoredProperties,必要屬性requiredProperties,索引indexedProperties。比較有用的是主鍵和索引。

3.儲存資料

新建物件


// (1) 建立一個Car物件,然後設定其屬性
Car *car = [[Car alloc] init];
car.carName = @"Lamborghini";

// (2) 通過字典建立Car物件
Car *myOtherCar = [[Car alloc] initWithValue:@{@"name" : @"Rolls-Royce"}];

// (3) 通過陣列建立狗狗物件
Car *myThirdcar = [[Car alloc] initWithValue:@[@"BMW"]];複製程式碼

注意,所有的必需屬性都必須在物件新增到 Realm 前被賦值

4.增

Realm資料庫 從入門到“放棄”



[realm beginWriteTransaction];
[realm addObject:Car];
[realm commitWriteTransaction];複製程式碼

請注意,如果在程式中存在多個寫入操作的話,那麼單個寫入操作將會阻塞其餘的寫入操作,並且還會鎖定該操作所在的當前執行緒。

Realm這個特性與其他持久化解決方案類似,我們建議您使用該方案常規的最佳做法:將寫入操作轉移到一個獨立的執行緒中執行。

官方給出了一個建議:

由於 Realm 採用了 MVCC 設計架構,讀取操作並不會因為寫入事務正在進行而受到影響。除非您需要立即使用多個執行緒來同時執行寫入操作,不然您應當採用批量化的寫入事務,而不是採用多次少量的寫入事務。


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        RLMRealm *realm = [RLMRealm defaultRealm];
        [realm transactionWithBlock:^{
            [realm addObject: Car];
        }];
    });複製程式碼

上面的程式碼就是把寫事務放到子執行緒中去處理。

5.刪

Realm資料庫 從入門到“放棄”


[realm beginWriteTransaction];
// 刪除單條記錄
[realm deleteObject:Car];
// 刪除多條記錄
[realm deleteObjects:CarResult];
// 刪除所有記錄
[realm deleteAllObjects];

[realm commitWriteTransaction];複製程式碼
6.改

Realm資料庫 從入門到“放棄”

當沒有主鍵的情況下,需要先查詢,再修改資料。
當有主鍵的情況下,有以下幾個非常好用的API


[realm addOrUpdateObject:Car];

[Car createOrUpdateInRealm:realm withValue:@{@"id": @1, @"price": @9000.0f}];複製程式碼

addOrUpdateObject會去先查詢有沒有傳入的Car相同的主鍵,如果有,就更新該條資料。這裡需要注意,addOrUpdateObject這個方法不是增量更新,所有的值都必須有,如果有哪幾個值是null,那麼就會覆蓋原來已經有的值,這樣就會出現資料丟失的問題。

createOrUpdateInRealm:withValue:這個方法是增量更新的,後面傳一個字典,使用這個方法的前提是有主鍵。方法會先去主鍵裡面找有沒有字典裡面傳入的主鍵的記錄,如果有,就只更新字典裡面的子集。如果沒有,就新建一條記錄。

7.查

Realm資料庫 從入門到“放棄”

在Realm中所有的查詢(包括查詢和屬性訪問)在 Realm 中都是延遲載入的,只有當屬性被訪問時,才能夠讀取相應的資料。

查詢結果並不是資料的拷貝:修改查詢結果(在寫入事務中)會直接修改硬碟上的資料。同樣地,您可以直接通過包含在RLMResults
中的RLMObject物件完成遍歷關係圖的操作。除非查詢結果被使用,否則檢索的執行將會被推遲。這意味著連結幾個不同的臨時 {RLMResults
} 來進行排序和匹配資料,不會執行額外的工作,例如處理中間狀態。
一旦檢索執行之後,或者通知模組被新增之後, RLMResults將隨時保持更新,接收 Realm 中,在後臺執行緒上執行的檢索操作中可能所做的更改。


//從預設資料庫查詢所有的車
RLMResults<Car *> *cars = [Car allObjects];

// 使用斷言字串查詢
RLMResults<Dog *> *tanDogs = [Dog objectsWhere:@"color = '棕黃色' AND name BEGINSWITH '大'"];

// 使用 NSPredicate 查詢
NSPredicate *pred = [NSPredicate predicateWithFormat:@"color = %@ AND name BEGINSWITH %@",
                     @"棕黃色", @"大"];
RLMResults *results = [Dog objectsWithPredicate:pred];

// 排序名字以“大”開頭的棕黃色狗狗
RLMResults<Dog *> *sortedDogs = [[Dog objectsWhere:@"color = '棕黃色' AND name BEGINSWITH '大'"] sortedResultsUsingProperty:@"name" ascending:YES];複製程式碼

Realm還能支援鏈式查詢

Realm 查詢引擎一個特性就是它能夠通過非常小的事務開銷來執行鏈式查詢(chain queries),而不需要像傳統資料庫那樣為每個成功的查詢建立一個不同的資料庫伺服器訪問。


RLMResults<Car *> *Cars = [Car objectsWhere:@"color = blue"];
RLMResults<Car *> *CarsWithBNames = [Cars objectsWhere:@"name BEGINSWITH 'B'"];複製程式碼
8.其他相關特性

1.支援KVC和KVO

RLMObject、RLMResult以及 RLMArray
都遵守鍵值編碼(Key-Value Coding)(KVC)機制。當您在執行時才能決定哪個屬性需要更新的時候,這個方法是最有用的。
將 KVC 應用在集合當中是大量更新物件的極佳方式,這樣就可以不用經常遍歷集合,為每個專案建立一個訪問器了。


RLMResults<Person *> *persons = [Person allObjects];
[[RLMRealm defaultRealm] transactionWithBlock:^{ 
    [[persons firstObject] setValue:@YES forKeyPath:@"isFirst"]; // 將每個人的 planet 屬性設定為“地球” 
    [persons setValue:@"地球" forKeyPath:@"planet"];
}];複製程式碼

Realm 物件的大多數屬性都遵從 KVO 機制。所有 RLMObject子類的持久化(persisted)儲存(未被忽略)的屬性都是遵循 KVO 機制的,並且 RLMObject以及 RLMArray中 無效的(invalidated)屬性也同樣遵循(然而 RLMLinkingObjects屬性並不能使用 KVO 進行觀察)。

2.支援資料庫加密


// 產生隨機金鑰
NSMutableData *key = [NSMutableData dataWithLength:64];
SecRandomCopyBytes(kSecRandomDefault, key.length, (uint8_t *)key.mutableBytes);

// 開啟加密檔案
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.encryptionKey = key;
NSError *error = nil;
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
if (!realm) {
    // 如果金鑰錯誤,`error` 會提示資料庫不可訪問
    NSLog(@"Error opening realm: %@", error);
}複製程式碼

Realm 支援在建立 Realm 資料庫時採用64位的金鑰對資料庫檔案進行 AES-256+SHA2 加密。這樣硬碟上的資料都能都採用AES-256來進行加密和解密,並用 SHA-2 HMAC 來進行驗證。每次您要獲取一個 Realm 例項時,您都需要提供一次相同的金鑰。

不過,加密過的 Realm 只會帶來很少的額外資源佔用(通常最多隻會比平常慢10%)。

3.通知


// 獲取 Realm 通知
token = [realm addNotificationBlock:^(NSString *notification, RLMRealm * realm) {
     [myViewController updateUI];
}];

[token stop];

// 移除通知
[realm removeNotification:self.token];複製程式碼

Realm 例項將會在每次寫入事務提交後,給其他執行緒上的 Realm 例項傳送通知。一般控制器如果想一直持有這個通知,就需要申請一個屬性,strong持有這個通知。


- (void)viewDidLoad {
    [super viewDidLoad];

    // 觀察 RLMResults 通知
    __weak typeof(self) weakSelf = self;
    self.notificationToken = [[Person objectsWhere:@"age > 5"] addNotificationBlock:^(RLMResults<Person *> *results, RLMCollectionChange *change, NSError *error) {
        if (error) {
            NSLog(@"Failed to open Realm on background worker: %@", error);
            return;
        }

        UITableView *tableView = weakSelf.tableView;
        // 對於變化資訊來說,檢索的初次執行將會傳遞 nil
        if (!changes) {
            [tableView reloadData];
            return;
        }

        // 檢索結果被改變,因此將它們應用到 UITableView 當中
        [tableView beginUpdates];
        [tableView deleteRowsAtIndexPaths:[changes deletionsInSection:0]
                         withRowAnimation:UITableViewRowAnimationAutomatic];
        [tableView insertRowsAtIndexPaths:[changes insertionsInSection:0]
                         withRowAnimation:UITableViewRowAnimationAutomatic];
        [tableView reloadRowsAtIndexPaths:[changes modificationsInSection:0]
                         withRowAnimation:UITableViewRowAnimationAutomatic];
        [tableView endUpdates];
    }];
}複製程式碼

我們還能進行更加細粒度的通知,用集合通知就可以做到。

集合通知是非同步觸發的,首先它會在初始結果出現的時候觸發,隨後當某個寫入事務改變了集合中的所有或者某個物件的時候,通知都會再次觸發。這些變化可以通過傳遞到通知閉包當的 RLMCollectionChange引數訪問到。這個物件當中包含了受 deletions、insertions和 modifications 狀態所影響的索引資訊。

集合通知對於 RLMResults、RLMArray、RLMLinkingObjects 以及 RLMResults 這些衍生出來的集合來說,當關系中的物件被新增或者刪除的時候,一樣也會觸發這個狀態變化。

4.資料庫遷移

這是Realm的優點之一,方便遷移。

對比Core Data的資料遷移,實在是方便太多了。關於iOS Core Data 資料遷移 指南請看這篇文章

資料庫儲存方面的增刪改查應該都沒有什麼大問題,比較蛋疼的應該就是資料遷移了。在版本迭代過程中,很可能會發生表的新增,刪除,或者表結構的變化,如果新版本中不做資料遷移,使用者升級到新版,很可能就直接crash了。對比Core Data的資料遷移比較複雜,Realm的遷移實在太簡單了。

1.新增刪除表,Realm不需要做遷移
2.新增刪除欄位,Realm不需要做遷移。Realm 會自行檢測新增和需要移除的屬性,然後自動更新硬碟上的資料庫架構。

舉個官方給的資料遷移的例子:


RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 2;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion)
{
    // enumerateObjects:block: 遍歷了儲存在 Realm 檔案中的每一個“Person”物件
    [migration enumerateObjects:Person.className block:^(RLMObject *oldObject, RLMObject *newObject) {
        // 只有當 Realm 資料庫的架構版本為 0 的時候,才新增 “fullName” 屬性
        if (oldSchemaVersion < 1) {
            newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@", oldObject[@"firstName"], oldObject[@"lastName"]];
        }
        // 只有當 Realm 資料庫的架構版本為 0 或者 1 的時候,才新增“email”屬性
        if (oldSchemaVersion < 2) {
            newObject[@"email"] = @"";
        }
       // 替換屬性名
       if (oldSchemaVersion < 3) { // 重新命名操作應該在呼叫 `enumerateObjects:` 之外完成 
            [migration renamePropertyForClass:Person.className oldName:@"yearsSinceBirth" newName:@"age"]; }
    }];
};
[RLMRealmConfiguration setDefaultConfiguration:config];
// 現在我們已經成功更新了架構版本並且提供了遷移閉包,開啟舊有的 Realm 資料庫會自動執行此資料遷移,然後成功進行訪問
[RLMRealm defaultRealm];複製程式碼

在block裡面分別有3種遷移方式,第一種是合併欄位的例子,第二種是增加新欄位的例子,第三種是原欄位重新命名的例子。

四. Realm 使用中可能需要注意的一些問題

Realm資料庫 從入門到“放棄”

在我從0開始接觸Realm到熟練上手,基本就遇到了多執行緒這一個坑。可見Realm的API文件是多麼的友好。雖然坑不多,但是還有有些需要注意的地方。

1.跨執行緒訪問資料庫,Realm物件一定需要新建一個

*** Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'**
***** First throw call stack:**
**(**
** 0   CoreFoundation                      0x000000011479f34b __exceptionPreprocess + 171**
** 1   libobjc.A.dylib                     0x00000001164a321e objc_exception_throw + 48**
** 2   BHFangChuang                        0x000000010dd4c2b5 -[RLMRealm beginWriteTransaction] + 77**
** 3   BHFangChuang                        0x000000010dd4c377 -[RLMRealm transactionWithBlock:error:] + 45**
** 4   BHFangChuang                        0x000000010dd4c348 -[RLMRealm transactionWithBlock:] + 19**
** 5   BHFangChuang                        0x000000010d51d7ae __71-[RealmDataBaseHelper updateUserWithLoginDate:andLogoutDate:according:]_block_invoke + 190**
** 6   libdispatch.dylib                   0x00000001180ef980 _dispatch_call_block_and_release + 12**
** 7   libdispatch.dylib                   0x00000001181190cd _dispatch_client_callout + 8**
** 8   libdispatch.dylib                   0x00000001180f8366 _dispatch_queue_override_invoke + 1426**
** 9   libdispatch.dylib                   0x00000001180fa3b7 _dispatch_root_queue_drain + 720**
** 10  libdispatch.dylib                   0x00000001180fa08b _dispatch_worker_thread3 + 123**
** 11  libsystem_pthread.dylib             0x00000001184c8746 _pthread_wqthread + 1299**
** 12  libsystem_pthread.dylib             0x00000001184c8221 start_wqthread + 13**
**)**
**libc++abi.dylib: terminating with uncaught exception of type NSException**複製程式碼

如果程式崩潰了,出現以上錯誤,那就是因為你訪問Realm資料的時候,使用的Realm物件所在的執行緒和當前執行緒不一致。

解決辦法就是在當前執行緒重新獲取最新的Realm,即可。

2. 自己封裝一個Realm全域性例項單例是沒啥作用的

這個也是我之前對Realm多執行緒理解不清,導致的一個誤解。

很多開發者應該都會對Core Data和Sqlite3或者FMDB,自己封裝一個類似Helper的單例。於是我也在這裡封裝了一個單例,在新建完Realm資料庫的時候strong持有一個Realm的物件。然後之後的訪問中只需要讀取這個單例持有的Realm物件就可以拿到資料庫了。

想法是好的,但是同一個Realm物件是不支援跨執行緒操作realm資料庫的。

Realm 通過確保每個執行緒始終擁有 Realm 的一個快照,以便讓併發執行變得十分輕鬆。你可以同時有任意數目的執行緒訪問同一個 Realm 檔案,並且由於每個執行緒都有對應的快照,因此執行緒之間絕不會產生影響。需要注意的一件事情就是不能讓多個執行緒都持有同一個 Realm 物件的 例項 。如果多個執行緒需要訪問同一個物件,那麼它們分別會獲取自己所需要的例項(否則在一個執行緒上發生的更改就會造成其他執行緒得到不完整或者不一致的資料)。

其實RLMRealm *realm = [RLMRealm defaultRealm]; 這句話就是獲取了當前realm物件的一個例項,其實實現就是拿到單例。所以我們每次在子執行緒裡面不要再去讀取我們自己封裝持有的realm例項了,直接呼叫系統的這個方法即可,能保證訪問不出錯。

3.transactionWithBlock 已經處於一個寫的事務中,事務之間不能巢狀

[realm transactionWithBlock:^{
                [self.realm beginWriteTransaction];
                [self convertToRLMUserWith:bhUser To:[self convertToRLMUserWith:bhUser To:nil]];
                [self.realm commitWriteTransaction];
            }];複製程式碼

transactionWithBlock 已經處於一個寫的事務中,如果還在block裡面再寫一個commitWriteTransaction,就會出錯,寫事務是不能巢狀的。

出錯資訊如下:



*** Terminating app due to uncaught exception 'RLMException', reason: 'The Realm is already in a write transaction'**
***** First throw call stack:**
**(**
** 0   CoreFoundation                      0x0000000112e2d34b __exceptionPreprocess + 171**
** 1   libobjc.A.dylib                     0x0000000114b3121e objc_exception_throw + 48**
** 2   BHFangChuang                        0x000000010c4702b5 -[RLMRealm beginWriteTransaction] + 77**
** 3   BHFangChuang                        0x000000010bc4175a __71-[RealmDataBaseHelper updateUserWithLoginDate:andLogoutDate:according:]_block_invoke_2 + 42**
** 4   BHFangChuang                        0x000000010c470380 -[RLMRealm transactionWithBlock:error:] + 54**
** 5   BHFangChuang                        0x000000010c470348 -[RLMRealm transactionWithBlock:] + 19**
** 6   BHFangChuang                        0x000000010bc416d7 __71-[RealmDataBaseHelper updateUserWithLoginDate:andLogoutDate:according:]_block_invoke + 231**
** 7   libdispatch.dylib                   0x0000000116819980 _dispatch_call_block_and_release + 12**
** 8   libdispatch.dylib                   0x00000001168430cd _dispatch_client_callout + 8**
** 9   libdispatch.dylib                   0x0000000116822366 _dispatch_queue_override_invoke + 1426**
** 10  libdispatch.dylib                   0x00000001168243b7 _dispatch_root_queue_drain + 720**
** 11  libdispatch.dylib                   0x000000011682408b _dispatch_worker_thread3 + 123**
** 12  libsystem_pthread.dylib             0x0000000116bed746 _pthread_wqthread + 1299**
** 13  libsystem_pthread.dylib             0x0000000116bed221 start_wqthread + 13**
**)**
**libc++abi.dylib: terminating with uncaught exception of type NSException**複製程式碼
4.建議每個model都需要設定主鍵,這樣可以方便add和update

如果能設定主鍵,請儘量設定主鍵,因為這樣方便我們更新資料,我們可以很方便的呼叫addOrUpdateObject: 或者 createOrUpdateInRealm:withValue:方法進行更新。這樣就不需要先根據主鍵,查詢出資料,然後再去更新。有了主鍵以後,這兩步操作可以一步完成。

5.查詢也不能跨執行緒查詢


RLMResults * results = [self selectUserWithAccid:bhUser.accid];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        RLMRealm *realm = [RLMRealm defaultRealm];
        [realm transactionWithBlock:^{
            [realm addOrUpdateObject:results[0]];
        }];
    });複製程式碼

由於查詢是在子執行緒外查詢的,所以跨執行緒也會出錯,出錯資訊如下:



***** Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread'**
***** First throw call stack:**
**(**
** 0   CoreFoundation                      0x000000011517a34b __exceptionPreprocess + 171**
** 1   libobjc.A.dylib                     0x0000000116e7e21e objc_exception_throw + 48**
** 2   BHFangChuang                        0x000000010e7c34ab _ZL10throwErrorP8NSString + 129**
** 3   BHFangChuang                        0x000000010e7c177f -[RLMResults count] + 40**
** 4   BHFangChuang                        0x000000010df8f3bf -[RealmDataBaseHelper convertToRLMUserWith:LoginDate:LogoutDate:To:] + 159**
** 5   BHFangChuang                        0x000000010df8efc1 __71-[RealmDataBaseHelper updateUserWithLoginDate:andLogoutDate:according:]_block_invoke_2 + 81**
** 6   BHFangChuang                        0x000000010e7bd320 -[RLMRealm transactionWithBlock:error:] + 54**
** 7   BHFangChuang                        0x000000010e7bd2e8 -[RLMRealm transactionWithBlock:] + 19**
** 8   BHFangChuang                        0x000000010df8eecf __71-[RealmDataBaseHelper updateUserWithLoginDate:andLogoutDate:according:]_block_invoke + 351**
** 9   libdispatch.dylib                   0x0000000118b63980 _dispatch_call_block_and_release + 12**
** 10  libdispatch.dylib                   0x0000000118b8d0cd _dispatch_client_callout + 8**
** 11  libdispatch.dylib                   0x0000000118b6c366 _dispatch_queue_override_invoke + 1426**
** 12  libdispatch.dylib                   0x0000000118b6e3b7 _dispatch_root_queue_drain + 720**
** 13  libdispatch.dylib                   0x0000000118b6e08b _dispatch_worker_thread3 + 123**
** 14  libsystem_pthread.dylib             0x0000000118f3c746 _pthread_wqthread + 1299**
** 15  libsystem_pthread.dylib             0x0000000118f3c221 start_wqthread + 13**
**)**
**libc++abi.dylib: terminating with uncaught exception of type **複製程式碼

五. Realm “放棄”——優點和缺點

關於Realm的優點,在官網上也說了很多了,我感觸最深的3個優點也在文章開頭提到了。

CoreData VS Realm 的對比,可以看看這篇文章

Realm資料庫 從入門到“放棄”

說到使用 Realm最後的二道門檻,一是如何從其他資料庫遷移到Realm,二是Realm資料庫的一些限制。

接下來請還在考慮是否使用Realm的同學仔細看清楚,下面是你需要權衡是否要換到Realm資料庫的重要標準。(以下描述基於Realm最新版 2.0.2)

1.從其他資料庫遷移到Realm

Realm資料庫 從入門到“放棄”

如果從其他資料庫遷移到Realm,請看我之前寫過的一篇文章,簡單的提一下蛋疼的問題,由於切換了資料庫,需要在未來幾個版本都必須維護2套資料庫,因為老使用者的資料需要慢慢從老資料庫遷移到Realm,這個有點蛋疼。遷移資料的那段程式碼需要“噁心”的存在工程裡。但是一旦都遷移完成,之後的路就比較平坦了。

關於Core Data遷移過來沒有fetchedResultController的問題,這裡提一下。由於使用Realm的話就無法使用Core Data的fetchedResultController,那麼如果資料庫更新了資料,是不是隻能通過reloadData來更新tableview了?目前基本上是的,Realm提供了我們通知機制,目前的Realm支援給realm資料庫物件新增通知,這樣就可以在資料庫寫入事務提交後獲取到,從而更新UI;詳情可以參考realm.io/cn/docs/swi…當然如果仍希望使用NSFetchedResultsController的話,那麼推薦使用RBQFetchedResultsController,這是一個替代品,地址是:github.com/Roobiq/RBQF…目前Realm計劃在未來實現類似的效果,具體您可以參見這個PR:github.com/realm/realm…

當然,如果是新的App,還在開發中,可以考慮直接使用Realm,會更爽。

以上是第一道門檻,如果覺得遷移帶來的代價還能承受,那麼恭喜你,已經踏入Realm一半了。那麼還請看第二道“門檻”。

2. Realm資料庫當前版本的限制

把使用者一部分攔在Realm門口的還在這第二道坎,因為這些限制,這些“缺點”,導致App的業務無法使用Realm得到滿足,所以最終放棄了Realm。當然,這些問題,有些是可以靈活通過改變表結構解決的,畢竟人是活的(如果真的想用Realm,想些辦法,誰也攔不住)

Realm資料庫 從入門到“放棄”

1.類名稱的長度最大隻能儲存 57 個 UTF8 字元。

2.屬性名稱的長度最大隻能支援 63 個 UTF8 字元。

3.NSData以及 NSString屬性不能儲存超過 16 MB 大小的資料。如果要儲存大量的資料,可通過將其分解為16MB 大小的塊,或者直接儲存在檔案系統中,然後將檔案路徑儲存在 Realm 中。如果您的應用試圖儲存一個大於 16MB 的單一屬性,系統將在執行時丟擲異常。

4.對字串進行排序以及不區分大小寫查詢只支援“基礎拉丁字符集”、“拉丁字元補充集”、“拉丁文擴充套件字符集 A” 以及”拉丁文擴充套件字符集 B“(UTF-8 的範圍在 0~591 之間)。

5.儘管 Realm 檔案可以被多個執行緒同時訪問,但是您不能跨執行緒處理 Realms、Realm 物件、查詢和查詢結果。(這個其實也不算是個問題,我們在多執行緒中新建新的Realm物件就可以解決)

6.Realm物件的 Setters & Getters 不能被過載

因為 Realm 在底層資料庫中重寫了 setters 和 getters 方法,所以您不可以在您的物件上再對其進行重寫。一個簡單的替代方法就是:建立一個新的 Realm 忽略屬性,該屬性的訪問起可以被重寫, 並且可以呼叫其他的 getter 和 setter 方法。

7.檔案大小 & 版本跟蹤

一般來說 Realm 資料庫比 SQLite 資料庫在硬碟上佔用的空間更少。如果您的 Realm 檔案大小超出了您的想象,這可能是因為您資料庫中的 RLMRealm中包含了舊版本資料。
為了使您的資料有相同的顯示方式,Realm 只在迴圈迭代開始的時候才更新資料版本。這意味著,如果您從 Realm 讀取了一些資料並進行了在一個鎖定的執行緒中進行長時間的執行,然後在其他執行緒進行讀寫 Realm 資料庫的話,那麼版本將不會被更新,Realm 將儲存中間版本的資料,但是這些資料已經沒有用了,這導致了檔案大小的增長。這部分空間會在下次寫入操作時被重複利用。這些操作可以通過呼叫writeCopyToPath:error:來實現。

解決辦法:
通過呼叫invalidate,來告訴 Realm 您不再需要那些拷貝到 Realm 的資料了。這可以使我們不必跟蹤這些物件的中間版本。在下次出現新版本時,再進行版本更新。
您可能在 Realm 使用Grand Central Dispatch時也發現了這個問題。在 dispatch 結束後自動釋放排程佇列(dispatch queue)時,排程佇列(dispatch queue)沒有隨著程式釋放。這造成了直到
RLMRealm 物件被釋放後,Realm 中間版本的資料空間才會被再利用。為了避免這個問題,您應該在 dispatch 佇列中,使用一個顯式的自動排程佇列(dispatch queue)。

8.Realm 沒有自動增長屬性

Realm 沒有執行緒/程式安全的自動增長屬性機制,這在其他資料庫中常常用來產生主鍵。然而,在絕大多數情況下,對於主鍵來說,我們需要的是一個唯一的、自動生成的值,因此沒有必要使用順序的、連續的、整數的 ID 作為主鍵。

解決辦法:

在這種情況下,一個獨一無二的字串主鍵通常就能滿足需求了。一個常見的模式是將預設的屬性值設定為 [[NSUUID UUID] UUIDString]
以產生一個唯一的字串 ID。
自動增長屬性另一種常見的動機是為了維持插入之後的順序。在某些情況下,這可以通過向某個 RLMArray中新增物件,或者使用 [NSDate date]預設值的createdAt屬性。

9.所有的資料模型必須直接繼承自RealmObject。這阻礙我們利用資料模型中的任意型別的繼承。

這一點也不算問題,我們只要自己在建立一個model就可以解決這個問題。自己建立的model可以自己隨意去繼承,這個model專門用來接收網路資料,然後把自己的這個model轉換成要儲存到表裡面的model,即RLMObject物件。這樣這個問題也可以解決了。

Realm 允許模型能夠生成更多的子類,也允許跨模型進行程式碼複用,但是由於某些 Cocoa 特性使得執行時中豐富的類多型無法使用。以下是可以完成的操作:

  • 父類中的類方法,例項方法和屬性可以被它的子類所繼承
  • 子類中可以在方法以及函式中使用父類作為引數

以下是不能完成的:

  • 多型類之間的轉換(例如子類轉換成子類,子類轉換成父類,父類轉換成子類等)
  • 同時對多個類進行檢索
  • 多類容器 (RLMArray以及 RLMResults)

10.Realm不支援集合型別

這一點也是比較蛋疼。

Realm支援以下的屬性型別:BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData以及 被特殊型別標記的NSNumber。CGFloat屬性的支援被取消了,因為它不具備平臺獨立性。

這裡就是不支援集合,比如說NSArray,NSMutableArray,NSDictionary,NSMutableDictionary,NSSet,NSMutableSet。如果伺服器傳來的一個字典,key是一個字串,對應的value就是一個陣列,這時候就想儲存這個陣列就比較困難了。當然Realm裡面是有集合的,就是RLMArray,這裡面裝的都是RLMObject。

所以我們想解決這個問題,就需要把資料裡面的東西都取出來,如果是model,就先自己接收一下,然後轉換成RLMObject的model,再儲存到RLMArray裡面去,這樣轉換一遍,還是可以的做到的。

這裡列出了暫時Realm當前辦法存在的“缺點”,如果這10點,在自己的App上都能滿足業務需求,那麼這一道坎也不是問題了。

以上兩道砍請仔細衡量清楚,這裡還有一篇文章是關於更換資料庫的心得體會的,高速公路換輪胎——為遺留系統替換資料庫考慮更換的同學也可以看看。這兩道坎如果真的不適合,過不去,那麼請放棄Realm吧!

六. Realm 到底是什麼?

Realm資料庫 從入門到“放棄”

大家都知道Sqlite3 是一個移動端上面使用的小型資料庫,FMDB是基於Sqlite3進行的一個封裝。

那Core Data是資料庫麼?
Core Data本身並不是資料庫,它是一個擁有多種功能的框架,其中一個重要的功能就是把應用程式同資料庫之間的互動過程自動化了。有了Core Data框架以後,我們無須編寫Objective-C程式碼,又可以是使用關係型資料庫。因為Core Data會在底層自動給我們生成應該最佳優化過的SQL語句。

那麼Realm是資料庫麼?

Realm資料庫 從入門到“放棄”

Realm 不是 ORM,也不基於 SQLite 建立,而是為移動開發者定製的全功能資料庫。它可以將原生物件直接對映到Realm的資料庫引擎(遠不僅是一個鍵值對儲存)中。

Realm 是一個 MVCC 資料庫 ,底層是用 C++ 編寫的。MVCC 指的是多版本併發控制。

Realm是滿足ACID的。原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、永續性(Durability)。一個支援事務(Transaction)的資料庫,必需要具有這四種特性。Realm都已經滿足。

1.Realm 採用MVCC的設計思想

MVCC 解決了一個重要的併發問題:在所有的資料庫中都有這樣的時候,當有人正在寫資料庫的時候有人又想讀取資料庫了(例如,不同的執行緒可以同時讀取或者寫入同一個資料庫)。這會導致資料的不一致性 - 可能當你讀取記錄的時候一個寫操作才部分結束。

有很多的辦法可以解決讀、寫併發的問題,最常見的就是給資料庫加鎖。在之前的情況下,我們在寫資料的時候就會加上一個鎖。在寫操作完成之前,所有的讀操作都會被阻塞。這就是眾所周知的讀-寫鎖。這常常都會很慢。Realm採用的是MVCC資料庫的優點就展現出來了,速度非常快。

MVCC 在設計上採用了和 Git 一樣的原始檔管理演算法。你可以把 Realm 的內部想象成一個 Git,它也有分支和原子化的提交操作。這意味著你可能工作在許多分支上(資料庫的版本),但是你卻沒有一個完整的資料拷貝。Realm 和真正的 MVCC 資料庫還是有些不同的。一個像 Git 的真正的 MVCC 資料庫,你可以有成為版本樹上 HEAD 的多個候選者。而 Realm 在某個時刻只有一個寫操作,而且總是操作最新的版本 - 它不可以在老的版本上工作。

Realm資料庫 從入門到“放棄”

Realm底層是B+樹實現的,在Realm團隊開源的realm-core裡面可以看到原始碼,裡面有用bpTree,這是一個B+樹的實現。B+ 樹是一種樹資料結構,是一個n叉樹,每個節點通常有多個孩子,一棵B+樹包含根節點、內部節點和葉子節點。根節點可能是一個葉子節點,也可能是一個包含兩個或兩個以上孩子節點的節點。

B+ 樹通常用於資料庫和作業系統的檔案系統中。NTFS, ReiserFS, NSS, XFS, JFS, ReFS 和BFS等檔案系統都在使用B+樹作為後設資料索引。B+ 樹的特點是能夠保持資料穩定有序,其插入與修改擁有較穩定的對數時間複雜度。B+ 樹元素自底向上插入。

Realm資料庫 從入門到“放棄”

Realm會讓每一個連線的執行緒都會有資料在一個特定時刻的快照。這也是為什麼能夠在上百個執行緒中做大量的操作並同時訪問資料庫,卻不會發生崩潰的原因。

Realm資料庫 從入門到“放棄”

上圖很好的展現了Realm的一次寫操作流程。這裡分3個階段,階段一中,V1指向根節點R。在階段二中,準備寫入操作,這個時候會有一個V2節點,指向新的R',並且新建一個分支出來,A'和C'。相應的右孩子指向原來V1指向的R的右孩子。如果寫入操作失敗,就丟棄左邊這個分支。這樣的設計可以保證即使失敗,也僅僅只丟失最新資料,而不會破壞整個資料庫。如果寫入成功,那麼把原來的R,A,C節點放入Garbage中,於是就到了第三階段,寫入成功,變成了V2指向根節點。

在這個寫入的過程中,第二階段是最關鍵的,寫入操作並不會改變原有資料,而是新建了一個新的分支。這樣就不用加鎖,也可以解決資料庫的併發問題。

正是B+樹的底層資料結構 + MVCC的設計,保證了Realm的高效能。

2.Realm 採用了 zero-copy 架構

因為 Realm 採用了 zero-copy 架構,這樣幾乎就沒有記憶體開銷。這是因為每一個 Realm 物件直接通過一個本地 long 指標和底層資料庫對應,這個指標是資料庫中資料的鉤子。

通常的傳統的資料庫操作是這樣的,資料儲存在磁碟的資料庫檔案中,我們的查詢請求會轉換為一系列的SQL語句,建立一個資料庫連線。資料庫伺服器收到請求,通過解析器對SQL語句進行詞法和語法語義分析,然後通過查詢優化器對SQL語句進行優化,優化完成執行對應的查詢,讀取磁碟的資料庫檔案(有索引則先讀索引),讀取命中查詢的每一行的資料,然後存到記憶體裡(這裡有記憶體消耗)。之後你需要把資料序列化成可在記憶體裡面儲存的格式,這意味著位元對齊,這樣 CPU 才能處理它們。最後,資料需要轉換成語言層面的型別,然後它會以物件的形式返回,比如Objective-C的物件等。

這裡就是Realm另外一個很快的原因,Realm的資料庫檔案是通過memory-mapped,也就是說資料庫檔案本身是對映到記憶體(實際上是虛擬記憶體)中的,Realm訪問檔案偏移就好比檔案已經在記憶體中一樣(這裡的記憶體是指虛擬記憶體),它允許檔案在沒有做反序列化的情況下直接從記憶體讀取,提高了讀取效率。Realm 只需要簡單地計算偏移來找到檔案中的資料,然後從原始訪問點返回資料結構的值 。

正是Realm採用了 zero-copy 架構,幾乎沒有記憶體開銷,Realm核心檔案格式基於memory-mapped,節約了大量的序列化和反序列化的開銷,導致了Realm獲取物件的速度特別高效。

3. Realm 物件在不同的執行緒間不能共享

Realm 物件不能線上程間傳遞的原因就是為了保證隔離性和資料一致性。這樣做的目的只有一個,為了速度。

由於Realm是基於零拷貝的,所有物件都在記憶體裡,所以會自動更新。如果允許Realm物件線上程間共享,Realm 會無法確保資料的一致性,因為不同的執行緒會在不確定的什麼時間點同時改變物件的資料。

要想保證多執行緒能共享物件就是加鎖,但是加鎖又會導致一個長時間的後臺寫事務會阻塞 UI 的讀事務。不加鎖就不能保證資料的一致性,但是可以滿足速度的要求。Realm在衡量之後,還是為了速度,做出了不允許執行緒間共享的妥協。

正是因為不允許物件在不同的執行緒間共享,保證了資料的一致性,不加執行緒鎖,保證了Realm的在速度上遙遙領先。

4. 真正的懶載入

大多數資料庫趨向於在水平層級儲存資料,這也就是為什麼你從 SQLite 讀取一個屬性的時候,你就必須要載入整行的資料。它在檔案中是連續儲存的。

不同的是,我們儘可能讓 Realm 在垂直層級連續儲存屬性,你也可以看作是按列儲存。

在查詢到一組資料後,只有當你真正訪問物件的時候才真正載入進來。

5. Realm 中的檔案

Realm資料庫 從入門到“放棄”

先來說說中間的Database File

.realm 檔案是memory mapped的,所有的物件都是檔案首地址偏移量的一個引用。物件的儲存不一定是連續的,但是Array可以保證是連續儲存。

.realm執行寫操作的時候,有3個指標,一個是*current top pointer ,一個是 other top pointer ,最後一個是 switch bit*。

switch bit* 標示著top pointer是否已經被使用過。如果被使用過了,代表著資料庫已經是可讀的。

the top pointer優先更新,緊接著是the switch bit更新。因為即使寫入失敗了,雖然丟失了所有資料,但是這樣能保證資料庫依舊是可讀的。

再來說說 .lock file。

.lock檔案中會包含 the shared group 的metadata。這個檔案承擔著允許多執行緒訪問相同的Realm物件的職責。

最後說說Commit logs history

這個檔案會用來更新索引indexes,會用來同步。裡面主要維護了3個小檔案,2個是資料相關的,1個是操作management的。

總結

經過上面的分析之後,深深的感受到Realm就是為速度而生的!在保證了ACID的要求下,很多設計都是以速度為主。當然,Realm 最核心的理念就是物件驅動,這是 Realm 的核心原則。Realm 本質上是一個嵌入式資料庫,但是它也是看待資料的另一種方式。它用另一種角度來重新看待移動應用中的模型和業務邏輯。

Realm還是跨平臺的,多個平臺都使用相同的資料庫,是多麼好的一件事情呀。相信使用Realm作為App資料庫的開發者會越來越多。

參考連結

Realm官網
Realm官方文件
Realm GitHub

相關文章