Realm数据查询修改踩坑之路

由于项目开始阶段为了图方便,所以数据存本地都采用了归档的方式,归档这种方式,操作简便,代码也很快,但是缺点就是稍微有点改动就需要把所有数据再归档一遍,费时费力,针对少量数据可以这么做,但是随着归档的数据越来越多,每次归档的时间也就越来越长了,所以数据库替换是势在必行;
由于之前都是对模型归档,所以进行数据库升级主要考虑的是CoreData跟Realm,经过一番比对之后,还是选择了Realm,它的优势就不多说了,支持OC、Swift、java,可以同时跨Android、iOS使用,具体可以参考下面几个帖子;
https://www.jianshu.com/p/5c5931f61600
https://blog.csdn.net/weixin_33691817/article/details/87958683
https://www.jianshu.com/p/50e0efb66bdf
这边主要对替换数据库遇到的几个问题做一下简单的记录;

1、数据类型问题

Realm支持的数据类型:
BOOL、int、NSInteger、long、long long、float、double、NSString、NSDate、NSNumber ;
所以对于CGRect、UIImage等数据都不支持,需要转换一下

- (void)setContentFrame:(CGRect)contentFrame {
    _contentFrame = contentFrame;
    _x = contentFrame.origin.x;
    _y = contentFrame.origin.y;
    _w = contentFrame.size.width;
    _h = contentFrame.size.height;
}

- (CGRect)contentFrame {
    if (CGRectEqualToRect(_contentFrame, CGRectZero) && _w > 0) {
        _contentFrame = CGRectMake(_x, _y, _w, _h);
    }
    return _contentFrame;
}

- (UIImage *)sourceImage {
    if (!_sourceImage && _sourceData) {
        _sourceImage = [UIImage imageWithData:_sourceData];
    }
    return _sourceImage;
}

- (void)setSourceImage:(UIImage *)sourceImage {
    _sourceImage = sourceImage;
    _sourceData = sourceImage ? UIImageJPEGRepresentation(sourceImage, 0.5) : nil;
}

- (UIImage *)backImage {
    if (!_backImage && _backImageData) {
        _backImage = [UIImage imageWithData:_backImageData];
    }
    return _backImage;
}

- (void)setBackImage:(UIImage *)backImage {
    _backImage = backImage;
    _backImageData = backImage ? UIImageJPEGRepresentation(backImage, 0.5) : nil;
}

// Specify properties to ignore (Realm won't persist these)
// 忽略的属性赋值
+ (NSArray *)ignoredProperties {
    return @[@"backImage",@"sourceImage",@"contentFrame"];
}

2、数据查询问题

由于我们数据结构的特殊性,存储的数据需要进一步处理才能渲染,所以这边就需要把数据库查询到的数据另外缓存处理;
很奇怪的是,想要使用查询到的数据,一旦把它取出来另外存储,就会没有数据,而且跟原来数据库中存储的对象地址不一样;



检查发现是无论使用forin方法遍历,还是objectIndex方法查询,最后都会返回一个新的对象,跟可知道打印出来的数据不一样;


通过控制台尝试修改属性值



虽然打印有报错,但是最后结果还是赋值成功了;



所以就有了第一个想法,通过属性拷贝的方式取出数据;
 // 数据拷贝
- (id)mutableCopy {
    unsigned int count = 0;
    Class class = self.class;
    id model = [class new];
    // 数据
    while (class && class != [RLMObject class]) {
        Ivar * ivars = class_copyIvarList(class, &count);
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            NSString * key = [NSString stringWithUTF8String:name];
            //设置到成员变量身上
            [model setValue:[self valueForKey:key] forKey:key];
        }
        
        free(ivars);
        class = class.superclass;
    }
    return model;
}

但是此方法最后也是铩羽而归,使用result[i]势必会走到init方法,所以,最后copy出来的还是空数据;

控制台突破口

上面有提到,通过控制台打印可以正常查看到数据,那么它内部肯定是对描述做了处理;



po [result description] 结果也是一样的



尝试进入内部查看

断点到具体的方法里面,那么接下来就是看一下内部是如何获取到数据的



到这一步发现它内部也是调用了forin遍历results;

遍历数据内部对数据做了排序,但是最后走回到了get->create->init,可见Realm设计是不希望外部拿到原始数据,难道只能在元数据上做处理

obj内部数据都是空,但是sub描述已经赋上值了,那么descriptionWithMaxDepth里面肯定是暗藏玄机;


