【问题标题】:NSFetchedResultsController does not read updated derived value in CoreData entity after RestKit EntityMapping update在 RestKit EntityMapping 更新后,NSFetchedResultsController 不会读取 CoreData 实体中更新的派生值
【发布时间】:2014-08-19 06:19:56
【问题描述】:

我有一个视图控制器,我在其中创建了一个 NSFetchedResultsController 以在 TableView 中显示一组 CoreData 对象。在 tableview 中查看这些对象时,它们通过调用 RestKit getObjectsAtPath 进行更新,该方法通过响应描述符和 RKEntityMapping 正确处理以更新 CoreData 对象上的字段。但是,这个特定实体也有一个自定义派生字段 - 实际上是一个状态机(基于 TransitionKit),它读取提供给实体的状态值并使用服务器提供的状态重新初始化状态机。但是,无论我在哪里重新初始化状态机(awakeFromFetch,willSave,key-value observing),当NSFetchedResultsController中的对象副本用于更新对应的表格单元格时(当NSFetchResultsController为通知该行的更改)。需要明确的是 - 通过 RestKit EntityMapping 更新的值已更新,但状态机(派生值)未更新。为什么会这样?

不应该以允许它们计算派生值的方式通知 NSFetchedResultsController 的对象数组吗?当我在代码中跟踪时,主线程中的 awakeFromFetch 尚未包含更新的值,并且在 willSave 或 setter 中计算我的派生值似乎不会在 NSFetchedResultsController 持有的对象实例中创建此派生值.

我已附上我的基本模型代码

#import "VCStateMachineManagedObject.h"

@interface VCStateMachineManagedObject ()

@property (nonatomic, strong) TKStateMachine * stateMachine;

@end



@implementation VCStateMachineManagedObject

@dynamic savedState;
@synthesize stateMachine = _stateMachine;
@synthesize forcedState;

-(id)init {
    self = [super init];
    if(self != nil) {
    }
    return self;
}


- (BOOL)canFireEvent:(id)eventOrEventName {
    return [_stateMachine canFireEvent:eventOrEventName];
}

