保存 PFObject NSCoding

Posted

技术标签:

【中文标题】保存 PFObject NSCoding【英文标题】:Saving PFObject NSCoding 【发布时间】:2014-05-31 22:24:37 【问题描述】:

我的问题: saveInBackground 不起作用。

它不起作用的原因:我正在使用 NSKeyedArchiving 将存储在 NSArray 中的 PFObjects 保存到文件中。我这样做的方法是通过这个library 实现NSCoding。由于某些我不知道的原因,正在添加其他几个字段并将其设置为 NULL。我感觉这搞砸了对saveInBackground 的API 调用。当我在第一组对象(NSKeyedArchiving 之前)上调用 saveInBackground 时,saveInBackground 工作得很好。但是,当我在第二个对象(NSKeyedArchiving 之后)上调用它时,它不会保存。这是为什么呢?

保存

[NSKeyedArchiver archiveRootObject:_myArray toFile:[self returnFilePathForType:@"myArray"]];

检索

_myArray = (NSMutableArray *)[NSKeyedUnarchiver unarchiveObjectWithFile:
                                             [self returnFilePathForType:@"myArray"]];

NSArchiving 之前的对象

2014-04-16 16:34:56.267 myApp[339:60b]
<UserToMessage:bXHfPM8sDs:(null)> 
    from = "<PFUser:sdjfa;lfj>";
    messageText = "<MessageText:asdffafs>";
    read = 0;
    to = "<PFUser:asdfadfd>";

2014-04-16 16:34:56.841 myApp[339:60b]
<UserToMessage:bXHsdafdfs:(null)> 
    from = "<PFUser:eIasdffoF3gi>";
    messageText = "<MessageText:asdffafs>";
    read = 1;
    to = "<PFUser:63sdafdf5>";

NSArchiving 后的对象

<UserToMessage:92GGasdffVQLa:(null)> 
    ACL = "<null>";
    createdAt = "<null>";
    from = "<PFUser:eIQsadffF3gi>";
    localId = "<null>";
    messageText = "<MessageText:EudsaffdHpc>";
    objectId = "<null>";
    parseClassName = "<null>";
    read = 0;
    saveDelegate = "<null>";
    to = "<PFUser:63spasdfsxNp5>";
    updatedAt = "<null>";


2014-04-16 16:37:46.527 myApp[352:60b]
<UserToMessage:92GadfQLa:(null)> 
    ACL = "<null>";
    createdAt = "<null>";
    from = "<PFUser:eIQsadffF3gi>";
    localId = "<null>";
    messageText = "<MessageText:EuTndasHpc>";
    objectId = "<null>";
    parseClassName = "<null>";
    read = 1;
    saveDelegate = "<null>";
    to = "<PFUser:63spPsadffp5>";
    updatedAt = "<null>";

使用 Florent 的 PFObject 类别更新:

PFObject+MyPFObject_NSCoding.h

#import <Parse/Parse.h>

@interface PFObject (MyPFObject_NSCoding)

-(void) encodeWithCoder:(NSCoder *) encoder;
-(id) initWithCoder:(NSCoder *) aDecoder;
@end

@interface PFACL (extensions)
-(void) encodeWithCoder:(NSCoder *) encoder;
-(id) initWithCoder:(NSCoder *) aDecoder;
@end


 PFObject+MyPFObject_NSCoding.m

#import "PFObject+MyPFObject_NSCoding.h"
@implementation PFObject (MyPFObject_NSCoding)
#pragma mark - NSCoding compliance
#define kPFObjectAllKeys @"___PFObjectAllKeys"
#define kPFObjectClassName @"___PFObjectClassName"
#define kPFObjectObjectId @"___PFObjectId"
#define kPFACLPermissions @"permissionsById"
-(void) encodeWithCoder:(NSCoder *) encoder

    // Encode first className, objectId and All Keys
    [encoder encodeObject:[self className] forKey:kPFObjectClassName];
    [encoder encodeObject:[self objectId] forKey:kPFObjectObjectId];
    [encoder encodeObject:[self allKeys] forKey:kPFObjectAllKeys];
    for (NSString * key in [self allKeys]) 
        [encoder  encodeObject:self[key] forKey:key];
    



