setNeedsDisplayInRect вызывается во время drawLayer:inContext:

Я использую CATiledLayer в своем приложении, и в результате рисование этого слоя выполняется в фоновом потоке. То есть метод drawLayer:inContext: моего делегата вызывается из фонового потока. setNeedsDisplayInRect, используемый для аннулирования частей CATiledLayer, всегда вызывается из основного потока.

Поскольку они являются независимыми потоками, иногда бывает так, что setNeedsDisplayInRect вызывается, когда фоновый поток находится в методе drawLayer:inContext. Я заметил, что setNeedsDisplayInRect в этой ситуации игнорируется (drawLayer:inContext больше не вызывается).

Я зарегистрировал ошибку в Apple , потому что я считаю, что это неправильно. Но мне трудно понять, как обойти эту ситуацию. У вас есть хорошие идеи?

РЕДАКТИРОВАТЬ:

Я проверил ответ Станислава, используя следующий код:

- (void) setNeedsDisplayInRect:(CGRect)rect
{
    NSLog(@"setNeedsDisplayInRect:%@", NSStringFromCGRect(rect));
    [super setNeedsDisplayInRect:rect];
}

- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)gc
{
    CGRect bounds = CGContextGetClipBoundingBox(gc);
    NSLog(@"drawLayer:inContext: bounds=%@", NSStringFromCGRect(bounds));

    dispatch_async(dispatch_get_current_queue(), ^{
        [self setNeedsDisplayInRect:bounds];
    });

    CGContextSetFillColorWithColor(gc, testColor.CGColor);
    CGContextFillRect(gc, bounds);
    sleep(0.2); // simulate the time it takes to draw complicated graphics
    NSLog(@"end drawLayer:inContext: bounds=%@", NSStringFromCGRect(bounds));
}

Как указано, код вызывает бесконечное повторение рисования, но иногда между setNeedsDisplayInRect: и соответствующим drawLayer:inContext: возникает задержка до 5 секунд, в которой больше ничего не происходит. См. журнал ниже в качестве примера. Обратите внимание на нестандартное поведение: в первую секунду некоторые плитки перерисовываются несколько раз, другие — только один раз. Затем следует пауза в 5 секунд, и цикл начинается заново.

Это было проверено на симуляторе с IOS6.0 (я выбираю эту версию, потому что в более ранних версиях есть еще одна ошибка, исправленная в 6.0: они иногда рисуют одни и те же плитки дважды).

2012-10-27 15:51:38.771 TiledLayerTest[39934:15a13] drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:38.774 TiledLayerTest[39934:15a13] end drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:38.774 TiledLayerTest[39934:1570f] drawLayer:inContext: bounds={{300, 0}, {20, 300}}
2012-10-27 15:51:38.776 TiledLayerTest[39934:1570f] end drawLayer:inContext: bounds={{300, 0}, {20, 300}}
2012-10-27 15:51:38.776 TiledLayerTest[39934:1630b] setNeedsDisplayInRect:{{0, 300}, {300, 180}}
2012-10-27 15:51:38.777 TiledLayerTest[39934:1540f] setNeedsDisplayInRect:{{300, 0}, {20, 300}}
2012-10-27 15:51:38.780 TiledLayerTest[39934:15a13] drawLayer:inContext: bounds={{300, 0}, {20, 300}}
2012-10-27 15:51:38.781 TiledLayerTest[39934:15a13] end drawLayer:inContext: bounds={{300, 0}, {20, 300}}
2012-10-27 15:51:38.782 TiledLayerTest[39934:1540f] setNeedsDisplayInRect:{{300, 0}, {20, 300}}
2012-10-27 15:51:38.789 TiledLayerTest[39934:1570f] drawLayer:inContext: bounds={{0, 0}, {300, 300}}
2012-10-27 15:51:38.791 TiledLayerTest[39934:15a13] drawLayer:inContext: bounds={{300, 300}, {20, 180}}
2012-10-27 15:51:38.792 TiledLayerTest[39934:15a13] end drawLayer:inContext: bounds={{300, 300}, {20, 180}}
2012-10-27 15:51:38.793 TiledLayerTest[39934:1570f] end drawLayer:inContext: bounds={{0, 0}, {300, 300}}
2012-10-27 15:51:38.795 TiledLayerTest[39934:1540f] setNeedsDisplayInRect:{{0, 0}, {300, 300}}
2012-10-27 15:51:38.795 TiledLayerTest[39934:1540f] setNeedsDisplayInRect:{{300, 300}, {20, 180}}
2012-10-27 15:51:38.798 TiledLayerTest[39934:15a13] drawLayer:inContext: bounds={{0, 0}, {300, 300}}
2012-10-27 15:51:38.800 TiledLayerTest[39934:15a13] end drawLayer:inContext: bounds={{0, 0}, {300, 300}}
2012-10-27 15:51:38.802 TiledLayerTest[39934:1630b] setNeedsDisplayInRect:{{0, 0}, {300, 300}}
2012-10-27 15:51:38.806 TiledLayerTest[39934:1570f] drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:38.808 TiledLayerTest[39934:1630b] setNeedsDisplayInRect:{{0, 300}, {300, 180}}
2012-10-27 15:51:38.809 TiledLayerTest[39934:1570f] end drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:38.813 TiledLayerTest[39934:15a13] drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:38.816 TiledLayerTest[39934:1630b] setNeedsDisplayInRect:{{0, 300}, {300, 180}}
2012-10-27 15:51:38.816 TiledLayerTest[39934:15a13] end drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:43.774 TiledLayerTest[39934:1540f] drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:43.776 TiledLayerTest[39934:1540f] end drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:43.776 TiledLayerTest[39934:1630f] drawLayer:inContext: bounds={{0, 0}, {300, 300}}