- (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(NSError **)error{
   return [_stateMachine fireEvent:eventOrEventName userInfo:userInfo error:error];
}

- (void) assignStatesAndEvents:(TKStateMachine *) stateMachine {
    [NSException raise:@"Invoked abstract method" format:@"Invoked abstract method"];
}

- (NSString *) getInitialState {
    [NSException raise:@"Invoked abstract method" format:@"Invoked abstract method"];
    return nil;
}


- (void)awakeFromInsert {
    if(self.savedState == nil){
        self.savedState = [self getInitialState];
    }
    [self createStateMachine];
}

- (void)awakeFromFetch {
    if(self.savedState == nil){
        self.savedState = [self getInitialState];
    }
    [self addObserver:self forKeyPath:@"savedState" options:NSKeyValueObservingOptionNew context:nil];
    [self createStateMachine];
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if(![[_stateMachine.currentState name] isEqualToString:self.savedState]){
        [self createStateMachine];
    }
}

- (void) willSave {
    NSLog(@"%@", self.savedState);
    [self createStateMachine];
}


// Manually set the state, for restkit object mapping
- (void) setForcedState: (NSString*) state__ {
    self.savedState = state__;
}

- (void) setSavedState:(NSString *)savedState__{
    [self willChangeValueForKey:@"savedState"];
    [self setPrimitiveValue:savedState__ forKey:@"savedState"];
    [self didChangeValueForKey:@"savedState"];
    [self createStateMachine];
}

- (NSString *) state {
    NSString * state = [_stateMachine.currentState name];
    return [NSString stringWithFormat:@"%@ %@", state, self.savedState];
}


#pragma mark - State Machine

- (void)prepareStateMachine {
    for(TKEvent * event in _stateMachine.events){
        [event setDidFireEventBlock:^(TKEvent *event, TKTransition *transition) {
            self.savedState = transition.destinationState.name;
        }];
    }
}

- (void) createStateMachine {
    _stateMachine = [TKStateMachine new];
    [self assignStatesAndEvents:_stateMachine];
    [self prepareStateMachine];
    _stateMachine.initialState = [_stateMachine stateNamed:self.savedState];
    [_stateMachine activate];
}


@end

@quellish 这是我在托管对象中跟踪断点时看到的内容。

  1. 我呼吁 restkit 下载新对象
  2. Restkit 在后台线程(不是主线程)中下载对象。我看到它找到匹配的对象(awakeFromFetch),更新状态(setForcedState),并保存(willSave),在这个对象实例上,我看到createStateMachine方法被调用了几次(这是因为我在所有这些函数中都有它,尽管这当然对 NSFetchedResultsController 中的实例没有影响)。
  3. 然后我看到主线程上的一个对象被提取 (awakeFromFetch) 并经过相同的过程。
  4. 然后我看到主线程上的一个对象被 KVO 触发并再次通过 createStateMachine 方法。

当我在状态机中查看变量时,所有原因都已将它们更新为正确的值。但是然后,当 NSFetchedResultsController 触发更改时,即使值本身是,状态机也不会更新。

在接下来的几个小时内,我将对此进行追溯,并提供有关我所看到的行为的更具体信息。我还将为实体的每个实例添加一个 UUID,以确保正在更新的那个实际上是 NSFetchedResultsController 中的那个。敬请期待。

【问题讨论】:

  • KVO 是否为密钥而被解雇?是否“只是”时间问题(因此 FRC 在 KVO 通知到达 MO 本身之前更新)?
  • 这是我想不通的奇怪事情之一。是的,KVO 被触发了,我可以通过我的 observeValueForKeyPath: 方法跟踪它,看起来一切都会好起来的。但是在视图控制器中,当对象被读取时,状态机仍然没有更新的值(虽然 savedState 有)。
  • 为什么选择使用setPrimitiveValue:forKey:?看起来您的实现可能会破坏 CoreData 的错误和关键值观察。
  • 我将其切换为 setPrimitive,因为以正常方式执行此操作不起作用。改回来对行为没有影响,正如我提到的,KVO 和 awakeFromFetch 似乎是在主线程上调用的。
  • 当您使用带有 performBlock 或 performBlockAndWait 的私有上下文时,它们会在主线程上被调用? performBlockAndWait 几乎总是从调用线程执行,但不应用于修改托管对象的故障状态的任何事情。

标签: ios objective-c core-data restkit


【解决方案1】:

您在问题中提供的实现存在一些问题。您的访问器和 KVO 观察实现对您不利,并且似乎干扰了基于 KVO 的 Core Data 更改跟踪。

以下是您可以改进的一些内容,可能会解决您遇到的问题。这肯定会帮助您解决一些您可能还没有遇到的问题,但肯定会。

observeValueForKeyPath:ofObject:change:context: 正确的实现应该根据您传递给addObserver:forKeyPath:context:removeObserver:forKeyPath:context: 的已知值检查上下文指针。这使您可以将您的观察与其他观察区分开来。这就引出了下一点—— 一个正确的实现调用super。如果上下文值不是您的已知值,请遵循 super。示例:

添加观察者:

[self addObserver:self forKeyPath:keyPath options:options context:(__bridge void*)self];

移除观察者:

[self removeObserver:self forKeyPath:keyPath context:(__bridge void*)self];

observeValueForKeyPath:ofObject:change:context: 实现:

- (void) observeValueForKeyPath: (NSString *) keyPath ofObject: (id) object change: (NSDictionary *) change context: (void *) context {
    if ((__bridge id)context == self){
        // This is our observation, handle it here.
                [self setStateMachine:nil];
    } else {
        // This is important for Core Data to work correctly. 
        [super observeValueForKeyPath: keyPath ofObject: object change: change context: context];
    }
}

CoreData 广泛使用 KVO 来跟踪托管对象快照的更改。将 KVO 与 Core Data 一起使用时,您应该注意这一点,并注意不要为 Core Data 正确实现 KVO——这与其他对象略有不同。例如,对于 NSManagedObject 子类的建模属性,自动 KVO 通知默认是关闭的。 这意味着如果一个属性由一个建模属性支持,默认情况下它不会发出外部 KVO 通知。模型中不存在的属性

要为建模属性启用自动 KVO 通知,请使用以下模式实现类方法:

+ (BOOL) automaticallyNotifiesObserversFor<PropertyName> {
    return YES;
}

建模属性的名称在哪里(即automaticallyNotifiesObserversForSavedState)。

在您的情况下,您选择为您的建模属性实现自定义访问器。目前尚不清楚为什么您选择从您发布的代码中执行此操作(也许您在willSave: 文档中看到了关于递归的可怕警告 - 您的 will/didChangeValueForKey 消息重新引入了这一点)。 非常罕见需要为托管对象子类提供您自己的访问器实现。通常,Core Data 在运行时为 @dynamic 属性提供访问器。当它这样做时,它提供了一个具有正确内存管理和更改跟踪以及 CPU 和内存优化的实现。

原始访问器方法用于访问托管对象的属性。这实质上意味着一个实例变量直接由数据模型中的值支持。不建议将属性作为原始值访问,而且很少值得。总是希望属性访问器从 Core Data 和您的模型对象中获得正确的行为。

  • 使用上述指南修复您的 KVO 实施。
  • 不要在托管对象子类上覆盖initinit 不是指定的初始值。
  • 从为您的建模属性实现自己的访问器转变为允许 Core Data 提供实现。这应该像删除您当前的访问器实现一样简单。
  • 如果您更改您的 NSKeyValueObservationOptions 以包含 NSKeyValueObservingOptionInitial,您将收到一个 KVO 通知,以了解观察到的键路径的初始值。在您的情况下,这也是为您的状态机设置初始状态的机会。
  • 由于您正在实现某种状态机,因此允许 KVO 管理值之间的依赖关系(即,forcedState、savedState 和 stateMachine 之间)可能是一个好主意。例如,将 savedState 设置为forcedState 的依赖keypath,这样当saveState 发生变化时,系统就知道forcedState 应该是“脏”的,需要重新计算:

    + (NSSet *)keyPathsForValuesAffectingValueForForcedState {
    return [NSSet setWithObject:@"savedState"];
    

    }

  • 一旦您的 KVO 实现得到修复,可能就不需要从托管对象生命周期方法更新您的状态机。如果您决定坚持使用生命周期方法,请查看 awakeFromSnapshotEvents: 是否更适合您的需求。

由于您似乎根本没有改变您的状态机,只是重新创建它,因此您的用例非常简单。如果我正确地阅读了您的课程,您只需在 KVO 告诉您 savedState 已更改时将 stateMachine 设置为 nil。如果访问state 时为nil,则调用您的create 方法设置该属性。大多数瞬态属性都是以这种方式实现的。

【讨论】:

  • 谢谢,我将逐步介绍这些建议并更新发生的情况。我还将更新一个更少“调试”场景和更多真实用例的类。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多