descriptionWithMaxDepth


到这一步就比较明朗了,ClassInfo里面的properties保存的所有存储的属性对象RLMProperty,通过objectForKeyedSubscript找出value;这样我们就可以参考这思路把我们想要的值赋值上去了;


下面的判断是做递归操作,如果value还是RLMObject对象,那么就继续深入下一层取值,这里打印depth最多是5层,我们要把值都取出来展示渲染,所以这个层级我们可以尽量多一点;

if ([object respondsToSelector:@selector(descriptionWithMaxDepth:)]) {
            sub = [object descriptionWithMaxDepth:depth - 1];
        }

顺便提一下这个objectForKeyedSubscript,


@property (nonatomic, readwrite) NSDictionary *allPropertiesByName;

就是通过_allPropertiesByName进行保存的映射关系查询,每次更新属性就会刷新这个map字典;


综合以上之后,就能从realm管理的对象中深拷贝一份新的数据供我们使用了,核心代码附上

/**
 * 读取缓存素材
 */
+ (NSArray *)getSaveData {
    DLog(@"素材解档=========开始");
    NSArray *info = [NSKeyedUnarchiver unarchiveObjectWithFile:SouceCachesDirectory];
    DLog(@"素材解档=========结束");
    if (info) {
        // 之前归档的数据迁移
        [self reSaveInfoToRealm:info];
        [FileTools deleteFileAtPath:SouceCachesDirectory];
    }
    DLog(@"读取数据库数据=========开始");
    NSMutableArray *resultData = [NSMutableArray array];
    RLMResults *result = [EMSourceCellModel allObjects];
    RLMResults *sortResult = [result sortedResultsUsingKeyPath:@"ID" ascending:NO];
    NSLog(@"%@",[sortResult description]);
    for (id obj in sortResult) {
        EMSourceCellModel *model = [EMSourceCellModel new];
        [self getDepthDataWithObj:obj resultModel:model];
        [resultData addObject:@[model]];
    }
    DLog(@"读取数据库数据=========结束");
    return resultData;
}

+ (void)getDepthDataWithObj:(RLMObjectBase *)obj resultModel:(id)model {
    NSArray *properties = [[obj valueForKey:@"_objectSchema"] valueForKey:@"properties"];
    for (RLMProperty *property in properties) {
        id object = [(id)obj objectForKeyedSubscript:property.name];
        if ([object isKindOfClass:[RLMObject class]]) {
            [self getDepthDataWithObj:object resultModel:model];
        } else {
            [model setValue:object forKey:property.name];
        }
    }
}

3、更新数据

正如上文所说的,通过唯一值ID查询的results数据,使用firstObject获取到的数据仍然是空的,所以更新这一块也同样遇到了查询一样的问题;



此处我也想不到更好的办法,只能使用最蠢的方式,如果有查询到就先delete掉,然后在重新add;
修改完之后,我的增删改的代码就变成了下面的样子;

- (void)add {
    [self update:true needRemove:false];
}

- (void)update {
    [self update:true needRemove:true];
}

- (void)remove {
    [self update:false needRemove:true];
}

- (void)update:(BOOL)needAdd needRemove:(BOOL)needRemove {
    NSLog(@"修改素材数据成功11111111");
    //更新数据方式一:根据条件取出model
    EMSourceCellModel *model = nil;
    if (needRemove) {
        NSString *condition = [NSString stringWithFormat:@"ID = %lld",self.ID];
        RLMResults * result = [EMSourceCellModel objectsWhere:condition];
        model = result.firstObject;
    }

    RLMRealm *realm = [RLMRealm defaultRealm];
    [realm transactionWithBlock:^{
        if (model && needRemove) {
            [realm deleteObject:model];
        }
        if (needAdd) {
           // 不能直接add本身,因为直接add的对象,由realm直接管理,外面没加事务锁,操作外面数据就会崩,用空间换时间,懒得每个修改的地方都去加锁              
           // Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first
            [realm addObject:[self mutableCopy]];
        }
        
        NSLog(@"修改素材数据成功22222222");
    }];
    RLMResults *result1 = [EMSourceCellModel allObjects];
    NSLog(@"result.count == %lu",(unsigned long)result1.count);
    NSLog(@"修改素材数据成功33333333");
}

我这种操作可能不是Realm官方所支持的操作,虽然安全性降低了,但是灵活性增加了,针对于我们这种本地素材图片图像类的缓存,其实灵活性比安全性可操作性更强一点;