person fishinear    schedule 16.11.2011    source источник
comment
вы также должны увидеть этот вопрос, поскольку первый ответ может показаться иметь лучшее решение.   -  person Grady Player    schedule 16.11.2011
comment
Я думал об этом, но подумал, что это может привести к переключению потоков сразу после моего кода в drawLayer:inContext:, до того, как ОС получит возможность сбросить флаг setNeedsDisplay (или что-то еще, что она использует внутри, чтобы определить, нужно ли ей вызывать снова нарисовать слой). Это все равно вызовет ту же проблему.   -  person fishinear    schedule 16.11.2011
comment
все, что вы хотите предотвратить, это установка флага needDisplay во время рисования, вы можете легко сделать это с помощью спин-блокировки или мьютекса, вы, по сути, хотите заблокировать не рисующий поток на время обновления   -  person Grady Player    schedule 17.11.2011
comment
И да, и нет — я хочу отложить установку флага needDisplay на время рисования и до тех пор, пока код Apple не сбросит флаг. Надеюсь, Apple сбрасывает его сразу после моего кода рисования, что уменьшит вероятность того, что он пойдет не так, но все равно будет состояние гонки.   -  person fishinear    schedule 21.11.2011
comment
Вы когда-нибудь находили решение этого? У меня аналогичная проблема с MapKit, который использует CATiledLayer...   -  person TomSwift    schedule 14.04.2012
comment
К сожалению нет. Я также не получил ответа от Apple. Я думаю, что лучшее, что вы можете сделать, это отправить setNeedsDisplayInRect с небольшой задержкой, чтобы он пришел после завершения drawLayer:inContext:.   -  person fishinear    schedule 14.04.2012
comment
Если это проблема в вашей работе, то зарегистрируйте ошибку и в Apple. Это повышает приоритет для Apple.   -  person fishinear    schedule 14.04.2012
comment
@fishinear, у вас есть какой-нибудь прогресс в этом вопросе с тех пор?   -  person Stanislav Pankevich    schedule 11.10.2012
comment
@Станислав Не совсем так. И я не получил обратной связи от Apple. Я пошел дальше и сделал свою собственную реализацию мозаичного слоя из-за этого и других ограничений CATiledLayer.   -  person fishinear    schedule 12.10.2012
comment
@fishinear, я думаю, этот вопрос, вероятно, сейчас не актуален для вас, но не могли бы вы просмотреть ответ, который я опубликовал?   -  person Stanislav Pankevich    schedule 27.10.2012
comment
@fishinear, мои небольшие извинения: я как-то упустил из виду, что вы работали с CATilesLayer, в то время как моя проблема связана с методами canDrawMapRect и DrawMapRect MapKit для оверлеев ‹MKOverlay›. Хотя мне кажется, что эта проблема с потоками общая для обоих. Я не вижу 5-секундных задержек с использованием dispatch_async в случае наложений моей карты.   -  person Stanislav Pankevich    schedule 27.10.2012
comment
@Stanislaw Я подозреваю, что MapKit использует CATiledLayer внизу, отсюда и подобное поведение. И я понятия не имею, откуда берутся 5 секунд. Я подозреваю, что не имеет большого значения, из какого потока вы вызываете setNeedsDisplay, если вы убедитесь, что он не находится в середине drawLayer:inContext: (или canDraw... в вашем случае). Самый простой способ сделать это, ИМХО, по-прежнему вызывать его после небольшой задержки.   -  person fishinear    schedule 28.10.2012


