由于项目开始阶段为了图方便,所以数据存本地都采用了归档的方式,归档这种方式,操作简便,代码也很快,但是缺点就是稍微有点改动就需要把所有数据再归档一遍,费时费力,针对少量数据可以这么做,但是随着归档的数据越来越多,每次归档的时间也就越来越长了,所以数据库替换是势在必行;
由于之前都是对模型归档,所以进行数据库升级主要考虑的是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"];
}