iOS学习笔记(11)-Realm初探

作者: __七把刀__ | 来源:发表于2016-07-16 21:50 被阅读4659次

    在raywenderlick上面看到一篇介绍Realm的文章,试用了一下,确实比CoreData方便不少,当然使用Realm会加大app的体积,官方文档上说的是Realm库文件大小在1M左右。这篇笔记对Realm的使用做一个简单的总结,raywenderlick上面的文章我也用Objective-C重新实现了一遍。

    1 Realm架构

    Realm是一个移动端数据库,专门针对移动APP设计,不仅适用于iOS,也适用于Android,目前最新版本是1.0.2,我这用的是0.9.8版本。它底层并不依赖SQLite,有自己的一套存储引擎。官网地址https://realm.io/cn/,有非常详细的中文文档,也支持CocoaPods,所以在项目中只要在Podfile中加入Realm然后 pod install就行。以下代码描述都是针对iOS,其他语言请参照官方文档。

    Realm是一个类MVCC数据库,每个连接的线程在特定的时刻都有一个数据库的快照。MVCC在设计上采用了和Git一样的源文件管理算法,也就是说你的每个连接线程就好比在一个分支(也就是数据库的快照)上工作,但是你并没有得到一个完整的数据库拷贝。Realm和一些真正的MVCC数据库如MySQL是不同的,Real在某个时刻只能有一个写操作,且总是操作最新的数据版本,不能在老版本操作。

    图1 Realm数据库文件管理图示

    Realm数据库使用了零拷贝技术,这是与CoreData及其他数据库完全不同的地方。

    通常的数据库操作是这样的,数据存储在磁盘的数据库文件中,我们的查询请求会转换为一系列的SQL语句,创建一个数据库连接。数据库服务器收到请求,通过解析器对SQL语句进行词法和语法语义分析,然后通过查询优化器对SQL语句进行优化,优化完成执行对应的查询,读取磁盘的数据库文件(有索引则先读索引),返回对应的数据内容并存储到内存中,数据还需要序列化成内存可存储的格式,最后数据还要转换成语言层面的类型,比如Objective-C的对象等。

    而Realm完全不同,它的数据库文件是通过memory-mapped,也就是说数据库文件本身是映射到内存中的,Realm访问文件偏移就好比文件已经在内存中一样(这里的内存是指虚拟内存),它允许文件在没有做反序列化的情况下直接从内存读取,提高了读取效率。

    2 Realm与多线程

    零拷贝架构也使得Realm可以自动更新对象和查询。在一个查询中更新对象,在另外一个查询中可以马上读取到更新的内容。多线程同时更新数据也是一样,可以即时更新对象的内容。正是因为对象的自动更新,所以Realm中也是不允许多线程之间的对象共享,因为如果多线程共享Realm对象,会导致数据的不一致性,虽然通过加锁是可以保证数据一致性的,但是会增加开销。

    因此,在使用Realm的时候,不要在多个线程之间共享对象。如果要在另外一个线程获取同样的数据,请重新执行查询。 多线程更新数据的操作后面会有例子演示。

    3 Realm的几个基本概念

    Realm Objective-C支持的数据类型有BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData 以及 被特殊类型标记的 NSNumber,注意:Realm不支持auto_increment类型。Realm中涉及的几个类和概念如下:

    RLMRealm

    这是Realm数据库框架的核心,它是一个访问底层数据库的指针,有点类似CoreData中的ManagedObjectContext对象。在代码中可以通过 [RLMRealm defaultRealm]获取。

    RLMObject

    这是Realm的对象模型。自己定义的对象要继承该类,然后可以定义自己的属性。也可以定义主键(覆写 + (NSString *)primaryKey方法)和索引(覆写+ (NSArray<NSString *> *)indexedProperties方法)等。

    Relationships

    • 多对一关系或一对一关系

      对于多对一或者一对一的关系,只需要声明一个RLMObject子类类型的属性即可。你可以简单的通过这个属性实现关系的绑定。

        ```
        //多对一或者一对一关系代码示例
        
        // Dog.h
        @interface Dog : RLMObject
        // 其余属性声明...
        @property Person *owner;
        @end
        
        //绑定代码
        Person *jim = [[Person alloc] init];
        Dog    *rex = [[Dog alloc] init];
        rex.owner = jim;
        
        ```
      
    • 一对多关系

      对于一对多的关系,则需要声明一个RLMArray类型的属性。另外,还要在一的类中加入一个协议声明。RLM_ARRAY_TYPE 宏创建了一个协议,从而允许 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];
      
    • 反向关系

      在前面的例子中,加入dog的时候,我们只是绑定了一个人可以有多只狗,但是并没有指定这只狗的主人是谁。为了自动加入狗与人的绑定关系,需要用反向关系来解决,在Dog.h中声明一个RLMLinkingObjects对象,然后在实现代码中加入反向关系的链接函数,这样,当我们在加入dogs的时候,会自动设置好这只狗的主人(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中所有涉及数据更改的操作如insert,delete,update等,必须在一个write事务中执行。

    [[RLMRealm defaultRealm] transactionWithBlock:^{
        ......
    }];
    

    Queries

    查询操作很简单,不需要在一个write事务中执行。Realm所有的查询操作都是延迟加载的,只有当属性被访问的时候,才会读取相应的数据。查询结果并不是数据的拷贝,修改查询结果(在写入事务中)会直接修改硬盘上的数据。

    查询对象的基本方法是 RLMObjectallObjects方法,查询的结果是一个RLMResults<RLMObject *>对象。还可以条件查询并对结果排序,代码如下:

    RLMResults<Dog *> *dogs = [Dog allObjects]; // 从默认的 Realm 数据库中,检索所有狗狗
    
    // 使用断言字符串查询
    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

    与其他数据库不同的是,Realm并没有提供LIMIT之类的关键字来限制一次加载的数据量。因为Realm中的查询是延迟加载的,只有在查询结果被使用到的时候,才会读取数据库文件去加载对象,所以并不用LIMIT来实现分页。

    Migration

    当我们需要修改数据模型时,比如增减属性,属性重命名等,都进行数据迁移。后面的一节会有实例介绍。

    4 实例

    首先创建一个Podfile,下载Realm库。为了查看Realm数据库的数据,推荐下载一个官方的工具Realm Browser,通过这个工具我们只要到对应数据库的目录下,点击default.realm就可以看到数据库的所有数据了。

    #Podfile
    platform :ios, ‘9.0’
    use_frameworks!
    
    target ‘RealmStart’ do
    pod 'Realm', '~> 0.98'
    end
    

    数据库文件的目录可以通过[RLMRealm defaultRealm].configuration.fileURL)得到,然后在文件管理器通过前往对应目录就可以了(注:Mac默认隐藏了用户目录的Library目录,可以通过菜单栏的前往或者快捷键Command+Shift+g打开前往对话框。数据库文件默认是没有加密的,如果需要加密,可以参照官方文档-加密这一节内容。另外,Realm还提供了Xcode插件,可以去官网下载并安装,方便建立数据模型。

    我这里新建了三个类,分布是Dog,Person以及Company。其中Dog和Person是多对多的关系,即一只狗可以属于多个人,而一个人可以有多只狗。Person和Company为多对一的关系,即一个人只能属于一个公司,一个公司可以有很多人。

    代码如下:

    //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中加入测试代码如下:

    /**
    清理数据库文件,为测试环境做准备。
    */
    - (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);
            }
        }
    }
    
    /**
    添加测试数据
    */
    - (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
    */
    
    

    如果我们要修改数据模型,比如Company增加一个属性age,这个时候就需要迁移数据。那么除了在Company.h中加入属性声明外,还要在app开始加入迁移代码,设置数据库版本。迁移数据的代码很简单,如下,每次修改数据模型都要修改版本号。如果是增加索引,测试发现可以不迁移数据,而如果是更改属性名字,则需要加入迁移代码保证新旧属性的数据迁移,更多用法可以参加官网文档。

    - (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 总结

      1. 涉及数据更新的操作必须在transactionWithBlock中完成(或者用
        [realm beginWriteTransaction][realm commitWriteTransaction])。
      1. 注意一对多关系中的协议的宏定义以及反向链接中的变量定义。
      1. 多线程不能共享Realm对象。多线程调用transactionWithBlock更新数据时,后调用的线程会阻塞直到前一个线程完成数据更新,如果是查询数据则不受影响。
    • 4)一个线程的transacWithBlock如果没有执行完成,则数据更新并没有写到磁盘上,因此这个时候在其他线程中看到的数据还是它的快照版本,并不是新的数据。见多线程例子。

    6 代码

    7 参考资料

    相关文章

      网友评论

      • EchooJ:相当给力
        __七把刀__:@linlin泠迹 多谢捧场:grin:
      • Ego_1973:感谢分享👍b( ̄▽ ̄)d
        就是在写属性的时候好像不用写nonatomic,Realm忽略了OC的property attributes(eg:nonatomic,atomic,strong,copy,weak等),假如设置了,这些attributes会一直生效到RLMObject被写入realm数据库
        __七把刀__:@Ego_1973 多谢指正
      • 少年_如他:问下楼主,relam和sqlite哪个效率更高些呢?前台需要处理大型数据..
        少年_如他:@__七把刀__ 😊
        __七把刀__:这个没有实测过,realm号称内存映射文件,可能会高一点吧,具体有待测试哈
      • 春暖花已开:感谢楼主分享!
        __七把刀__:@人民重重 客气客气:smile:
      • e1bbc2cbd095:你好 我想问下,数据库存在自定义的路径上,一直打不开,能告诉下为什么吗?《plesae enter a valid encryption key for this realm file. the encryption key must be entered as a 128-character string of hexadecimal values.》
        __七把刀__:啊哈,没有遇到过这种问题哦
      • 左饵ear:RLMArray has been invalidated or the containing object has been deleted,遇到再删除之后再去添加就崩溃。
        左饵ear:@__七把刀__ 后来是不直接使用RLMArray来显示数据就可以了。
        __七把刀__:@左饵ear 额。。清除数据库重新试试看。
      • visual_:感谢分享。:+1:
        __七把刀__:@MacChark 请多指教。 :smiley:

      本文标题:iOS学习笔记(11)-Realm初探

      本文链接:https://www.haomeiwen.com/subject/xfpujttx.html