4、修改前后对比

同样添加15张图片,效果对比;

磁盘缓存大小

归档15张图片是150M的磁盘缓存



Realm NSData存储是18MB左右,这多少可能跟这里image转data做了较大程度压缩有一点关系;


=

内存占用大小对比

归档过程中,内存占用跟CPU使用率都会飙升



Realm操作数据过程中,内存没有浮动,整体内存占用率也偏低很多,当然也可能跟我对图片做了压缩有关系;


增删改时间对比

归档15个内容,修改一次数据,大概是7秒左右时间



同样的数据,Realm删除、增加各操作一次的时间大概是2-5毫秒左右,查询all的时间大概需要200毫秒


以上,就是我在使用Realm过程中遇到的一些问题,以及自己的一些个人愚见,如有不妥,欢迎指正;

2022-11-24补充修改,子类继承问题

涉及到子类的问题,因为Realm是根据classname进行读取查询的,所以如果一组数据包括好几种类型的model,就会有问题,比如我的素材普通素材,还有画中画素材,画中画素材继承于普通素材,所以要想把数据全部取出来,就不能简单的直接取了,下面附上修改后的完整代码;

- (long long)ID {
    if (!_ID) {
        _ID = [[NSDate date] timeIntervalSince1970]*1000 + random()%1000;
    }
    return _ID;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {}
//- (void)setCustomValue:(id)value forKey:(NSString *)key {
//    if (value) {
//        [self setCustomValue:value forKey:key];
//    } else {
////        NSLog(@"---------- %s Crash Because Method %s  ---------- key=%@\n", class_getName(self.class), __func__,key);
//    }
//}

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

- (void)setContentFrame:(CGRect)contentFrame {
    _contentFrame = contentFrame;
    _x = contentFrame.origin.x;
    _y = contentFrame.origin.y;
    _w = contentFrame.size.width;
    _h = contentFrame.size.height;
}

- (CGRect)contentFrame {
    if (CGRectEqualToRect(_contentFrame, CGRectZero) && _w > 0) {
        _contentFrame = CGRectMake(_x, _y, _w, _h);
    }
    return _contentFrame;
}

- (UIImage *)sourceImage {
    if (!_sourceImage && _sourceData) {
        _sourceImage = [UIImage imageWithData:_sourceData];
    }
    return _sourceImage;
}

- (void)setSourceImage:(UIImage *)sourceImage {
    _sourceImage = sourceImage;
    _sourceData = sourceImage ? UIImageJPEGRepresentation(sourceImage, 0.5) : nil;
}

- (UIImage *)backImage {
    if (!_backImage && _backImageData) {
        _backImage = [UIImage imageWithData:_backImageData];
    }
    return _backImage;
}

- (void)setBackImage:(UIImage *)backImage {
    _backImage = backImage;
    _backImageData = backImage ? UIImageJPEGRepresentation(backImage, 0.5) : nil;
}

- (void)add {
    [self update:true needRemove:false];
}

- (void)update {
    [self update:true needRemove:true];
}

- (void)remove {
    [self update:false needRemove:true];
}

- (void)update:(BOOL)needAdd needRemove:(BOOL)needRemove {
    NSLog(@"修改素材数据成功11111111");
    //更新数据方式一:根据条件取出model
    EMSourceCellModel *model = nil;
    if (!self.ID) {
        DLog(@"生成ID成功");
    }
    if (needRemove) {
        NSString *condition = [NSString stringWithFormat:@"ID = %lld",self.ID];
//        RLMResults * result = [EMSourceCellModel objectsWhere:condition];
        RLMResults * result = [self.class objectsWhere:condition];
        model = result.firstObject;
    }

    RLMRealm *realm = [RLMRealm defaultRealm];
    [realm transactionWithBlock:^{
        if (model && needRemove) {
            [realm deleteObject:model];
        }
        if (needAdd) {
            // 不能直接add本身,因为直接add的对象,由realm直接管理,外面没加事务锁,操作外面数据就会崩,用空间换时间,懒得每个修改的地方都去加锁
            // Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first
            [realm addObject:[self mutableCopy]];
        }
        
        NSLog(@"修改素材数据成功22222222");
    }];
//    RLMResults *result1 = [EMSourceCellModel allObjects];
//    NSLog(@"result.count == %lu",(unsigned long)result1.count);
//    NSLog(@"修改素材数据成功33333333");
}

/**
 * 归档
 */
+ (void)save:(NSArray *)info {
    [NSKeyedArchiver archiveRootObject:info toFile:SouceCachesDirectory];
}

// 数据迁移
+ (void)reSaveInfoToRealm:(NSArray *)info {
    if (info && info.count > 0) {
        for (NSArray *sectionArr in info) {
            EMSourceCellModel *model = sectionArr.firstObject;
            if (model) {
                EMSourceCellModel *reModel = [model mutableCopy];
                [reModel add];
            }
        }
    }
}

/**
 * 读取缓存素材
 */
+ (NSArray *)getSaveData {
    DLog(@"素材解档=========开始");
    NSArray *info = [NSKeyedUnarchiver unarchiveObjectWithFile:SouceCachesDirectory];
    DLog(@"素材解档=========结束");
    if (info) {
        // 之前归档的数据迁移
        [self reSaveInfoToRealm:info];
        [FileTools deleteFileAtPath:SouceCachesDirectory];
    }
    DLog(@"读取数据库数据=========开始");
    NSMutableArray *resultData = [NSMutableArray array];
    [resultData addObjectsFromArray:[self readSourceData:EMSourceCellModel.className]];
    [resultData addObjectsFromArray:[self readSourceData:EMSourceVideoCellModel.className]];
    resultData = (NSMutableArray *)[EMTool arrayWithSort:resultData key:@"ID" ascending:NO];
    DLog(@"读取数据库数据=========结束");
    return resultData;
}

+ (NSArray *)readSourceData:(NSString *)className {
    Class sourceClass = objc_getClass([className UTF8String]);
    NSMutableArray *resultData = [NSMutableArray array];
    if ([sourceClass respondsToSelector:@selector(allObjects)]) {
        RLMResults *result = [sourceClass performSelector:@selector(allObjects)];
        for (id obj in result) {
            EMSourceCellModel *model = [sourceClass new];
            [self getDepthDataWithObj:obj resultModel:model];
            [resultData addObject:model];
        }
    }
    
    return resultData;
}

+ (void)getDepthDataWithObj:(RLMObjectBase *)obj resultModel:(id)model {
    NSArray *properties = [[obj valueForKey:@"_objectSchema"] valueForKey:@"properties"];
    for (RLMProperty *property in properties) {
        id object = [(id)obj objectForKeyedSubscript:property.name];
        if ([object isKindOfClass:[RLMObject class]]) {
            [self getDepthDataWithObj:object resultModel:model];
        } else {
            [model setValue:object forKey:property.name];
        }
    }
}

// 数据拷贝
- (id)mutableCopy {
    unsigned int count = 0;
    Class class = self.class;
    id model = [class new];
    // 数据
    while (class && class != [RLMObject class]) {
        Ivar * ivars = class_copyIvarList(class, &count);
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            NSString * key = [NSString stringWithUTF8String:name];
            //设置到成员变量身上
            [model setValue:[self valueForKey:key] forKey:key];
        }
        
        free(ivars);
        class = class.superclass;
    }
    return model;
}

