Помилка під час використання NSMutableSet

Я отримую помилку

* Terminating app due to uncaught exception 'NSGenericException', reason: '* Collection <__NSCFSet: 0x6b66390> was mutated while being enumerated.'

при додаванні нового делегата до мого класу. Або, принаймні, саме там я вважаю, що проблема.

Це мій код: MyAppAPI.m

[...]
static NSMutableSet *_delegates = nil;

@implementation MyAppAPI

+ (void)initialize
{
    if (self == [MyAppAPI class]) {
        _delegates = [[NSMutableSet alloc] init];
    }
}

+ (void)addDelegate:(id)delegate
{
    [_delegates addObject:delegate];
}

+ (void)removeDelegate:(id)delegate
{
    [_delegates removeObject:delegate];
}
[...]

@end

MyAppAPI is a singleton which I can use throughout my application. Wherever I can (or should be able to) do: [MyAppAPI addDelegate:self].
This works great, but only in the first view. This view has a UIScrollView with PageViewController which loads new views within itself. These new views register to MyAppAPI to listen to messages until they are unloaded (which in that case they do a removeDelegate). However, it seems to me that it dies directly after I did a addDelegate on the second view in the UIScrollView.

How could I improve the code so that this doesn't happen?

Update
I'd like to clarify me a bit further. What happens is that view controller "StartPage" has an UIScrollView with a page controller. It loads several other views (1 ahead of the current visible screen). Each view is an instans PageViewController, which registers itself using the addDelegate function shown above to the global singleton called MyAppAPI. However, as I understand this viewcontroller 1 is still reading from the delegate when viewcontroller 2 registers itself, hence the error shows above.

Я сподіваюся, що я зробив сценарій ясним. Я спробував кілька речей, але нічого не допомагає. Мені потрібно зареєструватися для делегатів за допомогою addDelegate навіть під час читання від делегатів. Як це зробити?

Update 2 This is one of the reponder methods:

+ (void)didRecieveFeaturedItems:(NSArray*)items
{   
    for (id delegate in _delegates)
    {
        if ([delegate respondsToSelector:@selector(didRecieveFeaturedItems:)])
            [delegate didRecieveFeaturedItems:items];
    }
}
1
@frowing Я думаю, що я
додано Автор Paul Peelen, джерело
@ NSResponder Я подивився на те, як ASIHttpRequest робив речі і гуглів, як створити клас делегатів з кількома слухачами. Це був кінцевийрезультат.
додано Автор Paul Peelen, джерело
Так, я боюся, що це проблема. Але як я її вирішу? Я спробував syncronize як @AndrewZimmer пояснив, але це не допомогло.
додано Автор Paul Peelen, джерело
Ваша проблема майже напевно змінює [делегат didReceiveFeaturedItems] об'єкт _delegates .
додано Автор Hot Licks, джерело
Чому у вас є кілька делегатів, а не шаблон сповіщень?
додано Автор NSResponder, джерело
Під час спроби додати нового представника ви повторюєте набір?
додано Автор Fran Sevillano, джерело

6 Відповіді

Скотт Хантер має рацію. Ця помилка виникає під час спроби редагування списку під час ітерації.

Ось приклад того, що ви можете робити.

+ (void)iteratingToRemove:(NSArray*)items {   
    for (id delegate in _delegates) {
        if(delegate.removeMePlease) {
          [MyAppAPI removeDelegate:delegate];  //error you are editing an NSSet while enumerating
        }
    }
}

І ось як ви маєте справу з цим правильно:

+ (void)iteratingToRemove:(NSArray*)items
{   
    NSMutableArray *delegatesToRemove = [[NSMutableArray alloc] init];
    for (id delegate in _delegates) {
        if(delegate.removeMePlease) {
          [delegatesToRemove addObject:delegate];
        }
    }

    for(id delegate in delegatesToRemove) {
         [MyAppAPI removeDelegate:delegate];  //This works better
    }

    [delegatesToRemove release];
}
11
додано
Це має працювати досить добре
додано Автор vodkhang, джерело
В порядку. Ви маєте на увазі, що я повинен замінити мій removeDelegate тим, і замість того, щоб мого класу видалити делегат, я встановлюю змінну з назвою removeMePlease на true у самому класі делегатів. Я правий?
додано Автор Paul Peelen, джерело
Важливою частиною цього прикладу є те, що кожного разу, коли ви редагуєте ваш масив, НЕ робите це під час перерахування. Зберігаючи елементи, які потрібно редагувати у тимчасовому масиві, можна перерахувати, а потім змінити. Зверніть увагу на різницю між першим і другим кодовим блоком.
додано Автор Andrew Zimmer, джерело

Помилка вказує на те, що в той час, як деякий код знаходиться десь у середині перегляду вашого списку, ви змінюєте список (що пояснює збій після виклику addDelegate). Якщо код, що виконує перерахування, змінює список, то потрібно просто відкласти модифікації, поки не буде виконано перерахування (скажімо, збираючи їх у іншому списку). Не знаючи нічого про код, який робить перерахування, не можна сказати більше, ніж це.

5
додано
Як би я "Відклав модифікації, поки не буде зроблено перелік"? Чи є спосіб зробити це, тому що об'єкт, що викликає, ніколи не зрозуміє, що це відбувається в моєму делегаті, коли він реєструє або реєструє себе делегат. (Сподіваюся, що мій коментар не є незрозумілим);)
додано Автор Paul Peelen, джерело
? Будь-які пропозиції до мого коментаря?
додано Автор Paul Peelen, джерело