-(id) initWithCoder:(NSCoder *) aDecoder

    // Decode the className and objectId
    NSString * aClassName  = [aDecoder decodeObjectForKey:kPFObjectClassName];
    NSString * anObjectId = [aDecoder decodeObjectForKey:kPFObjectObjectId];


    // Init the object
    self = [PFObject objectWithoutDataWithClassName:aClassName objectId:anObjectId];

    if (self) 
        NSArray * allKeys = [aDecoder decodeObjectForKey:kPFObjectAllKeys];
        for (NSString * key in allKeys) 
            id obj = [aDecoder decodeObjectForKey:key];
            if (obj) 
                self[key] = obj;
            

        
    
    return self;

@end

【问题讨论】:

到目前为止,我还没有使用 NSKeyedArchiving 并且看不到任何意义。只需将您的数组保存到文件中,我猜 saveinbackground 不支持 NSKeyedArchiving 对象。有什么问题请告诉我。 @walle84 如何将其保存到文件而不保存到后台? 尝试在不使用 NSKeyedArchiving 的情况下保存并创建数组并尝试 saveinbackground。检查此link 可能会对您有所帮助。 你知道你可以在保存时保存各种对象吗? [PFObject saveAllInBackground:]; @walle84 你是正确的 NSKeyedArchiver 是问题,但是我现在需要一个替代解决方案来保存到磁盘。 【参考方案1】:

正如你在问题中所说,空字段一定会搞砸saveInBackground 调用。

奇怪的是 parseClassName 也是空的,而 Parse 可能需要它来保存它。是否在您将NSArray 保存在文件中之前设置?

所以我看到了两种解决方案:

实现自己 NSCoding 不带空字段,但如果对象已经保存在服务器上,保存它的 objectIds、createdAt、updatedAt 字段等很有用(甚至是必要的)... 先在 Parse 上保存每个 PFObject,然后再将 NSArray 保存到文件中,这样这些字段就不会为空。

【讨论】:

我认为你的第二个要点是在做一些事情 @Sunder 这是不可能的。即使用户退出当前会话(即存储到磁盘),对象也需要可保存 @Auser 好的,您是否尝试过在文件中保存对象前后显示空字段?存档时可能会丢失某些内容。【参考方案2】:

PFObject 没有实现NSCoding,而且您使用的库似乎没有正确编码对象,因此您当前的方法不起作用。

Parse 推荐的方法是通过设置 cachePolicy 属性将 PFQuery 对象缓存到磁盘:

PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
query.cachePolicy = kPFCachePolicyNetworkElseCache;
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) 
  if (!error) 
    // Results were successfully found, looking first on the
    // network and then on disk.
   else 
    // The network was inaccessible and we have no cached data for
    // this query.
  
];

(来自Caching Queries documentation的代码。)

然后您的应用将从缓存中加载。如果您想先尝试磁盘缓存,请切换到kPFCachePolicyCacheElseNetwork(更快,但可能已过时。)

您的查询对象的 maxCacheAge 属性设置了某些内容在过期之前将在磁盘上保留多长时间。


另外,there's a PFObject category by Florent here 将 NSCoder 支持添加到 PFObject。它与您链接到的库中的实现不同,但我不确定它有多可靠。可能值得一试。

【讨论】:

你看我的问题了吗?我已经在实现一个 NSCoding 类别github.com/updatezen/Parse-NSCoding 是的,但它不起作用……您是否查看了我在答案末尾链接到的不同实现? 我是否必须将“PFObjectNSCoding+PFObject.h”导入任何我想要支持 NSCoding 的文件中? 您能否详细介绍一下我将如何实现 FLorent 的类别?总的来说,我对 Objective-C 和 iPhone 开发还比较陌生。 如果你自己回答了我们的问题,也许你会得到更好的答案。【参考方案3】:

