Свойство Singleton возвращает разные значения в зависимости от вызова - PullRequest
0 голосов
/ 25 апреля 2018

Фон

В моем приложении у меня есть класс FavoritesController, который управляет объектами, которые пользователь пометил как избранные, и этот статус избранного затем используется во всем приложении.FavoritesController разработан как одноэлементный класс, так как в приложении имеется ряд элементов пользовательского интерфейса, которым необходимо знать «статус избранного» для объектов в разных местах, а также сетевые запросы должны иметь возможность сигнализировать о том, что избранное должно быть признано недействительным.если сервер так говорит.

Эта часть аннулирования происходит, когда сервер отвечает ошибкой 404, указывающей, что избранный объект должен быть удален из избранного пользователя.Функция сетевого извлечения выдает ошибку, которая запускает FavoritesController для удаления объекта, а затем отправляет заинтересованным сторонам уведомление о необходимости обновления.

Проблема

При использовании модульного теста дляпроверьте качество реализации 404, все методы запускаются как задумано - ошибка выдается и перехватывается, FavoritesController удаляет объект и отправляет уведомление.В некоторых случаях, однако, удаленный фаворит все еще там - но это зависит от того, где запрос сделан!

Если я запрашиваю внутри одиночного, удаление прошло нормально, но если я запрашиваю из класса, который используетдля синглтона удаление не произошло.

Сведения о проекте

  • Свойство FavoritesController favorites использует ивар со всеми доступами @synchronized() и значением ивара.поддерживается свойством NSUserDefaults.
  • Любимым объектом является NSDictionary с двумя ключами: id и name.

Другая информация

  • Одна странная вещь, которую я не понимаю, почему это происходит: при некоторых попытках удаления значение name для избранного объекта устанавливается равным "", но ключ id сохраняет свое значение.

  • Я написал модульные тесты, которые добавляют недопустимое избранное и проверяют, удаляется ли оно при первом запросе к серверу.Этот тест проходит при запуске с пустым набором избранного, но не проходит, когда существует экземпляр «полуудаленного» объекта, как указано выше (который сохраняет свое значение id)

  • ЕдиницаТестирование теперь последовательно проходит, но в режиме реального времени не удаляется удаление.Я подозреваю, что это связано с тем, что NSUserDefaults не сохраняет данные на диск сразу.

Шаги, которые я пробовал

  • Убедившись, что реализация синглтона является 'истинным' синглтоном,т. е. sharedController всегда возвращает один и тот же экземпляр.
  • Я думал, что существует какая-то проблема 'захвата', когда закрытие будет сохранять свою собственную копию с устаревшими фаворитами, но я думаю, что нет.При NSLogging 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 Ответ

0 голосов
/ 05 мая 2018

Чтобы дать контекст вокруг моих ответов, вот как выглядел соответствующий код при предложении изменения:

- (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, что приведет к noop .

Это означало, что множественные вызовы favorites или setFavorites могут произойти смешноспособы, так как они на самом деле не будут синхронизированы.Предоставление @sychronized фактического объекта для синхронизации имело решающее значение для безопасности потока.Синхронизация на себя - это хорошо, но для определенного класса вы должны быть осторожны, чтобы не синхронизировать слишком много вещей на себя, иначе вы будете вынуждены создавать ненужную блокировку.Предоставление простых NSObject с @sychronized - это хороший способ сузить объем защищаемой информации.

Вот как можно избежать использования 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 уровня кеширования, и специально для выполнения тестов synchronize должен гарантировать, что все будет полностью и безоговорочно зафиксировано, прежде чем Xcode отключит пробный запуск.Документация очень резко настаивает на том, что это необязательный вызов, но если бы он действительно не был необходим, он бы не существовал :-).В моих первых проектах для iOS мы всегда звонили synchronize после каждого изменения по умолчанию ... так что я думаю, что документация более важна для инженеров Apple.Я рад, что эта интуиция помогла тебе.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...