因爲最近項目中在用Realm,因此把本身實踐過程當中的一些心得總結分享一下。html
Realm是由Y Combinator公司孵化出來的一款能夠用於iOS(一樣適用於Swift&Objective-C)和Android的跨平臺移動數據庫。目前最新版是Realm 2.0.2,支持的平臺包括Java,Objective-C,Swift,React Native,Xamarin。ios
Realm官網上說了好多優勢,我以爲選用Realm的最吸引人的優勢就三點:c++
跨平臺:如今不少應用都是要兼顧iOS和Android兩個平臺同時開發。若是兩個平臺都能使用相同的數據庫,那就不用考慮內部數據的架構不一樣,使用Realm提供的API,可使數據持久化層在兩個平臺上無差別化的轉換。git
簡單易用:Core Data 和 SQLite 冗餘、繁雜的知識和代碼足以嚇退絕大多數剛入門的開發者,而換用 Realm,則能夠極大地學習成本,當即學會本地化存儲的方法。絕不吹噓的說,把官方最新文檔完整看一遍,就徹底能夠上手開發了。github
可視化:Realm 還提供了一個輕量級的數據庫查看工具,在Mac Appstore 能夠下載「Realm Browser」這個工具,開發者能夠查看數據庫當中的內容,執行簡單的插入和刪除數據的操做。畢竟,不少時候,開發者使用數據庫的理由是由於要提供一些所謂的「知識庫」。算法
若是使用模擬器進行調試,能夠經過數據庫
[RLMRealmConfiguration defaultConfiguration].fileURL複製代碼
打印出Realm 數據庫地址,而後在Finder中⌘⇧G跳轉到對應路徑下,用Realm Browser打開對應的.realm文件就能夠看到數據啦.swift
若是是使用真機調試的話「Xcode->Window->Devices(⌘⇧2)」,而後找到對應的設備與項目,點擊Download Container,導出xcappdata文件後,顯示包內容,進到AppData->Documents,使用Realm Browser打開.realm文件便可.vim
自2012年起, Realm 就已經開始被用於正式的商業產品中了。通過4年的使用,逐步趨於穩定。api
使用 Realm 構建應用的基本要求:
注意 這裏若是是純的OC項目,就安裝OC的Realm,若是是純的Swift項目,就安裝Swift的Realm。若是是混編項目,就須要安裝OC的Realm,而後要把 Swift/RLMSupport.swift 文件一同編譯進去。
RLMSupport.swift這個文件爲 Objective-C 版本的 Realm 集合類型中引入了 Sequence 一致性,而且從新暴露了一些不可以從 Swift 中進行原生訪問的 Objective-C 方法,例如可變參數 (variadic arguments)。更加詳細的說明見官方文檔。
安裝方法就4種:
注意:動態框架與 iOS 7 不兼容,要支持 iOS 7 的話請查看「靜態框架」。
bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework/strip-frameworks.sh"複製代碼
這條腳本複製到文本框中。 由於要繞過APP商店提交的bug,這一步在打包通用設備的二進制發佈版本時是必須的。
在項目的Podfile中,添加pod 'Realm',在終端運行pod install。
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,這一步在打包通用設備的二進制發佈版本時是必須的。
爲了能更好的理解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的API極爲友好,一看就懂,因此這裏就按照平時開發的順序,把須要用到的都梳理一遍。
- (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 的其餘功能,包括查詢、關係以及線程安全。
若是須要一種靈活的數據讀寫但又不想儲存數據的方式的話,那麼能夠選擇用內存數據庫。(關於內存數據庫的性能 和 類屬性的 性能,尚未測試過,感受性能不會有太大的差別,因此內存數據庫使用場景感受很少)
使用內存數據庫須要注意的是:
內存數據庫會在臨時文件夾中建立多個文件,用來協調處理諸如跨進程通知之類的事務。 實際上沒有任何的數據會被寫入到這些文件當中,除非操做系統因爲內存過滿須要清除磁盤上的多餘空間。
若是某個內存 Realm 數據庫實例沒有被引用,那麼全部的數據就會被釋放。因此必需要在應用的生命週期內保持對Realm內存數據庫的強引用,以免數據丟失。
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
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文件裏面那些「箭頭」
@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。比較有用的是主鍵和索引。
新建對象
// (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 前被賦值
[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];
}];
});複製代碼
上面的代碼就是把寫事務放到子線程中去處理。
[realm beginWriteTransaction];
// 刪除單條記錄
[realm deleteObject:Car];
// 刪除多條記錄
[realm deleteObjects:CarResult];
// 刪除全部記錄
[realm deleteAllObjects];
[realm commitWriteTransaction];複製代碼
當沒有主鍵的狀況下,須要先查詢,再修改數據。
當有主鍵的狀況下,有如下幾個很是好用的API
[realm addOrUpdateObject:Car];
[Car createOrUpdateInRealm:realm withValue:@{@"id": @1, @"price": @9000.0f}];複製代碼
addOrUpdateObject會去先查找有沒有傳入的Car相同的主鍵,若是有,就更新該條數據。這裏須要注意,addOrUpdateObject這個方法不是增量更新,全部的值都必須有,若是有哪幾個值是null,那麼就會覆蓋原來已經有的值,這樣就會出現數據丟失的問題。
createOrUpdateInRealm:withValue:這個方法是增量更新的,後面傳一個字典,使用這個方法的前提是有主鍵。方法會先去主鍵裏面找有沒有字典裏面傳入的主鍵的記錄,若是有,就只更新字典裏面的子集。若是沒有,就新建一條記錄。
在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'"];複製代碼
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種遷移方式,第一種是合併字段的例子,第二種是增長新字段的例子,第三種是原字段重命名的例子。
在我從0開始接觸Realm到熟練上手,基本就遇到了多線程這一個坑。可見Realm的API文檔是多麼的友好。雖然坑很少,可是還有有些須要注意的地方。
*** 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,便可。
這個也是我以前對Realm多線程理解不清,致使的一個誤解。
不少開發者應該都會對Core Data和Sqlite3或者FMDB,本身封裝一個相似Helper的單例。因而我也在這裏封裝了一個單例,在新建完Realm數據庫的時候strong持有一個Realm的對象。而後以後的訪問中只須要讀取這個單例持有的Realm對象就能夠拿到數據庫了。
想法是好的,可是同一個Realm對象是不支持跨線程操做realm數據庫的。
Realm 經過確保每一個線程始終擁有 Realm 的一個快照,以便讓併發運行變得十分輕鬆。你能夠同時有任意數目的線程訪問同一個 Realm 文件,而且因爲每一個線程都有對應的快照,所以線程之間毫不會產生影響。須要注意的一件事情就是不能讓多個線程都持有同一個 Realm 對象的 實例 。若是多個線程須要訪問同一個對象,那麼它們分別會獲取本身所須要的實例(不然在一個線程上發生的更改就會形成其餘線程獲得不完整或者不一致的數據)。
其實RLMRealm *realm = [RLMRealm defaultRealm]; 這句話就是獲取了當前realm對象的一個實例,其實實現就是拿到單例。因此咱們每次在子線程裏面不要再去讀取咱們本身封裝持有的realm實例了,直接調用系統的這個方法便可,能保證訪問不出錯。
[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**複製代碼
若是能設置主鍵,請儘可能設置主鍵,由於這樣方便咱們更新數據,咱們能夠很方便的調用addOrUpdateObject: 或者 createOrUpdateInRealm:withValue:方法進行更新。這樣就不須要先根據主鍵,查詢出數據,而後再去更新。有了主鍵之後,這兩步操做能夠一步完成。
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的優勢,在官網上也說了不少了,我感觸最深的3個優勢也在文章開頭提到了。
CoreData VS Realm 的對比,能夠看看這篇文章
說到使用 Realm最後的二道門檻,一是如何從其餘數據庫遷移到Realm,二是Realm數據庫的一些限制。
接下來請還在考慮是否使用Realm的同窗仔細看清楚,下面是你須要權衡是否要換到Realm數據庫的重要標準。(如下描述基於Realm最新版 2.0.2)
若是從其餘數據庫遷移到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一半了。那麼還請看第二道「門檻」。
把用戶一部分攔在Realm門口的還在這第二道坎,由於這些限制,這些「缺點」,致使App的業務沒法使用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 特性使得運行時中豐富的類多態沒法使用。如下是能夠完成的操做:
如下是不能完成的:
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吧!
你們都知道Sqlite3 是一個移動端上面使用的小型數據庫,FMDB是基於Sqlite3進行的一個封裝。
那Core Data是數據庫麼?
Core Data自己並非數據庫,它是一個擁有多種功能的框架,其中一個重要的功能就是把應用程序同數據庫之間的交互過程自動化了。有了Core Data框架之後,咱們無須編寫Objective-C代碼,又能夠是使用關係型數據庫。由於Core Data會在底層自動給咱們生成應該最佳優化過的SQL語句。
那麼Realm是數據庫麼?
Realm 不是 ORM,也不基於 SQLite 建立,而是爲移動開發者定製的全功能數據庫。它能夠將原生對象直接映射到Realm的數據庫引擎(遠不只是一個鍵值對存儲)中。
Realm 是一個 MVCC 數據庫 ,底層是用 C++ 編寫的。MVCC 指的是多版本併發控制。
Realm是知足ACID的。原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)。一個支持事務(Transaction)的數據庫,必須要具備這四種特性。Realm都已經知足。
MVCC 解決了一個重要的併發問題:在全部的數據庫中都有這樣的時候,當有人正在寫數據庫的時候有人又想讀取數據庫了(例如,不一樣的線程能夠同時讀取或者寫入同一個數據庫)。這會致使數據的不一致性 - 可能當你讀取記錄的時候一個寫操做才部分結束。
有不少的辦法能夠解決讀、寫併發的問題,最多見的就是給數據庫加鎖。在以前的狀況下,咱們在寫數據的時候就會加上一個鎖。在寫操做完成以前,全部的讀操做都會被阻塞。這就是衆所周知的讀-寫鎖。這經常都會很慢。Realm採用的是MVCC數據庫的優勢就展示出來了,速度很是快。
MVCC 在設計上採用了和 Git 同樣的源文件管理算法。你能夠把 Realm 的內部想象成一個 Git,它也有分支和原子化的提交操做。這意味着你可能工做在許多分支上(數據庫的版本),可是你卻沒有一個完整的數據拷貝。Realm 和真正的 MVCC 數據庫仍是有些不一樣的。一個像 Git 的真正的 MVCC 數據庫,你能夠有成爲版本樹上 HEAD 的多個候選者。而 Realm 在某個時刻只有一個寫操做,並且老是操做最新的版本 - 它不能夠在老的版本上工做。
Realm底層是B+樹實現的,在Realm團隊開源的realm-core裏面能夠看到源碼,裏面有用bpTree,這是一個B+樹的實現。B+ 樹是一種樹數據結構,是一個n叉樹,每一個節點一般有多個孩子,一棵B+樹包含根節點、內部節點和葉子節點。根節點多是一個葉子節點,也多是一個包含兩個或兩個以上孩子節點的節點。
B+ 樹一般用於數據庫和操做系統的文件系統中。NTFS, ReiserFS, NSS, XFS, JFS, ReFS 和BFS等文件系統都在使用B+樹做爲元數據索引。B+ 樹的特色是可以保持數據穩定有序,其插入與修改擁有較穩定的對數時間複雜度。B+ 樹元素自底向上插入。
Realm會讓每個鏈接的線程都會有數據在一個特定時刻的快照。這也是爲何可以在上百個線程中作大量的操做並同時訪問數據庫,卻不會發生崩潰的緣由。
上圖很好的展示了Realm的一次寫操做流程。這裏分3個階段,階段一中,V1指向根節點R。在階段二中,準備寫入操做,這個時候會有一個V2節點,指向新的R',而且新建一個分支出來,A'和C'。相應的右孩子指向原來V1指向的R的右孩子。若是寫入操做失敗,就丟棄左邊這個分支。這樣的設計能夠保證即便失敗,也僅僅只丟失最新數據,而不會破壞整個數據庫。若是寫入成功,那麼把原來的R,A,C節點放入Garbage中,因而就到了第三階段,寫入成功,變成了V2指向根節點。
在這個寫入的過程當中,第二階段是最關鍵的,寫入操做並不會改變原有數據,而是新建了一個新的分支。這樣就不用加鎖,也能夠解決數據庫的併發問題。
正是B+樹的底層數據結構 + MVCC的設計,保證了Realm的高性能。
由於 Realm 採用了 zero-copy 架構,這樣幾乎就沒有內存開銷。這是由於每個 Realm 對象直接經過一個本地 long 指針和底層數據庫對應,這個指針是數據庫中數據的鉤子。
一般的傳統的數據庫操做是這樣的,數據存儲在磁盤的數據庫文件中,咱們的查詢請求會轉換爲一系列的SQL語句,建立一個數據庫鏈接。數據庫服務器收到請求,經過解析器對SQL語句進行詞法和語法語義分析,而後經過查詢優化器對SQL語句進行優化,優化完成執行對應的查詢,讀取磁盤的數據庫文件(有索引則先讀索引),讀取命中查詢的每一行的數據,而後存到內存裏(這裏有內存消耗)。以後你須要把數據序列化成可在內存裏面存儲的格式,這意味着比特對齊,這樣 CPU 才能處理它們。最後,數據須要轉換成語言層面的類型,而後它會以對象的形式返回,好比Objective-C的對象等。
這裏就是Realm另一個很快的緣由,Realm的數據庫文件是經過memory-mapped,也就是說數據庫文件自己是映射到內存(其實是虛擬內存)中的,Realm訪問文件偏移就比如文件已經在內存中同樣(這裏的內存是指虛擬內存),它容許文件在沒有作反序列化的狀況下直接從內存讀取,提升了讀取效率。Realm 只須要簡單地計算偏移來找到文件中的數據,而後從原始訪問點返回數據結構的值 。
正是Realm採用了 zero-copy 架構,幾乎沒有內存開銷,Realm核心文件格式基於memory-mapped,節約了大量的序列化和反序列化的開銷,致使了Realm獲取對象的速度特別高效。
Realm 對象不能在線程間傳遞的緣由就是爲了保證隔離性和數據一致性。這樣作的目的只有一個,爲了速度。
因爲Realm是基於零拷貝的,全部對象都在內存裏,因此會自動更新。若是容許Realm對象在線程間共享,Realm 會沒法確保數據的一致性,由於不一樣的線程會在不肯定的什麼時間點同時改變對象的數據。
要想保證多線程能共享對象就是加鎖,可是加鎖又會致使一個長時間的後臺寫事務會阻塞 UI 的讀事務。不加鎖就不能保證數據的一致性,可是能夠知足速度的要求。Realm在衡量以後,仍是爲了速度,作出了不容許線程間共享的妥協。
正是由於不容許對象在不一樣的線程間共享,保證了數據的一致性,不加線程鎖,保證了Realm的在速度上遙遙領先。
大多數數據庫趨向於在水平層級存儲數據,這也就是爲何你從 SQLite 讀取一個屬性的時候,你就必需要加載整行的數據。它在文件中是連續存儲的。
不一樣的是,咱們儘量讓 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數據庫的開發者會愈來愈多。
參考連接