您在 NSArchiving 之后获得所有 "&lt;null&gt;" 条目的原因是您使用的 NSCoding 库处理 nil Parse 属性的方式。特别是,在 2 月 18 日的一次提交中,对 nil 的处理发生了几处更改,包括删除了几个测试以查看属性是否为 nil,并在解码中添加了以下代码:

    //Deserialize each nil Parse property with NSNull
    //This is to prevent an NSInternalConsistencyException when trying to access them in the future
    for (NSString* key in [self dynamicProperties]) 
        if (![allKeys containsObject:key]) 
            self[key] = [NSNull null];
        
    

我建议您使用替代的 NSCoding 库。

@AaronBrager 在 4 月 22 日的回答中建议了一个替代库。

更新:

由于替代库缺少对 PFFile 的支持,下面是为 PFFile 实现 NSCoding 所需的更改的类别实现。只需编译并将PFFile+NSCoding.m 添加到您的项目中。 此实现来自您使用的原始 NSCoding 库。

PFFile+NSCoding.h

//
//  PFFile+NSCoding.h
//  UpdateZen
//
//  Created by Martin Rybak on 2/3/14.
//  Copyright (c) 2014 UpdateZen. All rights reserved.
//

#import <Parse/Parse.h>

@interface PFFile (NSCoding)

- (void)encodeWithCoder:(NSCoder*)encoder;
- (id)initWithCoder:(NSCoder*)aDecoder;

@end

PFFile+NSCoding.m

//
//  PFFile+NSCoding.m
//  UpdateZen
//
//  Created by Martin Rybak on 2/3/14.
//  Copyright (c) 2014 UpdateZen. All rights reserved.
//

#import "PFFile+NSCoding.h"
#import <objc/runtime.h>

#define kPFFileName @"_name"
#define kPFFileIvars @"ivars"
#define kPFFileData @"data"

@implementation PFFile (NSCoding)

- (void)encodeWithCoder:(NSCoder*)encoder

    [encoder encodeObject:self.name forKey:kPFFileName];
    [encoder encodeObject:[self ivars] forKey:kPFFileIvars];
    if (self.isDataAvailable) 
        [encoder encodeObject:[self getData] forKey:kPFFileData];
    


- (id)initWithCoder:(NSCoder*)aDecoder

    NSString* name = [aDecoder decodeObjectForKey:kPFFileName];
    NSDictionary* ivars = [aDecoder decodeObjectForKey:kPFFileIvars];
    NSData* data = [aDecoder decodeObjectForKey:kPFFileData];

    self = [PFFile fileWithName:name data:data];
    if (self) 
        for (NSString* key in [ivars allKeys]) 
            [self setValue:ivars[key] forKey:key];
        
    
    return self;


- (NSDictionary *)ivars

    NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
    unsigned int outCount;

    Ivar* ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i++)
        Ivar ivar = ivars[i];
        NSString* ivarNameString = [NSString stringWithUTF8String:ivar_getName(ivar)];
        NSValue* value = [self valueForKey:ivarNameString];
        if (value) 
            [dict setValue:value forKey:ivarNameString];
        
    

    free(ivars);
    return dict;


@end

第二次更新:

我描述的更新解决方案(使用Florent's PFObject / PFACL encoders 组合替换classNameparseClassName 加上Martin Rybak's PFFile encoder)确实有效 - 在下面的测试工具中(见下面的代码)第二次调用saveInBackgroundNSKeyedUnarchiver 恢复后调用确实有效。

- (void)viewDidLoad 
    [super viewDidLoad];

    PFObject *testObject = [PFObject objectWithClassName:@"TestObject"];
    testObject[@"foo1"] = @"bar1";
    [testObject saveInBackground];

    BOOL success = [NSKeyedArchiver archiveRootObject:testObject toFile:[self returnFilePathForType:@"testObject"]];
    NSLog(@"Test object after archive (%@): %@", (success ? @"succeeded" : @"failed"), testObject);

    testObject = [NSKeyedUnarchiver unarchiveObjectWithFile:[self returnFilePathForType:@"testObject"]];
    NSLog(@"Test object after restore: %@", testObject);

    // Change the object
    testObject[@"foo1"] = @"bar2";
    [testObject saveInBackground];


