Realm是由Y Combinator公司孵化出來的一款可以用于iOS(同樣適用于Swift&Objective-C)和Android的跨平臺移動數(shù)據(jù)庫菇夸。目前最新版是Realm 2.0.2,支持的平臺包括Java,Objective-C共郭,Swift磁玉,React Native驰唬,Xamarin份汗。后面將詳細(xì)介紹我在開發(fā)中使用realm的心得。
Realm官網(wǎng)上說了好多優(yōu)點(diǎn)精拟,我覺得選用Realm的最吸引人的優(yōu)點(diǎn)就三點(diǎn):
跨平臺:現(xiàn)在很多應(yīng)用都是要兼顧iOS和Android兩個平臺同時開發(fā)燎斩。如果兩個平臺都能使用相同的數(shù)據(jù)庫,那就不用考慮內(nèi)部數(shù)據(jù)的架構(gòu)不同蜂绎,使用Realm提供的API栅表,可以使數(shù)據(jù)持久化層在兩個平臺上無差異化的轉(zhuǎn)換。
簡單易用:Core Data 和 SQLite 冗余师枣、繁雜的知識和代碼足以嚇退絕大多數(shù)剛?cè)腴T的開發(fā)者谨读,而換用 Realm,則可以極大地減少學(xué)習(xí)成本坛吁,立即學(xué)會本地化存儲的方法劳殖。毫不吹噓的說铐尚,把官方最新文檔完整看一遍,就完全可以上手開發(fā)了哆姻。
可視化:Realm 還提供了一個輕量級的數(shù)據(jù)庫查看工具宣增,在Mac Appstore 可以下載“Realm Browser”這個工具,開發(fā)者可以查看數(shù)據(jù)庫當(dāng)中的內(nèi)容矛缨,執(zhí)行簡單的插入和刪除數(shù)據(jù)的操作爹脾。畢竟,很多時候箕昭,開發(fā)者使用數(shù)據(jù)庫的理由是因為要提供一些所謂的“知識庫”灵妨。
一. Realm 安裝
使用 Realm 構(gòu)建應(yīng)用的基本要求:
iOS 7 及其以上版本, macOS 10.9 及其以上版本,此外 Realm 支持 tvOS 和 watchOS 的所有版本落竹。
需要使用 Xcode 7.3 或者以后的版本泌霍。
注意
這里如果是純的OC項目,就安裝OC的Realm述召,如果是純的Swift項目朱转,就安裝Swift的Realm。如果是混編項目积暖,就需要安裝OC的Realm藤为,然后要把
文件一同編譯進(jìn)去。
RLMSupport.swift這個文件為 Objective-C 版本的 Realm 集合類型中引入了 Sequence 一致性夺刑,并且重新暴露了一些不能夠從 Swift 中進(jìn)行原生訪問的 Objective-C 方法缅疟,例如可變參數(shù) (variadic arguments)。更加詳細(xì)的說明見官方文檔遍愿。
安裝方法就4種:
一. Dynamic Framework
注意:動態(tài)框架與 iOS 7 不兼容存淫,要支持 iOS 7 的話請查看“靜態(tài)框架”。
下載最新的Realm發(fā)行版本错览,并解壓;
前往Xcode 工程的”General”設(shè)置項中煌往,從ios/dynamic/倾哺、osx/、tvos/
或者watchos/中將’Realm.framework’拖曳到”Embedded Binaries”選項中刽脖。確認(rèn)Copy items if needed被選中后羞海,點(diǎn)擊Finish按鈕;在單元測試 Target 的”Build Settings”中曲管,在”Framework Search Paths”中添加Realm.framework的上級目錄却邓;
如果希望使用 Swift 加載 Realm,請拖動Swift/RLMSupport.swift
文件到 Xcode 工程的文件導(dǎo)航欄中并選中Copy items if needed院水;如果在 iOS腊徙、watchOS 或者 tvOS 項目中使用 Realm简十,請在您應(yīng)用目標(biāo)的”Build Phases”中,創(chuàng)建一個新的”Run Script Phase”撬腾,并將
bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework/strip-frameworks.sh"
這條腳本復(fù)制到文本框中螟蝙。 因為要繞過APP商店提交的bug,這一步在打包通用設(shè)備的二進(jìn)制發(fā)布版本時是必須的民傻。
二.CocoaPods
在項目的Podfile中胰默,添加pod 'Realm',在終端運(yùn)行pod install漓踢。
三.Carthage
1.在Carthage 中添加github "realm/realm-cocoa"牵署,運(yùn)行carthage update。為了修改用以構(gòu)建項目的 Swift toolchain喧半,通過--toolchain參數(shù)來指定合適的 toolchain奴迅。--no-use-binaries參數(shù)也是必需的,這可以避免 Carthage 將預(yù)構(gòu)建的 Swift 3.0 二進(jìn)制包下載下來薯酝。 例如:
arthage update --toolchain com.apple.dt.toolchain.Swift_2_3 --no-use-binaries
2.從 Carthage/Build/目錄下對應(yīng)平臺文件夾中半沽,將 Realm.framework
拖曳到您 Xcode 工程”General”設(shè)置項的”Linked Frameworks and Libraries”選項卡中;
3.iOS/tvOS/watchOS:
在您應(yīng)用目標(biāo)的“Build Phases”設(shè)置選項卡中吴菠,點(diǎn)擊“+”按鈕并選擇“New Run Script Phase”者填。在新建的Run Script中,填寫:
/usr/local/bin/carthage copy-frameworks
在“Input Files”內(nèi)添加您想要使用的框架路徑做葵,例如:
$(SRCROOT)/Carthage/Build/iOS/Realm.framework
因為要繞過APP商店提交的bug占哟,這一步在打包通用設(shè)備的二進(jìn)制發(fā)布版本時是必須的。
四.Static Framework (iOS only)
-
下載
Realm 的最新版本并解壓酿矢,將 Realm.framework 從 ios/static/文件夾拖曳到您 Xcode 項目中的文件導(dǎo)航器當(dāng)中榨乎。確保
Copy items if needed
選中然后單擊
Finish;
-
在 Xcode 文件導(dǎo)航器中選擇您的項目瘫筐,然后選擇您的應(yīng)用目標(biāo)蜜暑,進(jìn)入到 Build Phases
選項卡中。在
Link Binary with Libraries
中單擊 + 號然后添加libc++.dylib策肝;
二. Realm 中的相關(guān)術(shù)語
為了能更好的理解Realm的使用肛捍,先介紹一下涉及到的相關(guān)術(shù)語。
RLMRealm:Realm是框架的核心所在之众,是我們構(gòu)建數(shù)據(jù)庫的訪問點(diǎn)拙毫,就如同Core Data的管理對象上下文(managed object context)一樣。出于簡單起見棺禾,realm提供了一個默認(rèn)的defaultRealm( )的便利構(gòu)造器方法缀蹄。
RLMObject:這是我們自定義的Realm數(shù)據(jù)模型。創(chuàng)建數(shù)據(jù)模型的行為對應(yīng)的就是數(shù)據(jù)庫的結(jié)構(gòu)。要創(chuàng)建一個數(shù)據(jù)模型缺前,我們只需要繼承RLMObject蛀醉,然后設(shè)計我們想要存儲的屬性即可。
關(guān)系(Relationships):通過簡單地在數(shù)據(jù)模型中聲明一個RLMObject類型的屬性诡延,我們就可以創(chuàng)建一個“一對多”的對象關(guān)系滞欠。同樣地,我們還可以創(chuàng)建“多對一”和“多對多”的關(guān)系肆良。
寫操作事務(wù)(Write Transactions):數(shù)據(jù)庫中的所有操作筛璧,比如創(chuàng)建、編輯惹恃,或者刪除對象夭谤,都必須在事務(wù)中完成∥撞冢“事務(wù)”是指位于write閉包內(nèi)的代碼段朗儒。
查詢(Queries):要在數(shù)據(jù)庫中檢索信息,我們需要用到“檢索”操作参淹。檢索最簡單的形式是對Realm( )數(shù)據(jù)庫發(fā)送查詢消息醉锄。如果需要檢索更復(fù)雜的數(shù)據(jù),那么還可以使用斷言(predicates)浙值、復(fù)合查詢以及結(jié)果排序等等操作恳不。
RLMResults:這個類是執(zhí)行任何查詢請求后所返回的類,其中包含了一系列的RLMObject對象开呐。RLMResults和NSArray類似烟勋,我們可以用下標(biāo)語法來對其進(jìn)行訪問,并且還可以決定它們之間的關(guān)系筐付。不僅如此卵惦,它還擁有許多更強(qiáng)大的功能,包括排序瓦戚、查找等等操作沮尿。
三.Realm 入門——如何使用
由于Realm的API極為友好,一看就懂较解,所以這里就按照平時開發(fā)的順序畜疾,把需要用到的都梳理一遍。
1. 創(chuàng)建數(shù)據(jù)庫
- (void)creatDataBaseWithName:(NSString *)databaseName
{
NSArray *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [docPath objectAtIndex:0];
NSString *filePath = [path stringByAppendingPathComponent:databaseName];
NSLog(@"數(shù)據(jù)庫目錄 = %@",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) { // 這里是設(shè)置數(shù)據(jù)遷移的block
if (oldSchemaVersion < currentVersion) {
}
};
[RLMRealmConfiguration setDefaultConfiguration:config];
}
創(chuàng)建數(shù)據(jù)庫主要設(shè)置RLMRealmConfiguration哨坪,設(shè)置數(shù)據(jù)庫名字和存儲地方庸疾。把路徑以及數(shù)據(jù)庫名字拼接好字符串乍楚,賦值給fileURL即可当编。
objectClasses這個屬性是用來控制對哪個類能夠存儲在指定 Realm 數(shù)據(jù)庫中做出限制。例如徒溪,如果有兩個團(tuán)隊分別負(fù)責(zé)開發(fā)您應(yīng)用中的不同部分忿偷,并且同時在應(yīng)用內(nèi)部使用了 Realm 數(shù)據(jù)庫金顿,那么您肯定不希望為它們協(xié)調(diào)進(jìn)行數(shù)據(jù)遷移您可以通過設(shè)置RLMRealmConfiguration的 objectClasses屬性來對類做出限制。objectClasses一般可以不用設(shè)置鲤桥。
readOnly是控制是否只讀屬性揍拆。
還有一個很特殊的數(shù)據(jù)庫,內(nèi)存數(shù)據(jù)庫茶凳。
通常情況下嫂拴,Realm 數(shù)據(jù)庫是存儲在硬盤中的,但是您能夠通過設(shè)置inMemoryIdentifier而不是設(shè)置RLMRealmConfiguration中的 fileURL屬性贮喧,以創(chuàng)建一個完全在內(nèi)存中運(yùn)行的數(shù)據(jù)庫筒狠。
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.inMemoryIdentifier = @"MyInMemoryRealm";
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];
內(nèi)存數(shù)據(jù)庫在每次程序運(yùn)行期間都不會保存數(shù)據(jù)。但是箱沦,這不會妨礙到 Realm 的其他功能辩恼,包括查詢疮跑、關(guān)系以及線程安全融蹂。
如果需要一種靈活的數(shù)據(jù)讀寫但又不想儲存數(shù)據(jù)的方式的話,那么可以選擇用內(nèi)存數(shù)據(jù)庫肚逸。(關(guān)于內(nèi)存數(shù)據(jù)庫的性能 和 類屬性的 性能寒跳,還沒有測試過聘萨,感覺性能不會有太大的差異,所以內(nèi)存數(shù)據(jù)庫使用場景感覺不多)
使用內(nèi)存數(shù)據(jù)庫需要注意的是:
-
內(nèi)存數(shù)據(jù)庫會在臨時文件夾中創(chuàng)建多個文件冯袍,用來協(xié)調(diào)處理諸如跨進(jìn)程通知之類的事務(wù)匈挖。 實(shí)際上沒有任何的數(shù)據(jù)會被寫入到這些文件當(dāng)中,除非操作系統(tǒng)由于內(nèi)存過滿康愤,
需要清除磁盤上的多余空間儡循。
才會去把內(nèi)存里面的數(shù)據(jù)存入到文件中。(感謝 @酷酷的哀殿 指出)
如果某個內(nèi)存 Realm 數(shù)據(jù)庫實(shí)例沒有被引用征冷,那么所有的數(shù)據(jù)就會被釋放择膝。所以必須要在應(yīng)用的生命周期內(nèi)保持對Realm內(nèi)存數(shù)據(jù)庫的強(qiáng)引用,以避免數(shù)據(jù)丟失检激。
2. 建表
Realm數(shù)據(jù)模型是基于標(biāo)準(zhǔn) Objective?C 類來進(jìn)行定義的肴捉,使用屬性來完成模型的具體定義。
我們只需要繼承 RLMObject或者一個已經(jīng)存在的模型類叔收,您就可以創(chuàng)建一個新的 Realm 數(shù)據(jù)模型對象齿穗。對應(yīng)在數(shù)據(jù)庫里面就是一張表。
#import @interface RLMUser : RLMObject
@property NSString *accid;//用戶注冊id
@property NSInteger custId;//姓名
@property NSString *custName;//頭像大圖url
@property NSString *avatarBig;
@property RLMArray *cars;
// 定義RLMArray
@interface Car : RLMObject
@property NSString *carName;
@property RLMUser *owner;
@end
// 定義RLMArray
@end
注意饺律,RLMObject 官方建議不要加上 Objective-C的property attributes(如nonatomic, atomic, strong, copy, weak 等等)假如設(shè)置了窃页,這些attributes會一直生效直到RLMObject被寫入realm數(shù)據(jù)庫。
RLM_ARRAY_TYPE宏創(chuàng)建了一個協(xié)議,從而允許 RLMArray<car>語法的使用脖卖。如果該宏沒有放置在模型接口的底部的話乒省,您或許需要提前聲明該模型類。</car>
關(guān)于RLMObject的的關(guān)系
1.對一(To-One)關(guān)系
對于多對一(many-to-one)或者一對一(one-to-one)關(guān)系來說畦木,只需要聲明一個RLMObject子類類型的屬性即可袖扛,如上面代碼例子,@property RLMUser *owner;
2.對多(To-Many)關(guān)系
通過 RLMArray類型的屬性您可以定義一個對多關(guān)系十籍。如上面代碼例子蛆封,@property RLMArray<car>*cars;</car>
3.反向關(guān)系(Inverse Relationship)
鏈接是單向性的。因此勾栗,如果對多關(guān)系屬性 RLMUser.cars鏈接了一個 Car實(shí)例娶吞,而這個實(shí)例的對一關(guān)系屬性 Car.owner又鏈接到了對應(yīng)的這個 RLMUser實(shí)例,那么實(shí)際上這些鏈接仍然是互相獨(dú)立的械姻。
@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文件里面那些“箭頭”
// 主鍵
+ (NSString *)primaryKey {
return @"ID";
}
//設(shè)置屬性默認(rèn)值
+ (NSDictionary *)defaultPropertyValues
{
return @{@"carName":@"測試" };
}
//設(shè)置忽略屬性,即不存到realm數(shù)據(jù)庫中
+ (NSArray *)ignoredProperties
{
return @[@"ID"];
}
//一般來說,屬性為nil的話realm會拋出異常,但是如果實(shí)現(xiàn)了這個方法的話,就只有name為nil會拋出異常,也就是說現(xiàn)在cover屬性可以為空了
+ (NSArray *)requiredProperties
{
return @[@"name"];
}
//設(shè)置索引,可以加快檢索的速度
+ (NSArray *)indexedProperties {
return @[@"ID"];
}
還可以給RLMObject設(shè)置主鍵primaryKey妒蛇,默認(rèn)值defaultPropertyValues,忽略的屬性ignoredProperties楷拳,必要屬性requiredProperties绣夺,索引indexedProperties。比較有用的是主鍵和索引欢揖。
3.存儲數(shù)據(jù)
新建對象
(1) 創(chuàng)建一個Car對象陶耍,然后設(shè)置其屬性
Car *car = [[Car alloc] init];
car.carName = @"Lamborghini";
(2) 通過字典創(chuàng)建Car對象Car *myOtherCar = [[Car alloc] initWithValue:@{@"name" : @"Rolls-Royce"}];
(3) 通過數(shù)組創(chuàng)建狗狗對象Car *myThirdcar = [[Car alloc] initWithValue:@[@"BMW"]];
注意,所有的必需屬性都必須在對象添加到 Realm 前被賦值
4.增
[realm beginWriteTransaction];
[realm addObject:Car];
[realm commitWriteTransaction];
請注意她混,如果在進(jìn)程中存在多個寫入操作的話烈钞,那么單個寫入操作將會阻塞其余的寫入操作,并且還會鎖定該操作所在的當(dāng)前線程坤按。
Realm這個特性與其他持久化解決方案類似毯欣,我們建議您使用該方案常規(guī)的最佳做法:將寫入操作轉(zhuǎn)移到一個獨(dú)立的線程中執(zhí)行。
官方給出了一個建議:
由于 Realm 采用了 MVCC 設(shè)計架構(gòu)臭脓,讀取操作并不會因為寫入事務(wù)正在進(jìn)行而受到影響酗钞。除非您需要立即使用多個線程來同時執(zhí)行寫入操作,不然您應(yīng)當(dāng)采用批量化的寫入事務(wù)来累,而不是采用多次少量的寫入事務(wù)砚作。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
RLMRealm *realm = [RLMRealm defaultRealm];
[realm transactionWithBlock:^{
[realm addObject: Car];
}];
});
上面的代碼就是把寫事務(wù)放到子線程中去處理。
5.刪
[realm beginWriteTransaction];
// 刪除單條記錄
[realm deleteObject:Car];
// 刪除多條記錄
[realm deleteObjects:CarResult];
// 刪除所有記錄
[realm deleteAllObjects];
[realm commitWriteTransaction];
6.改
當(dāng)沒有主鍵的情況下嘹锁,需要先查詢葫录,再修改數(shù)據(jù)。
當(dāng)有主鍵的情況下领猾,有以下幾個非常好用的API
[realm addOrUpdateObject:Car];
[Car createOrUpdateInRealm:realm withValue:@{@"id": @1, @"price": @9000.0f}];
addOrUpdateObject會去先查找有沒有傳入的Car相同的主鍵米同,如果有求冷,就更新該條數(shù)據(jù)。這里需要注意窍霞,addOrUpdateObject這個方法不是增量更新,所有的值都必須有拯坟,如果有哪幾個值是null但金,那么就會覆蓋原來已經(jīng)有的值,這樣就會出現(xiàn)數(shù)據(jù)丟失的問題郁季。
createOrUpdateInRealm:withValue:這個方法是增量更新的冷溃,后面?zhèn)饕粋€字典,使用這個方法的前提是有主鍵梦裂。方法會先去主鍵里面找有沒有字典里面?zhèn)魅氲闹麈I的記錄似枕,如果有,就只更新字典里面的子集年柠。如果沒有凿歼,就新建一條記錄。
7.查
在Realm中所有的查詢(包括查詢和屬性訪問)在 Realm 中都是延遲加載的冗恨,只有當(dāng)屬性被訪問時答憔,才能夠讀取相應(yīng)的數(shù)據(jù)。
查詢結(jié)果并不是數(shù)據(jù)的拷貝:修改查詢結(jié)果(在寫入事務(wù)中)會直接修改硬盤上的數(shù)據(jù)掀抹。同樣地虐拓,您可以直接通過包含在RLMResults中的RLMObject對象完成遍歷關(guān)系圖的操作。除非查詢結(jié)果被使用傲武,否則檢索的執(zhí)行將會被推遲蓉驹。這意味著鏈接幾個不同的臨時 {RLMResults} 來進(jìn)行排序和匹配數(shù)據(jù),不會執(zhí)行額外的工作揪利,例如處理中間狀態(tài)态兴。
一旦檢索執(zhí)行之后,或者通知模塊被添加之后疟位, RLMResults將隨時保持更新诗茎,接收 Realm 中,在后臺線程上執(zhí)行的檢索操作中可能所做的更改献汗。
//從默認(rèn)數(shù)據(jù)庫查詢所有的車
RLMResults *cars = [Car allObjects];
// 使用斷言字符串查詢
RLMResults *tanDogs = [Dog objectsWhere:@"color = '棕黃色' AND name BEGINSWITH '大'"];
// 使用 NSPredicate 查
NSPredicate *pred = [NSPredicate predicateWithFormat:@"color = %@ AND name BEGINSWITH %@", @"棕黃色", @"大"];
RLMResults *results = [Dog objectsWithPredicate:pred];
// 排序名字以“大”開頭的棕黃色狗狗
RLMResults *sortedDogs = [[Dog objectsWhere:@"color = '棕黃色' AND name BEGINSWITH '大'"] sortedResultsUsingProperty:@"name" ascending:YES];'
Realm還能支持鏈?zhǔn)讲樵?/p>
Realm 查詢引擎一個特性就是它能夠通過非常小的事務(wù)開銷來執(zhí)行鏈?zhǔn)讲樵?chain queries)敢订,而不需要像傳統(tǒng)數(shù)據(jù)庫那樣為每個成功的查詢創(chuàng)建一個不同的數(shù)據(jù)庫服務(wù)器訪問。
RLMResults *Cars = [Car objectsWhere:@"color = blue"];
RLMResults *CarsWithBNames = [Cars objectsWhere:@"name BEGINSWITH 'B'"];
8.其他相關(guān)特性
1.支持KVC和KVO
RLMObject罢吃、RLMResult以及 RLMArray
都遵守鍵值編碼(Key-Value Coding)(KVC)機(jī)制楚午。當(dāng)您在運(yùn)行時才能決定哪個屬性需要更新的時候,這個方法是最有用的尿招。
將 KVC 應(yīng)用在集合當(dāng)中是大量更新對象的極佳方式矾柜,這樣就可以不用經(jīng)常遍歷集合阱驾,為每個項目創(chuàng)建一個訪問器了。
RLMResults *persons = [Person allObjects];
[[RLMRealm defaultRealm] transactionWithBlock:^{
[[persons firstObject] setValue:@YES forKeyPath:@"isFirst"]; // 將每個人的 planet 屬性設(shè)置為“地球”
[persons setValue:@"地球" forKeyPath:@"planet"];
}];
Realm 對象的大多數(shù)屬性都遵從 KVO 機(jī)制怪蔑。所有 RLMObject子類的持久化(persisted)存儲(未被忽略)的屬性都是遵循 KVO 機(jī)制的里覆,并且 RLMObject以及 RLMArray中 無效的(invalidated)屬性也同樣遵循(然而 RLMLinkingObjects屬性并不能使用 KVO 進(jìn)行觀察)。
2.支持?jǐn)?shù)據(jù)庫加密
// 產(chǎn)生隨機(jī)密鑰
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` 會提示數(shù)據(jù)庫不可訪問
NSLog(@"Error opening realm: %@", error);
}
Realm 支持在創(chuàng)建 Realm 數(shù)據(jù)庫時采用64位的密鑰對數(shù)據(jù)庫文件進(jìn)行 AES-256+SHA2 加密喧枷。這樣硬盤上的數(shù)據(jù)都能都采用AES-256來進(jìn)行加密和解密,并用 SHA-2 HMAC 來進(jìn)行驗證弓坞。每次您要獲取一個 Realm 實(shí)例時隧甚,您都需要提供一次相同的密鑰。
不過渡冻,加密過的 Realm 只會帶來很少的額外資源占用(通常最多只會比平常慢10%)戚扳。
3.通知
// 獲取 Realm 通知token = [realm addNotificationBlock:^(NSString *notification, RLMRealm * realm) {
[myViewController updateUI];
}];
[token stop];
// 移除通知
[realm removeNotification:self.token];
Realm 實(shí)例將會在每次寫入事務(wù)提交后,給其他線程上的 Realm 實(shí)例發(fā)送通知族吻。一般控制器如果想一直持有這個通知帽借,就需要申請一個屬性,strong持有這個通知超歌。
- (void)viewDidLoad {
[super viewDidLoad]; // 觀察 RLMResults 通知
__weak typeof(self) weakSelf = self;
self.notificationToken = [[Person objectsWhere:@"age > 5"] addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) { if (error) {
NSLog(@"Failed to open Realm on background worker: %@", error);
return;
}
UITableView *tableView = weakSelf.tableView;
// 對于變化信息來說宜雀,檢索的初次運(yùn)行將會傳遞 nil
if (!changes) {
[tableView reloadData]; return;
} // 檢索結(jié)果被改變,因此將它們應(yīng)用到 UITableView 當(dāng)中
[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];
}];
}
我們還能進(jìn)行更加細(xì)粒度的通知握础,用集合通知就可以做到辐董。
集合通知是異步觸發(fā)的,首先它會在初始結(jié)果出現(xiàn)的時候觸發(fā)禀综,隨后當(dāng)某個寫入事務(wù)改變了集合中的所有或者某個對象的時候简烘,通知都會再次觸發(fā)。這些變化可以通過傳遞到通知閉包當(dāng)?shù)?RLMCollectionChange參數(shù)訪問到定枷。這個對象當(dāng)中包含了受 deletions孤澎、insertions和 modifications 狀態(tài)所影響的索引信息。
集合通知對于 RLMResults欠窒、RLMArray覆旭、RLMLinkingObjects 以及 RLMResults 這些衍生出來的集合來說,當(dāng)關(guān)系中的對象被添加或者刪除的時候岖妄,一樣也會觸發(fā)這個狀態(tài)變化型将。
4.數(shù)據(jù)庫遷移
這是Realm的優(yōu)點(diǎn)之一,方便遷移荐虐。
對比Core Data的數(shù)據(jù)遷移七兜,實(shí)在是方便太多了。關(guān)于iOS Core Data 數(shù)據(jù)遷移 指南請看這篇文章福扬。
數(shù)據(jù)庫存儲方面的增刪改查應(yīng)該都沒有什么大問題腕铸,比較蛋疼的應(yīng)該就是數(shù)據(jù)遷移了惜犀。在版本迭代過程中,很可能會發(fā)生表的新增狠裹,刪除虽界,或者表結(jié)構(gòu)的變化,如果新版本中不做數(shù)據(jù)遷移涛菠,用戶升級到新版莉御,很可能就直接crash了。對比Core Data的數(shù)據(jù)遷移比較復(fù)雜碗暗,Realm的遷移實(shí)在太簡單了。
1.新增刪除表梢夯,Realm不需要做遷移
2.新增刪除字段言疗,Realm不需要做遷移。Realm 會自行檢測新增和需要移除的屬性颂砸,然后自動更新硬盤上的數(shù)據(jù)庫架構(gòu)噪奄。
舉個官方給的數(shù)據(jù)遷移的例子:
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) {
// 只有當(dāng) Realm 數(shù)據(jù)庫的架構(gòu)版本為 0 的時候,才添加 “fullName” 屬性
if (oldSchemaVersion < 1) {
newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@", oldObject[@"firstName"], oldObject[@"lastName"]];
}
// 只有當(dāng) Realm 數(shù)據(jù)庫的架構(gòu)版本為 0 或者 1 的時候人乓,才添加“email”屬性
if (oldSchemaVersion < 2) {
newObject[@"email"] = @"";
}
// 替換屬性名
if (oldSchemaVersion < 3) {
// 重命名操作應(yīng)該在調(diào)用 `enumerateObjects:` 之外完成
[migration renamePropertyForClass:Person.className oldName:@"yearsSinceBirth" newName:@"age"]; }
}];
};
[RLMRealmConfiguration setDefaultConfiguration:config];
// 現(xiàn)在我們已經(jīng)成功更新了架構(gòu)版本并且提供了遷移閉包勤篮,打開舊有的 Realm 數(shù)據(jù)庫會自動執(zhí)行此數(shù)據(jù)遷移,然后成功進(jìn)行訪問[RLMRealm defaultRealm];
在block里面分別有3種遷移方式色罚,第一種是合并字段的例子碰缔,第二種是增加新字段的例子,第三種是原字段重命名的例子戳护。
四. Realm 使用中可能需要注意的一些問題
在我從0開始接觸Realm到熟練上手金抡,基本就遇到了多線程這一個坑‰缜遥可見Realm的API文檔是多么的友好梗肝。雖然坑不多,但是還有有些需要注意的地方铺董。
1.跨線程訪問數(shù)據(jù)庫巫击,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
如果程序崩潰了,出現(xiàn)以上錯誤精续,那就是因為你訪問Realm數(shù)據(jù)的時候坝锰,使用的Realm對象所在的線程和當(dāng)前線程不一致。
解決辦法就是在當(dāng)前線程重新獲取最新的Realm重付,即可什黑。
2. 自己封裝一個Realm全局實(shí)例單例是沒啥作用的
這個也是我之前對Realm多線程理解不清,導(dǎo)致的一個誤解堪夭。
很多開發(fā)者應(yīng)該都會對Core Data和Sqlite3或者FMDB愕把,自己封裝一個類似Helper的單例拣凹。于是我也在這里封裝了一個單例,在新建完Realm數(shù)據(jù)庫的時候strong持有一個Realm的對象恨豁。然后之后的訪問中只需要讀取這個單例持有的Realm對象就可以拿到數(shù)據(jù)庫了嚣镜。
想法是好的,但是同一個Realm對象是不支持跨線程操作realm數(shù)據(jù)庫的橘蜜。
Realm 通過確保每個線程始終擁有 Realm 的一個快照菊匿,以便讓并發(fā)運(yùn)行變得十分輕松。你可以同時有任意數(shù)目的線程訪問同一個 Realm 文件计福,并且由于每個線程都有對應(yīng)的快照跌捆,因此線程之間絕不會產(chǎn)生影響。需要注意的一件事情就是不能讓多個線程都持有同一個 Realm 對象的 實(shí)例 象颖。如果多個線程需要訪問同一個對象佩厚,那么它們分別會獲取自己所需要的實(shí)例(否則在一個線程上發(fā)生的更改就會造成其他線程得到不完整或者不一致的數(shù)據(jù))。
其實(shí)RLMRealm *realm = [RLMRealm defaultRealm]; 這句話就是獲取了當(dāng)前realm對象的一個實(shí)例说订,其實(shí)實(shí)現(xiàn)就是拿到單例抄瓦。所以我們每次在子線程里面不要再去讀取我們自己封裝持有的realm實(shí)例了,直接調(diào)用系統(tǒng)的這個方法即可陶冷,能保證訪問不出錯钙姊。
3.transactionWithBlock 已經(jīng)處于一個寫的事務(wù)中,事務(wù)之間不能嵌套
[realm transactionWithBlock:^{
[self.realm beginWriteTransaction];
[self convertToRLMUserWith:bhUser To:[self convertToRLMUserWith:bhUser To:nil]];
[self.realm commitWriteTransaction];
}];
transactionWithBlock 已經(jīng)處于一個寫的事務(wù)中埂伦,如果還在block里面再寫一個commitWriteTransaction煞额,就會出錯,寫事務(wù)是不能嵌套的沾谜。
出錯信息如下:
*** 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都需要設(shè)置主鍵立镶,這樣可以方便add和update
如果能設(shè)置主鍵,請盡量設(shè)置主鍵类早,因為這樣方便我們更新數(shù)據(jù)媚媒,我們可以很方便的調(diào)用addOrUpdateObject: 或者 createOrUpdateInRealm:withValue:方法進(jìn)行更新。這樣就不需要先根據(jù)主鍵涩僻,查詢出數(shù)據(jù)缭召,然后再去更新。有了主鍵以后逆日,這兩步操作可以一步完成嵌巷。
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 “放棄”——優(yōu)點(diǎn)和缺點(diǎn)
關(guān)于Realm的優(yōu)點(diǎn)搪哪,在官網(wǎng)上也說了很多了,我感觸最深的3個優(yōu)點(diǎn)也在文章開頭提到了坪圾。
CoreData VS Realm 的對比晓折,可以看看這篇文章
說到使用 Realm最后的二道門檻惑朦,一是如何從其他數(shù)據(jù)庫遷移到Realm,二是Realm數(shù)據(jù)庫的一些限制漓概。
接下來請還在考慮是否使用Realm的同學(xué)仔細(xì)看清楚漾月,下面是你需要權(quán)衡是否要換到Realm數(shù)據(jù)庫的重要標(biāo)準(zhǔn)。(以下描述基于Realm最新版 2.0.2)
1.從其他數(shù)據(jù)庫遷移到Realm
如果從其他數(shù)據(jù)庫遷移到Realm胃珍,請看我之前寫過的一篇文章梁肿,簡單的提一下蛋疼的問題,由于切換了數(shù)據(jù)庫觅彰,需要在未來幾個版本都必須維護(hù)2套數(shù)據(jù)庫吩蔑,因為老用戶的數(shù)據(jù)需要慢慢從老數(shù)據(jù)庫遷移到Realm,這個有點(diǎn)蛋疼填抬。遷移數(shù)據(jù)的那段代碼需要“惡心”的存在工程里烛芬。但是一旦都遷移完成,之后的路就比較平坦了痴奏。
關(guān)于Core Data遷移過來沒有fetchedResultController的問題蛀骇,這里提一下厌秒。由于使用Realm的話就無法使用Core Data的fetchedResultController读拆,那么如果數(shù)據(jù)庫更新了數(shù)據(jù),是不是只能通過reloadData來更新tableview了鸵闪?目前基本上是的檐晕,Realm提供了我們通知機(jī)制,目前的Realm支持給realm數(shù)據(jù)庫對象添加通知蚌讼,這樣就可以在數(shù)據(jù)庫寫入事務(wù)提交后獲取到辟灰,從而更新UI;詳情可以參考https://realm.io/cn/docs/swift/latest/#notification當(dāng)然如果仍希望使用NSFetchedResultsController的話篡石,那么推薦使用RBQFetchedResultsController芥喇,這是一個替代品,地址是:https://github.com/Roobiq/RBQFetchedResultsController目前Realm計劃在未來實(shí)現(xiàn)類似的效果凰萨,具體您可以參見這個PR:http://github.com/realm/realm-cocoa/issues/687继控。
當(dāng)然,如果是新的App胖眷,還在開發(fā)中武通,可以考慮直接使用Realm,會更爽珊搀。
以上是第一道門檻冶忱,如果覺得遷移帶來的代價還能承受,那么恭喜你境析,已經(jīng)踏入Realm一半了囚枪。那么還請看第二道“門檻”派诬。
2. Realm數(shù)據(jù)庫當(dāng)前版本的限制
把用戶一部分?jǐn)r在Realm門口的還在這第二道坎,因為這些限制眶拉,這些“缺點(diǎn)”千埃,導(dǎo)致App的業(yè)務(wù)無法使用Realm得到滿足,所以最終放棄了Realm忆植。當(dāng)然放可,這些問題,有些是可以靈活通過改變表結(jié)構(gòu)解決的朝刊,畢竟人是活的(如果真的想用Realm耀里,想些辦法,誰也攔不资懊ァ)
1.類名稱的長度最大只能存儲 57 個 UTF8 字符冯挎。
2.屬性名稱的長度最大只能支持 63 個 UTF8 字符。
3.NSData以及 NSString屬性不能保存超過 16 MB 大小的數(shù)據(jù)咙鞍。如果要存儲大量的數(shù)據(jù)房官,可通過將其分解為16MB 大小的塊,或者直接存儲在文件系統(tǒng)中续滋,然后將文件路徑存儲在 Realm 中翰守。如果您的應(yīng)用試圖存儲一個大于 16MB 的單一屬性,系統(tǒng)將在運(yùn)行時拋出異常疲酌。
4.對字符串進(jìn)行排序以及不區(qū)分大小寫查詢只支持“基礎(chǔ)拉丁字符集”蜡峰、“拉丁字符補(bǔ)充集”、“拉丁文擴(kuò)展字符集 A” 以及”拉丁文擴(kuò)展字符集 B“(UTF-8 的范圍在 0~591 之間)朗恳。
5.盡管 Realm 文件可以被多個線程同時訪問湿颅,但是您不能跨線程處理 Realms、Realm 對象粥诫、查詢和查詢結(jié)果油航。(這個其實(shí)也不算是個問題,我們在多線程中新建新的Realm對象就可以解決)
6.Realm對象的 Setters & Getters 不能被重載
因為 Realm 在底層數(shù)據(jù)庫中重寫了 setters 和 getters 方法怀浆,所以您不可以在您的對象上再對其進(jìn)行重寫谊囚。一個簡單的替代方法就是:創(chuàng)建一個新的 Realm 忽略屬性,該屬性的訪問起可以被重寫揉稚, 并且可以調(diào)用其他的 getter 和 setter 方法秒啦。
7.文件大小 & 版本跟蹤
一般來說 Realm 數(shù)據(jù)庫比 SQLite 數(shù)據(jù)庫在硬盤上占用的空間更少。如果您的 Realm 文件大小超出了您的想象搀玖,這可能是因為您數(shù)據(jù)庫中的 RLMRealm中包含了舊版本數(shù)據(jù)余境。
為了使您的數(shù)據(jù)有相同的顯示方式,Realm 只在循環(huán)迭代開始的時候才更新數(shù)據(jù)版本。這意味著芳来,如果您從 Realm 讀取了一些數(shù)據(jù)并進(jìn)行了在一個鎖定的線程中進(jìn)行長時間的運(yùn)行含末,然后在其他線程進(jìn)行讀寫 Realm 數(shù)據(jù)庫的話,那么版本將不會被更新即舌,Realm 將保存中間版本的數(shù)據(jù)佣盒,但是這些數(shù)據(jù)已經(jīng)沒有用了,這導(dǎo)致了文件大小的增長顽聂。這部分空間會在下次寫入操作時被重復(fù)利用肥惭。這些操作可以通過調(diào)用writeCopyToPath:error:來實(shí)現(xiàn)。
解決辦法:
通過調(diào)用invalidate紊搪,來告訴 Realm 您不再需要那些拷貝到 Realm 的數(shù)據(jù)了蜜葱。這可以使我們不必跟蹤這些對象的中間版本。在下次出現(xiàn)新版本時耀石,再進(jìn)行版本更新牵囤。
您可能在 Realm 使用Grand Central Dispatch時也發(fā)現(xiàn)了這個問題。在 dispatch 結(jié)束后自動釋放調(diào)度隊列(dispatch queue)時滞伟,調(diào)度隊列(dispatch queue)沒有隨著程序釋放揭鳞。這造成了直到
RLMRealm 對象被釋放后,Realm 中間版本的數(shù)據(jù)空間才會被再利用梆奈。為了避免這個問題野崇,您應(yīng)該在 dispatch 隊列中,使用一個顯式的自動調(diào)度隊列(dispatch queue)鉴裹。
8.Realm 沒有自動增長屬性
Realm 沒有線程/進(jìn)程安全的自動增長屬性機(jī)制舞骆,這在其他數(shù)據(jù)庫中常常用來產(chǎn)生主鍵钥弯。然而径荔,在絕大多數(shù)情況下,對于主鍵來說脆霎,我們需要的是一個唯一的总处、自動生成的值,因此沒有必要使用順序的睛蛛、連續(xù)的鹦马、整數(shù)的 ID 作為主鍵。
解決辦法:
在這種情況下忆肾,一個獨(dú)一無二的字符串主鍵通常就能滿足需求了荸频。一個常見的模式是將默認(rèn)的屬性值設(shè)置為 [[NSUUID UUID] UUIDString]
以產(chǎn)生一個唯一的字符串 ID。
自動增長屬性另一種常見的動機(jī)是為了維持插入之后的順序客冈。在某些情況下旭从,這可以通過向某個 RLMArray中添加對象,或者使用 [NSDate date]默認(rèn)值的createdAt屬性。
9.所有的數(shù)據(jù)模型必須直接繼承自RealmObject和悦。這阻礙我們利用數(shù)據(jù)模型中的任意類型的繼承退疫。
這一點(diǎn)也不算問題,我們只要自己在建立一個model就可以解決這個問題鸽素。自己建立的model可以自己隨意去繼承褒繁,這個model專門用來接收網(wǎng)絡(luò)數(shù)據(jù),然后把自己的這個model轉(zhuǎn)換成要存儲到表里面的model馍忽,即RLMObject對象棒坏。這樣這個問題也可以解決了。
Realm 允許模型能夠生成更多的子類遭笋,也允許跨模型進(jìn)行代碼復(fù)用俊抵,但是由于某些 Cocoa 特性使得運(yùn)行時中豐富的類多態(tài)無法使用。以下是可以完成的操作:
父類中的類方法坐梯,實(shí)例方法和屬性可以被它的子類所繼承
子類中可以在方法以及函數(shù)中使用父類作為參數(shù)
以下是不能完成的:
多態(tài)類之間的轉(zhuǎn)換(例如子類轉(zhuǎn)換成子類徽诲,子類轉(zhuǎn)換成父類,父類轉(zhuǎn)換成子類等)
同時對多個類進(jìn)行檢索
多類容器 (RLMArray以及 RLMResults)
10.Realm不支持集合類型
這一點(diǎn)也是比較蛋疼吵血。
Realm支持以下的屬性類型:BOOL谎替、bool、int蹋辅、NSInteger钱贯、long、long long侦另、float秩命、double、NSString褒傅、NSDate弃锐、NSData以及
被特殊類型標(biāo)記的NSNumber。CGFloat屬性的支持被取消了殿托,因為它不具備平臺獨(dú)立性霹菊。
這里就是不支持集合,比如說NSArray支竹,NSMutableArray旋廷,NSDictionary,NSMutableDictionary礼搁,NSSet饶碘,NSMutableSet。如果服務(wù)器傳來的一個字典馒吴,key是一個字符串扎运,對應(yīng)的value就是一個數(shù)組卑雁,這時候就想存儲這個數(shù)組就比較困難了。當(dāng)然Realm里面是有集合的绪囱,就是RLMArray测蹲,這里面裝的都是RLMObject。
所以我們想解決這個問題鬼吵,就需要把數(shù)據(jù)里面的東西都取出來扣甲,如果是model,就先自己接收一下齿椅,然后轉(zhuǎn)換成RLMObject的model琉挖,再存儲到RLMArray里面去奉瘤,這樣轉(zhuǎn)換一遍六孵,還是可以的做到的。
這里列出了暫時Realm當(dāng)前辦法存在的“缺點(diǎn)”掌桩,如果這10點(diǎn)遣蚀,在自己的App上都能滿足業(yè)務(wù)需求矾麻,那么這一道坎也不是問題了。
以上兩道砍請仔細(xì)衡量清楚芭梯,這里還有一篇文章是關(guān)于更換數(shù)據(jù)庫的心得體會的险耀,高速公路換輪胎——為遺留系統(tǒng)替換數(shù)據(jù)庫考慮更換的同學(xué)也可以看看。這兩道坎如果真的不適合玖喘,過不去甩牺,那么請放棄Realm吧!
六. Realm 到底是什么累奈?
大家都知道Sqlite3 是一個移動端上面使用的小型數(shù)據(jù)庫贬派,F(xiàn)MDB是基于Sqlite3進(jìn)行的一個封裝。
那Core Data是數(shù)據(jù)庫么澎媒?
Core Data本身并不是數(shù)據(jù)庫搞乏,它是一個擁有多種功能的框架,其中一個重要的功能就是把應(yīng)用程序同數(shù)據(jù)庫之間的交互過程自動化了旱幼。有了Core Data框架以后查描,我們無須編寫Objective-C代碼突委,又可以是使用關(guān)系型數(shù)據(jù)庫柏卤。因為Core Data會在底層自動給我們生成應(yīng)該最佳優(yōu)化過的SQL語句。
那么Realm是數(shù)據(jù)庫么匀油?
Realm 不是 ORM缘缚,也不基于 SQLite 創(chuàng)建,而是為移動開發(fā)者定制的全功能數(shù)據(jù)庫敌蚜。它可以將原生對象直接映射到Realm的數(shù)據(jù)庫引擎(遠(yuǎn)不僅是一個鍵值對存儲)中桥滨。
Realm 是一個
,底層是用 C++ 編寫的。MVCC 指的是多版本并發(fā)控制齐媒。
Realm是滿足ACID的蒲每。原子性(Atomicity)、一致性(Consistency)喻括、隔離性(Isolation)邀杏、持久性(Durability)。一個支持事務(wù)(Transaction)的數(shù)據(jù)庫唬血,必需要具有這四種特性望蜡。Realm都已經(jīng)滿足。
1.Realm 采用MVCC的設(shè)計思想
MVCC 解決了一個重要的并發(fā)問題:在所有的數(shù)據(jù)庫中都有這樣的時候拷恨,當(dāng)有人正在寫數(shù)據(jù)庫的時候有人又想讀取數(shù)據(jù)庫了(例如脖律,不同的線程可以同時讀取或者寫入同一個數(shù)據(jù)庫)。這會導(dǎo)致數(shù)據(jù)的不一致性 - 可能當(dāng)你讀取記錄的時候一個寫操作才部分結(jié)束腕侄。
有很多的辦法可以解決讀小泉、寫并發(fā)的問題,最常見的就是給數(shù)據(jù)庫加鎖冕杠。在之前的情況下膏孟,我們在寫數(shù)據(jù)的時候就會加上一個鎖。在寫操作完成之前拌汇,所有的讀操作都會被阻塞柒桑。這就是眾所周知的讀-寫鎖。這常常都會很慢噪舀。Realm采用的是MVCC數(shù)據(jù)庫的優(yōu)點(diǎn)就展現(xiàn)出來了魁淳,速度非常快与倡。
MVCC 在設(shè)計上采用了和 Git 一樣的源文件管理算法界逛。你可以把 Realm 的內(nèi)部想象成一個 Git,它也有分支和原子化的提交操作纺座。這意味著你可能工作在許多分支上(數(shù)據(jù)庫的版本)息拜,但是你卻沒有一個完整的數(shù)據(jù)拷貝。Realm 和真正的 MVCC 數(shù)據(jù)庫還是有些不同的净响。一個像 Git 的真正的 MVCC 數(shù)據(jù)庫少欺,你可以有成為版本樹上 HEAD 的多個候選者。而 Realm 在某個時刻只有一個寫操作馋贤,而且總是操作最新的版本 - 它不可以在老的版本上工作赞别。
Realm底層是B+樹實(shí)現(xiàn)的,在Realm團(tuán)隊開源的realm-core里面可以看到源碼配乓,里面有用bpTree仿滔,這是一個B+樹的實(shí)現(xiàn)惠毁。B+ 樹是一種樹數(shù)據(jù)結(jié)構(gòu),是一個n叉樹崎页,每個節(jié)點(diǎn)通常有多個孩子鞠绰,一棵B+樹包含根節(jié)點(diǎn)、內(nèi)部節(jié)點(diǎn)和葉子節(jié)點(diǎn)飒焦。根節(jié)點(diǎn)可能是一個葉子節(jié)點(diǎn)洞豁,也可能是一個包含兩個或兩個以上孩子節(jié)點(diǎn)的節(jié)點(diǎn)。
B+ 樹通常用于數(shù)據(jù)庫和操作系統(tǒng)的文件系統(tǒng)中荒给。NTFS, ReiserFS, NSS, XFS, JFS, ReFS 和BFS等文件系統(tǒng)都在使用B+樹作為元數(shù)據(jù)索引丈挟。B+ 樹的特點(diǎn)是能夠保持?jǐn)?shù)據(jù)穩(wěn)定有序,其插入與修改擁有較穩(wěn)定的對數(shù)時間復(fù)雜度志电。B+ 樹元素自底向上插入曙咽。
Realm會讓每一個連接的線程都會有數(shù)據(jù)在一個特定時刻的快照。這也是為什么能夠在上百個線程中做大量的操作并同時訪問數(shù)據(jù)庫挑辆,卻不會發(fā)生崩潰的原因例朱。
上圖很好的展現(xiàn)了Realm的一次寫操作流程。這里分3個階段鱼蝉,階段一中洒嗤,V1指向根節(jié)點(diǎn)R。在階段二中魁亦,準(zhǔn)備寫入操作渔隶,這個時候會有一個V2節(jié)點(diǎn),指向新的R'洁奈,并且新建一個分支出來间唉,A'和C'。相應(yīng)的右孩子指向原來V1指向的R的右孩子利术。如果寫入操作失敗呈野,就丟棄左邊這個分支。這樣的設(shè)計可以保證即使失敗印叁,也僅僅只丟失最新數(shù)據(jù)被冒,而不會破壞整個數(shù)據(jù)庫。如果寫入成功轮蜕,那么把原來的R昨悼,A,C節(jié)點(diǎn)放入Garbage中肠虽,于是就到了第三階段幔戏,寫入成功,變成了V2指向根節(jié)點(diǎn)税课。
在這個寫入的過程中闲延,第二階段是最關(guān)鍵的,寫入操作并不會改變原有數(shù)據(jù)韩玩,而是新建了一個新的分支垒玲。這樣就不用加鎖,也可以解決數(shù)據(jù)庫的并發(fā)問題找颓。
正是B+樹的底層數(shù)據(jù)結(jié)構(gòu) + MVCC的設(shè)計合愈,保證了Realm的高性能。
2.Realm 采用了 zero-copy 架構(gòu)
因為 Realm 采用了 zero-copy 架構(gòu)击狮,這樣幾乎就沒有內(nèi)存開銷佛析。這是因為每一個 Realm 對象直接通過一個本地 long 指針和底層數(shù)據(jù)庫對應(yīng),這個指針是數(shù)據(jù)庫中數(shù)據(jù)的鉤子彪蓬。
通常的傳統(tǒng)的數(shù)據(jù)庫操作是這樣的寸莫,數(shù)據(jù)存儲在磁盤的數(shù)據(jù)庫文件中,我們的查詢請求會轉(zhuǎn)換為一系列的SQL語句档冬,創(chuàng)建一個數(shù)據(jù)庫連接膘茎。數(shù)據(jù)庫服務(wù)器收到請求,通過解析器對SQL語句進(jìn)行詞法和語法語義分析酷誓,然后通過查詢優(yōu)化器對SQL語句進(jìn)行優(yōu)化披坏,優(yōu)化完成執(zhí)行對應(yīng)的查詢,讀取磁盤的數(shù)據(jù)庫文件(有索引則先讀索引)盐数,讀取命中查詢的每一行的數(shù)據(jù)棒拂,然后存到內(nèi)存里(這里有內(nèi)存消耗)。之后你需要把數(shù)據(jù)序列化成可在內(nèi)存里面存儲的格式玫氢,這意味著比特對齊着茸,這樣 CPU 才能處理它們。最后琐旁,數(shù)據(jù)需要轉(zhuǎn)換成語言層面的類型涮阔,然后它會以對象的形式返回,比如Objective-C的對象等灰殴。
這里就是Realm另外一個很快的原因敬特,Realm的數(shù)據(jù)庫文件是通過memory-mapped,也就是說數(shù)據(jù)庫文件本身是映射到內(nèi)存(實(shí)際上是虛擬內(nèi)存)中的牺陶,Realm訪問文件偏移就好比文件已經(jīng)在內(nèi)存中一樣(這里的內(nèi)存是指虛擬內(nèi)存)伟阔,它允許文件在沒有做反序列化的情況下直接從內(nèi)存讀取,提高了讀取效率掰伸。Realm 只需要簡單地計算偏移來找到文件中的數(shù)據(jù)皱炉,然后從原始訪問點(diǎn)返回數(shù)據(jù)結(jié)構(gòu)的值 。
正是Realm采用了 zero-copy 架構(gòu)狮鸭,幾乎沒有內(nèi)存開銷合搅,Realm核心文件格式基于memory-mapped多搀,節(jié)約了大量的序列化和反序列化的開銷,導(dǎo)致了Realm獲取對象的速度特別高效灾部。
3. Realm 對象在不同的線程間不能共享
Realm 對象不能在線程間傳遞的原因就是為了保證隔離性和數(shù)據(jù)一致性康铭。這樣做的目的只有一個,為了速度赌髓。
由于Realm是基于零拷貝的从藤,所有對象都在內(nèi)存里,所以會自動更新锁蠕。如果允許Realm對象在線程間共享夷野,Realm 會無法確保數(shù)據(jù)的一致性,因為不同的線程會在不確定的什么時間點(diǎn)同時改變對象的數(shù)據(jù)荣倾。
要想保證多線程能共享對象就是加鎖悯搔,但是加鎖又會導(dǎo)致一個長時間的后臺寫事務(wù)會阻塞 UI 的讀事務(wù)。不加鎖就不能保證數(shù)據(jù)的一致性逃呼,但是可以滿足速度的要求鳖孤。Realm在衡量之后,還是為了速度抡笼,做出了不允許線程間共享的妥協(xié)苏揣。
正是因為不允許對象在不同的線程間共享,保證了數(shù)據(jù)的一致性推姻,不加線程鎖平匈,保證了Realm的在速度上遙遙領(lǐng)先。
4. 真正的懶加載
大多數(shù)數(shù)據(jù)庫趨向于在水平層級存儲數(shù)據(jù)藏古,這也就是為什么你從 SQLite 讀取一個屬性的時候增炭,你就必須要加載整行的數(shù)據(jù)。它在文件中是連續(xù)存儲的拧晕。
不同的是隙姿,Realm盡可能讓 Realm 在垂直層級連續(xù)存儲屬性,你也可以看作是按列存儲厂捞。
在查詢到一組數(shù)據(jù)后输玷,只有當(dāng)你真正訪問對象的時候才真正加載進(jìn)來。
5. Realm 中的文件
先來說說中間的Database File
.realm 文件是memory mapped的靡馁,所有的對象都是文件首地址偏移量的一個引用欲鹏。對象的存儲不一定是連續(xù)的,但是Array可以保證是連續(xù)存儲臭墨。
.realm執(zhí)行寫操作的時候赔嚎,有3個指針,一個是current top pointer ,一個是 other top pointer 尤误,最后一個是 switch bit侠畔。
switch bit* 標(biāo)示著top pointer是否已經(jīng)被使用過。如果被使用過了袄膏,代表著數(shù)據(jù)庫已經(jīng)是可讀的践图。
the top pointer優(yōu)先更新掺冠,緊接著是the switch bit更新沉馆。因為即使寫入失敗了,雖然丟失了所有數(shù)據(jù)德崭,但是這樣能保證數(shù)據(jù)庫依舊是可讀的斥黑。
再來說說 .lock file。
.lock文件中會包含 the shared group 的metadata眉厨。這個文件承擔(dān)著允許多線程訪問相同的Realm對象的職責(zé)锌奴。
最后說說Commit logs history
這個文件會用來更新索引indexes,會用來同步憾股。里面主要維護(hù)了3個小文件鹿蜀,2個是數(shù)據(jù)相關(guān)的,1個是操作management的服球。
總結(jié)
經(jīng)過上面的分析之后茴恰,深深的感受到Realm就是為速度而生的!在保證了ACID的要求下斩熊,很多設(shè)計都是以速度為主往枣。當(dāng)然,Realm 最核心的理念就是對象驅(qū)動粉渠,這是 Realm 的核心原則分冈。Realm 本質(zhì)上是一個嵌入式數(shù)據(jù)庫,但是它也是看待數(shù)據(jù)的另一種方式霸株。它用另一種角度來重新看待移動應(yīng)用中的模型和業(yè)務(wù)邏輯雕沉。
Realm還是跨平臺的,多個平臺都使用相同的數(shù)據(jù)庫去件,是多么好的一件事情呀坡椒。相信使用Realm作為App數(shù)據(jù)庫的開發(fā)者會越來越多。
參考鏈接