//解档
- (instancetype)initWithCoder:(NSCoder *)coder {
    if ((self = [super modelInitWithCoder:coder])) {
        unsigned int count = 0;
        Class class = self.class;
        // 层层解档档,父类数据也要取出
        while (class && class != [NSObject class]) {
            Ivar * ivars = class_copyIvarList(class, &count);
            for (int i = 0; i < count; i++) {
                Ivar ivar = ivars[i];
                const char * name = ivar_getName(ivar);
                NSString * key = [NSString stringWithUTF8String:name];
                //解档
                id value = [coder decodeObjectForKey:key];
                //设置到成员变量身上
                [self setValue:value forKey:key];
            }
            
            free(ivars);
            class = class.superclass;
        }
    }
    return self;
}

- (void)setValue:(id)value forKey:(NSString *)key {
    if (value) {
        if ([key isEqualToString:@"_contentFrame"]) {
            self.contentFrame = [value CGRectValue];
        } else if ([key isEqualToString:@"_backImage"]) {
            self.backImage = value;
        } else if ([key isEqualToString:@"_sourceImage"]) {
            self.sourceImage = value;
        } else {
            [super setValue:value forKey:key];
        }
    }
}

// Specify properties to ignore (Realm won't persist these)
// 忽略的属性赋值
+ (NSArray *)ignoredProperties {
    return @[@"backImage",@"sourceImage",@"contentFrame"];
}

你可能感兴趣的:(Realm数据查询修改踩坑之路)