- (NSString *)returnFilePathForType:(NSString *)param 
    NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *filePath = [docDir stringByAppendingPathComponent:[param stringByAppendingString:@".dat"]];

    return filePath;

但是,查看 Parse 服务器,对 saveInBackground 的第二次调用创建了新版本的对象。

即使这超出了原始问题的范围,我也会看看是否可以鼓励 Parse 服务器重新保存原始对象。同时,请投票和/或接受答案,因为它解决了在NSKeyedArchiving 之后使用saveInBackground 的问题。

最终更新:

这个问题原来只是一个时间问题 - 当 NSKeyedArchiver 发生时,第一个 saveInBackground 尚未完成 - 因此在归档时 objectId 仍然是 nil,因此在第二个 saveInBackground 时仍然是一个新对象.使用块(类似于下面)来检测保存何时完成并且可以调用 NSKeyedArchiver 也可以工作

以下版本不会导致保存第二个副本:

- (void)viewDidLoad 
    [super viewDidLoad];

    __block PFObject *testObject = [PFObject objectWithClassName:@"TestObject"];
    testObject[@"foo1"] = @"bar1";
    [testObject saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) 
        if (succeeded) 
            BOOL success = [NSKeyedArchiver archiveRootObject:testObject toFile:[self returnFilePathForType:@"testObject"]];
            NSLog(@"Test object after archive (%@): %@", (success ? @"succeeded" : @"failed"), testObject);

            testObject = [NSKeyedUnarchiver unarchiveObjectWithFile:[self returnFilePathForType:@"testObject"]];
            NSLog(@"Test object after restore: %@", testObject);

            // Change the object
            testObject[@"foo1"] = @"bar2";
            [testObject saveInBackground];
        
     ];


【讨论】:

感谢您的回复。我尝试实现 Florent 的类别,但它不支持 PFFile 归档,而且我不是 ios 专家,所以我不知道如何自己编写此代码 我已经更新了答案,为您使用的原始 NSCoding 库中存在的 PFFile 提供 NSCoding 实现。原始 NSCoding 库的许可证是 here。它是 MIT 许可证,如果包含原始许可证通知,则允许复制源代码。 感谢您的更新。我遇到的另一个问题是,当我实现 Florent 的类别时,我收到以下错误:“PFObject”没有可见的@interface 声明选择器“classsName”。你知道如何解决这个问题吗? className 更改为parseClassName - 即[encoder encodeObject:[self className] forKey:kPFObjectClassName]; 这一行应改为[encoder encodeObject:[self parseClassName] forKey:kPFObjectClassName]; 我将构建一个测试工具来尝试重现您的问题。可能需要一段时间。【参考方案4】:

我创建了一个非常简单的解决方法,不需要更改上述 NSCoding 库:

PFObject *tempRelationship = [PFObject objectWithoutDataWithClassName:@"relationship" objectId:messageRelationship.objectId];
        [tempRelationship setObject:[NSNumber numberWithBool:YES] forKey:@"read"];
        [tempRelationship saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) 
            if (succeeded)
                NSLog(@"Success");
            else
                NSLog(@"Error");
        ];

我们在这里所做的是创建一个具有相同 objectId 的临时对象,并保存它。这是一个工作解决方案,不会在服务器上创建对象的副本。感谢所有提供帮助的人。

【讨论】:

另见我的评论above

以上是关于保存 PFObject NSCoding的主要内容,如果未能解决你的问题,请参考以下文章

Swift - PFObject 从保存回调而不是布尔返回自身

将 PFObject 的数量保存到多行

保存 PFObject NSCoding

在 iOS 应用程序中更新 PFObject 的密钥时,需要从 Parse Cloud Code 保存和登录时运行代码

没有 PFObject 的 PFFile - 解析

使用完整的 PFUser 信息查询 PFObject