代码之家  ›  专栏  ›  技术社区  ›  Frost

Singleton属性根据调用返回不同的值

  •  1
  • Frost  · 技术社区  · 7 年前

    出身背景

    在我的应用程序中,我有一个名为FavoritesController的类,它管理用户标记为收藏夹的对象,然后在整个应用程序中使用此收藏夹状态。FavoritesController设计为一个单例类,因为整个应用程序中有许多UI元素需要知道不同位置对象的“收藏夹状态”,而且如果服务器这样说,网络请求需要能够发出收藏夹需要失效的信号。

    当服务器响应404错误时,会发生此无效部分,指示必须从用户的收藏夹中删除收藏夹对象。network fetch函数引发错误,这会触发FavoritesController删除对象,然后向相关方发送需要刷新的通知。

    问题所在

    当使用单元测试检查404实现的质量时,所有方法都会按预期触发–抛出并捕获错误,FavoritesController删除对象并发送通知。但在某些情况下,删除的收藏夹仍然存在,但这取决于查询的位置!

    如果我在单例中进行查询,则删除操作正常,但如果我从使用单例的类中进行查询,则不会发生删除操作。

    设计细节

    • FavoritesController属性 favorites 对所有访问使用ivar @synchronized() ,并且ivar的值由NSUserDefaults属性支持。
    • 收藏夹对象是具有两个键的NSDictionary: id name

    其他信息

    • 有一件奇怪的事情我不明白为什么会发生:在一些删除尝试中 名称 收藏夹对象的值设置为 "" 但是 身份证件 键保留其值。

    • 我编写了单元测试,添加一个无效的收藏夹,并检查它是否在第一次服务器查询时被删除。当从一组空收藏夹开始时,此测试通过,但当存在如上所述的“半删除”对象的实例(保留其 身份证件 值)

    • 单元测试现在一直通过,但在实际使用中,删除失败仍然存在。我怀疑这是由于NSUserDefaults没有立即保存到磁盘。

    我试过的步骤

    • 确保singleton实现是“真正的”singleton,即。 sharedController 始终返回相同的实例。
    • 我原以为存在某种“捕获”问题,即闭包会将自己的副本保存在过时的收藏夹中,但我认为不是这样。在记录对象ID时,它会返回相同的ID。

    密码

    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")
        
    }
    
    1 回复  |  直到 4 年前
        1
  •  1
  •   allenh    7 年前

    为了给我的答案提供上下文,下面是建议更改时所讨论的代码的样子:

    - (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;
    }
    

    我怀疑这张支票 if (!internalFavorites) { 随后 @synchronized(internalFavorites) 因为这意味着 @synchronized 正在通过 nil 哪一个 results in a noop

    这意味着要多次调用 favorites setFavorites 可能会以有趣的方式发生,因为它们实际上不会同步。给予 @sychronized 要同步的实际对象对于线程安全至关重要。在self上同步是可以的,但是对于一个特定的类,您必须小心不要在self上同步太多的东西,否则您将不可避免地创建不必要的阻塞。提供简单的 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;
    }
    

    关于测试运行之间的异常情况,明确要求 synchronize NSUserDefaults 将有所帮助,因为更改默认值的调用是异步的,这意味着涉及其他线程。还有3层缓存,专门用于运行测试 使同步 应该确保在Xcode拔掉测试运行的插头之前,事情已经完全、干净地提交。文档非常突然地坚持认为这不是必要的调用,但如果确实没有必要,它就不存在:-)。在我的第一个iOS项目中,我们总是打电话 使同步 每次更改默认值后。。。因此,我认为文档对苹果工程师来说更具吸引力。我很高兴这种直觉帮助了你。