Singleton 属性 returns 不同的值取决于调用
Singleton property returns different values depending on invocation
背景
在我的应用程序中,我 class 调用了 FavoritesController 来管理 object 用户标记为收藏夹的内容,然后在整个应用程序中使用此收藏夹状态。 FavoritesController 被设计为单例 class 因为整个应用程序中有许多 UI 元素需要知道 'favorite status' 用于 object 在不同的地方,还有网络请求需要能够发出信号,表明收藏夹需要在服务器要求时失效。
当服务器响应 404 错误时会发生此失效部分,表明必须从用户的收藏夹中删除收藏夹 object。网络获取功能抛出错误,触发 FavoritesController 删除 object,然后向感兴趣的各方发送通知,告知他们需要刷新。
问题
当使用单元测试检查 404 实现的质量时,所有方法都按预期触发 - 错误被抛出并被捕获,FavoritesController 删除 object 并发送通知。但在某些情况下,已删除的收藏夹仍然存在 – 但这取决于从何处完成查询!
如果我在单例中查询,删除就成功了,但如果我从使用单例的 class 查询,删除就不会发生。
设计细节
- FavoritesController 属性
favorites
使用具有所有访问权限的 ivar @synchronized()
,并且 ivar 的值由 NSUserDefaults 属性.[=67= 支持]
- 最喜欢的 object 是具有两个键的 NSDictionary:
id
和 name
。
其他信息
一件我不明白为什么会发生的奇怪事情:在一些删除尝试中,收藏夹 object 的 name
值设置为 ""
但 id
键保留其值。
我编写了单元测试来添加无效的收藏夹并检查它是否在第一次服务器查询时被删除。当从空的收藏夹开始时,此测试通过,但当存在上述 'semi-deleted' object 的实例时失败(保留其 id
值)
单元测试现在一直通过,但在实际使用中,删除失败仍然存在。我怀疑这是因为 NSUserDefaults 没有立即保存到磁盘。
我尝试过的步骤
- 确保单例实现是 'true' 单例,即
sharedController
总是 returns 相同的实例。
- 我认为存在某种 'capture' 问题,闭包会保留自己的副本和过时的收藏夹,但我认为不会。当 NSLogging object ID 时,它 returns 相同。
代码
FavoritesController 主要方法
- (void) serverCanNotFindFavorite:(NSInteger)siteID {
NSLog(@"Server can't find favorite");
NSDictionary * removedFavorite = [NSDictionary dictionaryWithDictionary:[self favoriteWithID:siteID]];
NSUInteger index = [self indexOfFavoriteWithID:siteID];
[self debugLogFavorites];
dispatch_async(dispatch_get_main_queue(), ^{
[self removeFromFavorites:siteID completion:^(BOOL success) {
if (success) {
NSNotification * note = [NSNotification notificationWithName:didRemoveFavoriteNotification object:nil userInfo:@{@"site" : removedFavorite, @"index" : [NSNumber numberWithUnsignedInteger:index]}];
NSLog(@"Will post notification");
[self debugLogFavorites];
[self debugLogUserDefaultsFavorites];
[[NSNotificationCenter defaultCenter] postNotification:note];
NSLog(@"Posted notification with name: %@", didRemoveFavoriteNotification);
}
}];
});
}
- (void) removeFromFavorites:(NSInteger)siteID completion:(completionBlock) completion {
if ([self isFavorite:siteID]) {
NSMutableArray * newFavorites = [NSMutableArray arrayWithArray:self.favorites];
NSIndexSet * indices = [newFavorites indexesOfObjectsPassingTest:^BOOL(NSDictionary * entryUnderTest, NSUInteger idx, BOOL * _Nonnull stop) {
NSNumber * value = (NSNumber *)[entryUnderTest objectForKey:@"id"];
if ([value isEqualToNumber:[NSNumber numberWithInteger:siteID]]) {
return YES;
}
return NO;
}];
__block NSDictionary* objectToRemove = [[newFavorites objectAtIndex:indices.firstIndex] copy];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Will remove %@", objectToRemove);
[newFavorites removeObject:objectToRemove];
[self setFavorites:[NSArray arrayWithArray:newFavorites]];
if ([self isFavorite:siteID]) {
NSLog(@"Failed to remove!");
if (completion) {
completion(NO);
}
} else {
NSLog(@"Removed OK");
if (completion) {
completion(YES);
}
}
});
} else {
NSLog(@"Tried removing site %li which is not a favorite", (long)siteID);
if (completion) {
completion(NO);
}
}
}
- (NSArray *) favorites
{
@synchronized(self) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self->internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
return internalFavorites;
}
}
- (void) setFavorites:(NSArray *)someFavorites {
@synchronized(self) {
internalFavorites = someFavorites;
[self.defaults setObject:internalFavorites forKey:k_key_favorites];
}
}
- (void) addToFavorites:(NSInteger)siteID withName:(NSString *)siteName {
if (![self isFavorite:siteID]) {
NSDictionary * newFavorite = @{
@"name" : siteName,
@"id" : [NSNumber numberWithInteger:siteID]
};
dispatch_async(dispatch_get_main_queue(), ^{
NSArray * newFavorites = [self.favorites arrayByAddingObject:newFavorite];
[self setFavorites:newFavorites];
});
NSLog(@"Added site %@ with id %ld to favorites", siteName, (long)siteID);
} else {
NSLog(@"Tried adding site as favorite a second time");
}
}
- (BOOL) isFavorite:(NSInteger)siteID
{
@synchronized(self) {
NSNumber * siteNumber = [NSNumber numberWithInteger:siteID];
NSArray * favs = [NSArray arrayWithArray:self.favorites];
if (favs.count == 0) {
NSLog(@"No favorites");
return NO;
}
NSIndexSet * indices = [favs indexesOfObjectsPassingTest:^BOOL(NSDictionary * entryUnderTest, NSUInteger idx, BOOL * _Nonnull stop) {
if ([[entryUnderTest objectForKey:@"id"] isEqualToNumber:siteNumber]) {
return YES;
}
return NO;
}];
if (indices.count > 0) {
return YES;
}
}
return NO;
}
FavoritesController 的单例实现
- (instancetype) init {
static PKEFavoritesController *initedObject;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initedObject = [super init];
self.defaults = [NSUserDefaults standardUserDefaults];
});
return initedObject;
}
+ (instancetype) sharedController
{
return [self new];
}
单元测试代码
func testObsoleteFavoriteRemoval() {
let addToFavorites = self.expectation(description: "addToFavorites")
let networkRequest = self.expectation(description: "network request")
unowned let favs = PKEFavoritesController.shared()
favs.clearFavorites()
XCTAssertFalse(favs.isFavorite(313), "Should not be favorite initially")
if !favs.isFavorite(313) {
NSLog("Adding 313 to favorites")
favs.add(toFavorites: 313, withName: "Skatås")
}
let notification = self.expectation(forNotification: NSNotification.Name("didRemoveFavoriteNotification"), object: nil) { (notification) -> Bool in
NSLog("Received notification: \(notification.name.rawValue)")
return true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
NSLog("Verifying 313 is favorite")
XCTAssertTrue(favs.isFavorite(313))
addToFavorites.fulfill()
}
self.wait(for: [addToFavorites], timeout: 5)
NSLog("Will trigger removal for 313")
let _ = SkidsparAPI.fetchRecentReports(forSite: 313, session: SkidsparAPI.session()) { (reports) in
NSLog("Network request completed")
networkRequest.fulfill()
}
self.wait(for: [networkRequest, notification], timeout: 10)
XCTAssertFalse(favs.isFavorite(313), "Favorite should be removed after a 404 error from server")
}
为了给出我的答案的上下文,这是建议更改时相关代码的样子:
- (NSArray *)favorites {
@synchronized(internalFavorites) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
}
return internalFavorites;
}
我对 @synchronized(internalFavorites)
之后的检查 if (!internalFavorites) {
表示怀疑,因为这意味着期望 @synchronized
通过 nil
,results in a noop.
这意味着对 favorites
或 setFavorites
的多次调用可能会以有趣的方式发生,因为它们实际上不会同步。给 @sychronized
一个要同步的实际对象对于线程安全至关重要。在 self 上同步很好,但是对于特定的 class,您必须小心不要在 self 上同步太多东西,否则您将不可避免地造成不必要的阻塞。为 @sychronized
提供简单的 NSObject
s 是缩小保护范围的好方法。
以下是避免使用 self
作为锁定的方法。
- (instancetype)init {
static PKEFavoritesController *initedObject;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initedObject = [super init];
self.lock = [NSObject new];
self.defaults = [NSUserDefaults standardUserDefaults];
});
return initedObject;
}
+ (instancetype)sharedController {
return [self new];
}
- (NSArray *)favorites {
@synchronized(_lock) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self->internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
}
return internalFavorites;
}
关于测试运行之间的异常,在NSUserDefaults
上调用synchronize
肯定会有帮助,因为更改默认值的调用是异步的,这意味着涉及其他线程。也有大约 3 层缓存,特别是为了 运行 宁测试的目的 synchronize
应该确保在 Xcode 停止测试之前事情已经完全干净地提交 运行。文档非常突然地坚持认为这不是必要的调用,但如果确实没有必要,它就不会存在 :-)。在我的第一个 iOS 项目中,我们总是在每次默认值更改后调用 synchronize
...因此,我认为文档对 Apple 工程师来说更有吸引力。很高兴这种直觉对你有所帮助。
背景
在我的应用程序中,我 class 调用了 FavoritesController 来管理 object 用户标记为收藏夹的内容,然后在整个应用程序中使用此收藏夹状态。 FavoritesController 被设计为单例 class 因为整个应用程序中有许多 UI 元素需要知道 'favorite status' 用于 object 在不同的地方,还有网络请求需要能够发出信号,表明收藏夹需要在服务器要求时失效。
当服务器响应 404 错误时会发生此失效部分,表明必须从用户的收藏夹中删除收藏夹 object。网络获取功能抛出错误,触发 FavoritesController 删除 object,然后向感兴趣的各方发送通知,告知他们需要刷新。
问题
当使用单元测试检查 404 实现的质量时,所有方法都按预期触发 - 错误被抛出并被捕获,FavoritesController 删除 object 并发送通知。但在某些情况下,已删除的收藏夹仍然存在 – 但这取决于从何处完成查询!
如果我在单例中查询,删除就成功了,但如果我从使用单例的 class 查询,删除就不会发生。
设计细节
- FavoritesController 属性
favorites
使用具有所有访问权限的 ivar@synchronized()
,并且 ivar 的值由 NSUserDefaults 属性.[=67= 支持] - 最喜欢的 object 是具有两个键的 NSDictionary:
id
和name
。
其他信息
一件我不明白为什么会发生的奇怪事情:在一些删除尝试中,收藏夹 object 的
name
值设置为""
但id
键保留其值。我编写了单元测试来添加无效的收藏夹并检查它是否在第一次服务器查询时被删除。当从空的收藏夹开始时,此测试通过,但当存在上述 'semi-deleted' object 的实例时失败(保留其id
值)单元测试现在一直通过,但在实际使用中,删除失败仍然存在。我怀疑这是因为 NSUserDefaults 没有立即保存到磁盘。
我尝试过的步骤
- 确保单例实现是 'true' 单例,即
sharedController
总是 returns 相同的实例。 - 我认为存在某种 'capture' 问题,闭包会保留自己的副本和过时的收藏夹,但我认为不会。当 NSLogging object ID 时,它 returns 相同。
代码
FavoritesController 主要方法
- (void) serverCanNotFindFavorite:(NSInteger)siteID {
NSLog(@"Server can't find favorite");
NSDictionary * removedFavorite = [NSDictionary dictionaryWithDictionary:[self favoriteWithID:siteID]];
NSUInteger index = [self indexOfFavoriteWithID:siteID];
[self debugLogFavorites];
dispatch_async(dispatch_get_main_queue(), ^{
[self removeFromFavorites:siteID completion:^(BOOL success) {
if (success) {
NSNotification * note = [NSNotification notificationWithName:didRemoveFavoriteNotification object:nil userInfo:@{@"site" : removedFavorite, @"index" : [NSNumber numberWithUnsignedInteger:index]}];
NSLog(@"Will post notification");
[self debugLogFavorites];
[self debugLogUserDefaultsFavorites];
[[NSNotificationCenter defaultCenter] postNotification:note];
NSLog(@"Posted notification with name: %@", didRemoveFavoriteNotification);
}
}];
});
}
- (void) removeFromFavorites:(NSInteger)siteID completion:(completionBlock) completion {
if ([self isFavorite:siteID]) {
NSMutableArray * newFavorites = [NSMutableArray arrayWithArray:self.favorites];
NSIndexSet * indices = [newFavorites indexesOfObjectsPassingTest:^BOOL(NSDictionary * entryUnderTest, NSUInteger idx, BOOL * _Nonnull stop) {
NSNumber * value = (NSNumber *)[entryUnderTest objectForKey:@"id"];
if ([value isEqualToNumber:[NSNumber numberWithInteger:siteID]]) {
return YES;
}
return NO;
}];
__block NSDictionary* objectToRemove = [[newFavorites objectAtIndex:indices.firstIndex] copy];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Will remove %@", objectToRemove);
[newFavorites removeObject:objectToRemove];
[self setFavorites:[NSArray arrayWithArray:newFavorites]];
if ([self isFavorite:siteID]) {
NSLog(@"Failed to remove!");
if (completion) {
completion(NO);
}
} else {
NSLog(@"Removed OK");
if (completion) {
completion(YES);
}
}
});
} else {
NSLog(@"Tried removing site %li which is not a favorite", (long)siteID);
if (completion) {
completion(NO);
}
}
}
- (NSArray *) favorites
{
@synchronized(self) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self->internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
return internalFavorites;
}
}
- (void) setFavorites:(NSArray *)someFavorites {
@synchronized(self) {
internalFavorites = someFavorites;
[self.defaults setObject:internalFavorites forKey:k_key_favorites];
}
}
- (void) addToFavorites:(NSInteger)siteID withName:(NSString *)siteName {
if (![self isFavorite:siteID]) {
NSDictionary * newFavorite = @{
@"name" : siteName,
@"id" : [NSNumber numberWithInteger:siteID]
};
dispatch_async(dispatch_get_main_queue(), ^{
NSArray * newFavorites = [self.favorites arrayByAddingObject:newFavorite];
[self setFavorites:newFavorites];
});
NSLog(@"Added site %@ with id %ld to favorites", siteName, (long)siteID);
} else {
NSLog(@"Tried adding site as favorite a second time");
}
}
- (BOOL) isFavorite:(NSInteger)siteID
{
@synchronized(self) {
NSNumber * siteNumber = [NSNumber numberWithInteger:siteID];
NSArray * favs = [NSArray arrayWithArray:self.favorites];
if (favs.count == 0) {
NSLog(@"No favorites");
return NO;
}
NSIndexSet * indices = [favs indexesOfObjectsPassingTest:^BOOL(NSDictionary * entryUnderTest, NSUInteger idx, BOOL * _Nonnull stop) {
if ([[entryUnderTest objectForKey:@"id"] isEqualToNumber:siteNumber]) {
return YES;
}
return NO;
}];
if (indices.count > 0) {
return YES;
}
}
return NO;
}
FavoritesController 的单例实现
- (instancetype) init {
static PKEFavoritesController *initedObject;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initedObject = [super init];
self.defaults = [NSUserDefaults standardUserDefaults];
});
return initedObject;
}
+ (instancetype) sharedController
{
return [self new];
}
单元测试代码
func testObsoleteFavoriteRemoval() {
let addToFavorites = self.expectation(description: "addToFavorites")
let networkRequest = self.expectation(description: "network request")
unowned let favs = PKEFavoritesController.shared()
favs.clearFavorites()
XCTAssertFalse(favs.isFavorite(313), "Should not be favorite initially")
if !favs.isFavorite(313) {
NSLog("Adding 313 to favorites")
favs.add(toFavorites: 313, withName: "Skatås")
}
let notification = self.expectation(forNotification: NSNotification.Name("didRemoveFavoriteNotification"), object: nil) { (notification) -> Bool in
NSLog("Received notification: \(notification.name.rawValue)")
return true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
NSLog("Verifying 313 is favorite")
XCTAssertTrue(favs.isFavorite(313))
addToFavorites.fulfill()
}
self.wait(for: [addToFavorites], timeout: 5)
NSLog("Will trigger removal for 313")
let _ = SkidsparAPI.fetchRecentReports(forSite: 313, session: SkidsparAPI.session()) { (reports) in
NSLog("Network request completed")
networkRequest.fulfill()
}
self.wait(for: [networkRequest, notification], timeout: 10)
XCTAssertFalse(favs.isFavorite(313), "Favorite should be removed after a 404 error from server")
}
为了给出我的答案的上下文,这是建议更改时相关代码的样子:
- (NSArray *)favorites {
@synchronized(internalFavorites) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
}
return internalFavorites;
}
我对 @synchronized(internalFavorites)
之后的检查 if (!internalFavorites) {
表示怀疑,因为这意味着期望 @synchronized
通过 nil
,results in a noop.
这意味着对 favorites
或 setFavorites
的多次调用可能会以有趣的方式发生,因为它们实际上不会同步。给 @sychronized
一个要同步的实际对象对于线程安全至关重要。在 self 上同步很好,但是对于特定的 class,您必须小心不要在 self 上同步太多东西,否则您将不可避免地造成不必要的阻塞。为 @sychronized
提供简单的 NSObject
s 是缩小保护范围的好方法。
以下是避免使用 self
作为锁定的方法。
- (instancetype)init {
static PKEFavoritesController *initedObject;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initedObject = [super init];
self.lock = [NSObject new];
self.defaults = [NSUserDefaults standardUserDefaults];
});
return initedObject;
}
+ (instancetype)sharedController {
return [self new];
}
- (NSArray *)favorites {
@synchronized(_lock) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self->internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
}
return internalFavorites;
}
关于测试运行之间的异常,在NSUserDefaults
上调用synchronize
肯定会有帮助,因为更改默认值的调用是异步的,这意味着涉及其他线程。也有大约 3 层缓存,特别是为了 运行 宁测试的目的 synchronize
应该确保在 Xcode 停止测试之前事情已经完全干净地提交 运行。文档非常突然地坚持认为这不是必要的调用,但如果确实没有必要,它就不会存在 :-)。在我的第一个 iOS 项目中,我们总是在每次默认值更改后调用 synchronize
...因此,我认为文档对 Apple 工程师来说更有吸引力。很高兴这种直觉对你有所帮助。