Ответы (2)


Я опубликовал свой ответ на аналогичную проблему: setNeedsDisplayInMapRect не срабатывает новый drawMapRect: call (просто ссылка, чтобы не дублировать ответ здесь).

Кратко: вы должны отправить вызов метода setNeedsDisplayInRect в dispatch_get_main_queue().

person Stanislav Pankevich    schedule 26.10.2012
comment
Спасибо за ваши усилия, но, к сожалению, это не работает надежно. Прежде всего, когда я тестирую ваш подход, он продолжает надежно выполнять перерисовку, но иногда возникает задержка до 5 секунд между setNeedsDisplayInRect и drawLayer:inContext. - person fishinear; 27.10.2012
comment
Кроме того, в практической программе вы бы не обнаружили, что вам нужно сделать перерисовку на потоке рисования. Обычно вы обнаружите это в основном потоке. Если я изменю ваш пример, чтобы выполнить setNeedsDisplayInRect в основном потоке (в get_main_queue() вместо get_current_queue()), то он снова выйдет из строя (вообще не перерисовывается). - person fishinear; 27.10.2012
comment
Просто предположение, по вашему первому комментарию: не думаете ли вы, что эти 5 секунд тратятся на обработку других плиток, которые были добавлены в очередь карты до того, как вы в dispatch_async'е установилиNeedsDisplayInRect, так что это в настоящее время обработаны, но после этих старых? - person Stanislav Pankevich; 27.10.2012
comment
Во-вторых: я проверил очередь, которая используется для плиток внутри тела метода canDraw... - я не знаю, как это связано с dispatch_get_main_queue() (нужно проверить!), но я я уверен, что эта очередь одинакова для всех вызовов canDraw... по карте. Просто интересно: вы вызываете setNeedsDisplatInRect внутри метода canDraw...? Было бы интересно посмотреть на код, который вы используете. Я сам усиленно думаю, чтобы иметь какую-либо окончательную точку по этому вопросу. - person Stanislav Pankevich; 27.10.2012
comment
Я добавлю то, что я сделал к вопросу, чтобы было легче говорить об этом. - person fishinear; 27.10.2012

Вы можете попробовать использовать NSRecursiveLock таким образом:

- (void) setNeedsDisplayInRect:(CGRect)rect
{
    [self.lock lock]
    NSLog(@"setNeedsDisplayInRect:%@", NSStringFromCGRect(rect));
    [super setNeedsDisplayInRect:rect];
    [self.lock unlock]
}

- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)gc
{
    [self.lock lock]
    CGRect bounds = CGContextGetClipBoundingBox(gc);
    NSLog(@"drawLayer:inContext: bounds=%@", NSStringFromCGRect(bounds));


    // drawing code


    NSLog(@"end drawLayer:inContext: bounds=%@", NSStringFromCGRect(bounds));
    [self.lock unlock]
}

Это гарантирует, что setNeedsDisplayInRect не будет вызываться во время рисования. Однако это может повлиять на производительность, поскольку lock может заблокировать основной поток, и вы не сможете рисовать несколько плиток параллельно.

person Felix    schedule 27.10.2012
comment
Есть еще один недостаток, помимо тех, которые вы сами уже указали. Из-за того, как работает обработка потоков, если другой поток ожидает блокировки, разблокировка в конце drawLayer:inContext: в значительной степени гарантирует, что произойдет переключение потока. Это означает, что ожидающий setNeedsDisplayInRect: всегда будет выполняться до возврата drawLayer:inContext:. И это именно то, чего мы хотели избежать. - person fishinear; 27.10.2012