Просте рішення, не використовуйте змінюваний набір. Вони небезпечні з різних причин, у тому числі і з цього.

Ви можете використовувати -copy і -mutableCopy для перетворення між змінюваними і незмінними версіями NSSet (і багатьох інших класів). Будьте обережні, всі методи копіювання повертають новий об'єкт з кількістю збережених 1 (так само, як і alloc), тому вам потрібно їх звільнити.

Окрім зменшення потенціалу помилок, об'єкти, що не піддаються зміні, швидше працюють з і використовують менше пам'яті.

[...]
static NSSet *_delegates = nil;

@implementation MyAppAPI

+ (void)initialize
{
    if (self == [MyAppAPI class]) {
        _delegates = [[NSSet alloc] init];
    }
}

+ (void)addDelegate:(id)delegate
{
    NSMutableSet *delegatesMutable = [_delegates mutableCopy];
    [delegatesMutable addObject:delegate];

    [_delegates autorelease];
    _delegates = [delegatesMutable copy];

    [delegatesMutable release];
}

+ (void)removeDelegate:(id)delegate
{
    NSMutableSet *delegatesMutable = [_delegates mutableCopy];
    [delegatesMutable removeObject:delegate];

    [_delegates autorelease];
    _delegates = [delegatesMutable copy];

    [delegatesMutable release];
}
[...]

@end
2
додано
оскільки значення _delegates є об'єктом NSSet , який є незмінним, він гарантовано ніколи не змінюватиметься, доки ви захочете (і збережете) вказівник на нього перед його використанням ваш потік.
додано Автор Abhi Beckert, джерело
ніколи не думав, що це може бути ще один хороший спосіб! .. спасибі.
додано Автор Ved, джерело
Я просто придумав проблему з цим підходом, він не є безпечним для потоку і може призвести до суперечливих результатів, якщо дві теми спробувати і додати одночасно. Ітерація над копією є єдиним рішенням (без замків).
додано Автор Ved, джерело

Scott Hunter має рацію - це проблема з модифікацією NSSet, коли ви перераховуєте елементи набору. Ви повинні мати трасування стека, звідки падає програма. Ймовірно, це рядок, де ви додаєте/видаляєте з набору _delegates. Це де потрібно внести зміни. Це легко зробити. Замість додавання/видалення з набору виконайте наведені нижче дії.

NSMutableSet *tempSet = [_delegates copy];
for (id delegate in _delegates)
{
    //add or remove from tempSet instead
}
[_delegates release], _delegates = tempSet;

Крім того, NSMutableSet не є безпечним для потоку , тому вам слід завжди дзвонити методам з основного потоку. Якщо ви ще не додали жодних додаткових потоків, вам нема чого турбувати.

1
додано

Що завжди пам'ятати проobjective-c"швидке перерахування".
Існує 2 великі відмінності між "швидким перерахуванням" і циклом for.

"швидке перерахування" швидше, ніж цикл for.
АЛЕ Ви не можете змінювати колекцію, що перераховується.

Ви можете запитати ваш NSSet для - (NSArray *) allObjects і перераховувати цей масив, змінюючи ваш NSSet.

0
додано

Ви отримуєте цю помилку, коли потік намагається змінити (додати, видалити) масив, а інший потік перебирає його.

Один із способів вирішення цієї проблеми за допомогою NSLock або синхронізації методів. Те, що способи додавання, видалення і повторення методів не можна викликати паралельно. Але це матиме вплив на продуктивність і/або швидкість реагування, оскільки будь-яке додавання/видалення доведеться чекати на потік, який перебирається по масиву.

Кращим рішенням, натхненим від CopyOnWriteArrayList від Java, було б створити копію масиву і повторити копію. Таким чином, єдина зміна у вашому коді буде: -

//better solution
+ (void)didRecieveFeaturedItems:(NSArray*)items
{   
    NSArray *copyOfDelegates = [_delegates copy]
    for (id delegate in copyOfDelegates)
    {
        if ([delegate respondsToSelector:@selector(didRecieveFeaturedItems:)])
            [delegate didRecieveFeaturedItems:items];
    }
}

Рішення з використанням замків з продуктивністю впливу

//not a good solution

+ (void)addDelegate:(id)delegate
{
    @synchronized(self){
        [_delegates addObject:delegate];
    }
}

+ (void)removeDelegate:(id)delegate
{
    @synchronized(self){
        [_delegates removeObject:delegate];
   }
}

+ (void)didRecieveFeaturedItems:(NSArray*)items
{   
    @synchronized(self){
        for (id delegate in _delegates)
        {
            if ([delegate respondsToSelector:@selector(didRecieveFeaturedItems:)])
                [delegate didRecieveFeaturedItems:items];
        }
    }
}
0
додано
IT KPI iOS
IT KPI iOS
74 учасників

Чат обсуждения IOS. - Оффтоп, флуд, оскорбления и вбросы здесь не приняты. - За нарушение - предупреждение или mute на неделю. - За спам и рекламу - ban. Все чаты IT KPI: https://t.me/itkpi/602

ios_jobs_ua
ios_jobs_ua
27 учасників

Mobile Dev Jobs UA
Mobile Dev Jobs UA
20 учасників

Публикуем вакансии и запросы на поиск работы по направлению iOS, Android, Xamarin, RN и т.д.